PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.2
Pay with Vipps and MobilePay for WooCommerce v6.2.2
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 5.4.1 5.4.0 All 185 releases
← All changes | payment/Vipps.class.php +902 -588 6.0.46.2.2 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 }
@@ -114,12 +121,17 @@
114 121 add_action('init',array($Vipps,'init'));
115 122 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
116 123 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
117 124 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
118 - // 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
119 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
120 127 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
121 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);
122 134 }
123 135
124 136 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
125 137 public function woocommerce_blocks_loaded() {
@@ -234,9 +246,9 @@
234 246 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
235 247
236 248 // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
237 249 add_action('rest_api_init', function() {
238 - register_rest_route('woo-vipps/v1', '/express-products', [
250 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
239 251 'methods' => 'GET',
240 252 'callback' => [$this, 'rest_express_checkout_products'],
241 253 'permission_callback' => '__return_true',
242 254 ]);
@@ -271,8 +283,50 @@
271 283 add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
272 284 add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
273 285 }
274 286
287 + // Set default button options, migrating any older setup IOK 2026-07-15
288 + $this->init_button_options();
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.
294 +
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');
304 + }
305 + };
306 + add_action('delete_post', $delete_special_page_id, 10, 2);
307 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
308 +
309 + $this->ensure_special_page_exists();
310 +
311 +
312 + // We want this special page to have a certain title and maybe special scripts and so on,
313 + // this gets run in template redirect for these pages.
314 + add_action('woo_vipps_before_handling_special_page', function ($action) {
315 + // Change title dynamically depending on action. LP 2026-09-02
316 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
317 +
318 + // If we are handling the 'wait for payment' action, we need to poll the order status before
319 + // we start producing content IOK 2026-09-21
320 + if ($action == 'wait_for_payment') {
321 + $this->handle_payment_poll_and_redirect();
322 + }
323 +
324 + });
325 +
326 + // Add an admin interface for this page as well IOK 2026-09-11
327 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
328 +
275 329 }
276 330
277 331 public function admin_init () {
278 332 $gw = $this->gateway();
@@ -392,9 +446,77 @@
392 446 }
393 447 }
394 448 }
395 449
450 +
451 + /** Ensure we have a special page for payment flows
452 + *
453 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
454 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
455 + **/
456 + public function ensure_special_page_exists() {
457 + if (static::get_special_page_id()) return;
458 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
396 459
460 + // 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
461 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
462 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
463 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
464 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
465 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
466 +
467 + // Ensure this page has the necessary shortcode. LP 2026-09-01
468 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
469 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
470 + wp_update_post([
471 + 'ID' => $old_special_page_id,
472 + 'post_content' => $new_content,
473 + ]);
474 + }
475 + } else {
476 + // Create special page if its missing. LP 2026-09-01
477 + $this->maybe_create_vipps_pages();
478 + }
479 + }
480 +
481 + // Admin interface for the special page on woo/advanced/pages
482 + public function woocommerce_settings_pages ($settings) {
483 + $i = -1;
484 + foreach($settings as $entry) {
485 + $i++;
486 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
487 + break;
488 + }
489 + }
490 + if ($i > 0) {
491 + $vippspagesettings = array(
492 + array(
493 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
494 + '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') ,
495 + 'id' => 'woocommerce_vipps_special_page_page_id',
496 + 'type' => 'single_select_page_with_search',
497 + 'default' => '',
498 + 'class' => 'wc-page-search',
499 + 'css' => 'min-width:300px;',
500 + 'args' => array(
501 + 'exclude' =>
502 + array(
503 + wc_get_page_id( 'myaccount' ),
504 + wc_get_page_id( 'checkout' ),
505 + wc_get_page_id( 'cart' ),
506 + ),
507 + ),
508 + 'desc_tip' => true,
509 + 'autoload' => false,
510 + ));
511 + array_splice($settings, $i, 0, $vippspagesettings);
512 + }
513 +
514 + return $settings;
515 + }
516 +
517 +
518 +
397 519 // Runs on init, adds the Vipps badge feature if activated
398 520 public function maybe_add_vipps_badge_feature () {
399 521 $badge_options = get_option('vipps_badge_options');
400 522 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -723,8 +845,11 @@
723 845
724 846 // Get current brand and language
725 847 $current_brand = strtolower($this->get_payment_method_name());
726 848 $current_language = $this->get_customer_language();
849 + if ('se' === $current_language) $current_language = 'sv';
850 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
851 + if ('dk' === $current_language) $current_language = 'da';
727 852
728 853 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
729 854 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
730 855 'purple'=> __('Purple', 'woo-vipps')];
@@ -787,9 +912,9 @@
787 912
788 913 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
789 914 <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>
790 915 <br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br>
791 - <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|dk} ] </pre><br>
916 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
792 917 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
793 918 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
794 919 </p>
795 920
@@ -816,21 +941,24 @@
816 941 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
817 942 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
818 943 }
819 944
820 - $options = get_option('vipps_button_options');
821 - if (isset($_POST['express']['variant'])) {
822 - $options['express']['variant'] = sanitize_title($_POST['express']['variant']);
945 + $old = get_option('vipps_button_options2', []);
946 + $new = $old;
947 + if (isset($_POST['express']['configs'])) {
948 + foreach ($_POST['express']['configs'] as $ctx => $config) {
949 + $sanitized_ctx = sanitize_title($ctx);
950 + $sanitized_config = map_deep($config, 'sanitize_title');
951 +
952 + // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
953 + if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
954 + $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
955 + } else {
956 + $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
957 + }
958 + }
823 959 }
824 - if (isset($_POST['express']['mini-variant'])) {
825 - $options['express']['mini-variant'] = sanitize_title($_POST['express']['mini-variant']);
826 - }
827 - if (isset($_POST['express']['force-mini']) && is_array($_POST['express']['force-mini'])) {
828 - foreach($_POST['express']['force-mini'] as $key => $val)
829 - $options['express']['force-mini'][$key] = sanitize_title($val);
830 - }
831 -
832 - update_option('vipps_button_options', $options);
960 + update_option('vipps_button_options2', $new);
833 961 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
834 962 exit();
835 963 }
836 964
@@ -864,9 +992,12 @@
864 992 public function vipps_mobilepay_badge_shortcode($atts) {
865 993 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
866 994
867 995 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : "";
868 - $language = in_array($args['language'], ['en','no', 'fi', 'dk']) ? $args['language'] : $this->get_customer_language();
996 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
997 + if ('se' === $language) $language = 'sv';
998 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
999 + if ('dk' === $language) $language = 'da';
869 1000 $id = sanitize_title($args['id']);
870 1001 $class = sanitize_text_field($args['class']);
871 1002
872 1003 $attributes = [];
@@ -882,13 +1013,18 @@
882 1013 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
883 1014 }
884 1015
885 1016 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1017 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
886 1018 public function vipps_badge_shortcode($atts) {
887 1019 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
888 1020
889 1021 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : "";
890 - $language = in_array($args['language'], ['en','no', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1022 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1023 + if ('se' === $language) $language = 'sv';
1024 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1025 + if ('dk' === $language) $language = 'da';
1026 +
891 1027 $id = sanitize_title($args['id']);
892 1028 $class = sanitize_text_field($args['class']);
893 1029
894 1030 $attributes = [];
@@ -902,163 +1038,318 @@
902 1038
903 1039 return "<vipps-badge $badgeatts></vipps-badge>";
904 1040 }
905 1041
906 - public function get_express_logo_variants() {
1042 + public function get_html_button_default_attrs() {
907 1043 return [
908 - 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
909 - 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
910 - 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
911 - 'express-pill' => __('Express pill', 'woo-vipps'),
912 - 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
913 - 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
1044 + 'language' => 'store',
1045 + 'variant' => 'primary',
1046 + 'rounded' => 'false',
1047 + 'verb' => 'buy',
1048 + 'stretched' => 'false',
1049 + 'compact' => 'false',
1050 + '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
914 1051 ];
915 1052 }
916 1053
1054 + public function get_html_button_attrs_for_context($context = 'global') {
1055 + $options = get_option('vipps_button_options2', []);
1056 + if (!is_string($context)) $context = 'global';
1057 + $config = $options['express']['configs'][$context] ?? [];
1058 + if (!$config || ($config['use-global-config'] ?? false)) {
1059 + $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
1060 + }
1061 + return $config;
1062 + }
917 1063
1064 + public function get_html_button_for_context($context = 'global') {
1065 + return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1066 + }
1067 +
1068 + // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1069 + // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1070 + public function get_html_button($attrs = []) {
1071 + $payment_method = $this->get_payment_method_name();
1072 + $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1073 + $attrs['brand'] = strtolower($payment_method);
1074 + $attrs['type'] = 'button'; // static
1075 +
1076 + // Support using store language
1077 + if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1078 + // Don't support these login verbs. LP 2026-06-04
1079 + if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1080 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1081 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1082 +
1083 + $escaped_attrs = [];
1084 + foreach($attrs as $k => $v) {
1085 + $escaped_attrs[$k] = esc_attr($v);
1086 + }
1087 +
1088 + // id attribute
1089 + $id = $escaped_attrs['id'] ?? '';
1090 + $id_str = $id ? "id='$id'" : '';
1091 +
1092 + // class attribute
1093 + $class_str = '';
1094 + if (isset($attrs['class'])) {
1095 + if (is_array($attrs['class'])) {
1096 + $class_str = implode(' ', $attrs['class']);
1097 + } else if (is_string($attrs['class'])) {
1098 + $class_str = $attrs['class'];
1099 + }
1100 + }
1101 +
1102 + // The html
1103 + $html = <<<EOF
1104 +<vipps-mobilepay-button
1105 + $id_str
1106 + $class_str
1107 + type="{$escaped_attrs['type']}"
1108 + brand="{$escaped_attrs['brand']}"
1109 + language="{$escaped_attrs['language']}"
1110 + variant="{$escaped_attrs['variant']}"
1111 + rounded="{$escaped_attrs['rounded']}"
1112 + verb="{$escaped_attrs['verb']}"
1113 + stretched="{$escaped_attrs['stretched']}"
1114 + compact="{$escaped_attrs['compact']}"
1115 +></vipps-mobilepay-button>
1116 +EOF;
1117 + return apply_filters('woo_vipps_html_button', $html, $attrs);
1118 + }
1119 +
918 1120 public function button_menu_page() {
919 1121 if (!current_user_can('manage_woocommerce')) {
920 1122 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
921 1123 }
922 - $payment_method = $this->get_payment_method_name();
923 - $lang = $this->get_customer_language();
924 - $button_options = get_option('vipps_button_options');
1124 + wp_enqueue_script('vipps-button-webcomponent');
1125 + ?>
1126 + <div class='wrap vipps-button-settings'>
1127 + <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1128 + <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1129 + <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1130 + <input type="hidden" name="action" value="update_vipps_button_settings" />
1131 + <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
925 1132
926 - $variants = $this->get_express_logo_variants();
927 - $mini_variants = array_filter($variants, fn($key) => str_ends_with($key, 'mini'), ARRAY_FILTER_USE_KEY);
1133 + <!-- Express section -->
1134 + <?php $this->button_menu_express_section(); ?>
928 1135
929 - $init_states = [
930 - 'express' => [
931 - 'variant' => array_key_exists(@$button_options['express']['variant'], $variants) ? $button_options['express']['variant'] : 'buy-now-rectangular',
932 - 'mini-variant' => array_key_exists(@$button_options['express']['mini-variant'], $mini_variants) ? $button_options['express']['mini-variant'] : 'express-rectangular-mini',
933 - 'force-mini' => [
934 - 'product' => @$button_options['express']['force-mini']['product'] ?? 'no',
935 - 'catalog' => @$button_options['express']['force-mini']['catalog'] ?? 'yes',
936 - 'cart' => @$button_options['express']['force-mini']['cart'] ?? 'no',
937 - 'minicart' => @$button_options['express']['force-mini']['minicart'] ?? 'no',
938 - ],
939 - ],
1136 + <!-- submit button -->
1137 + <div id="vipps-button-settings-save">
1138 + <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1139 + </div>
1140 + </form>
1141 + </div>
1142 + <?php
1143 + }
1144 +
1145 + private function button_menu_express_section() {
1146 + $options = get_option('vipps_button_options2', []);
1147 + $express = $options['express'] ?? [];
1148 + $configs = $express['configs'] ?? [];
1149 + $contexts = [
1150 + 'global' => __('Global', 'woo-vipps'),
1151 + 'product' => __('Product', 'woo-vipps'),
1152 + 'catalog' => __('Catalog', 'woo-vipps'),
1153 + 'cart' => __('Cart', 'woo-vipps'),
1154 + 'minicart' => __('Mini cart', 'woo-vipps'),
1155 + 'checkout' => __('Checkout', 'woo-vipps'),
940 1156 ];
1157 + $init_context = 'global';
1158 + $init_config = $configs[$init_context] ?? [];
941 1159
1160 + // html button args
1161 + $init_args = $init_config;
1162 + $init_args['id'] = 'vipps-button-express-preview';
1163 +
942 1164 ?>
943 - <div class='wrap vipps-button-settings'>
944 - <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
945 - <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
946 - <form class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1165 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1166 + <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
947 1167
948 - <!-- EXPRESS SECTION -->
949 - <div id="vipps-button-settings-express-container">
950 - <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
951 - <input type="hidden" name="action" value="update_vipps_button_settings" />
952 - <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
1168 + <!-- Context dropdown -->
1169 + <div id="vipps-button-settings-express-context">
1170 + <label>
1171 + <?php _e('Config context', 'woo-vipps'); ?>
1172 + </label>
1173 + <select id="context" onChange='updateContext()'>
1174 + <?php foreach($contexts as $key => $label): ?>
1175 + <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1176 + <?php echo $label ; ?>
1177 + </option>
1178 + <?php endforeach; ?>
1179 + </select>
1180 + <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>
1181 + </div>
1182 +
953 1183
954 - <!-- variant -->
955 - <div class="vipps-button-settings-section">
956 - <!-- variant dropdown -->
957 - <div class="vipps-button-settings-express-demo-container">
958 - <label for="vippsButtonVariant"><?php _e('Choose variant', 'woo-vipps'); ?></label>
959 - <select id="vippsButtonVariant" name="express[variant]" onChange='changeExpressVariant()'>
960 - <?php foreach($variants as $key=>$name): ?>
961 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['variant'] === $key) echo " selected "; ?> >
962 - <?php echo $name ; ?>
963 - </option>
964 - <?php endforeach; ?>
965 - </select>
966 - </div>
1184 + <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1185 + On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1186 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1187 + <fieldset>
1188 + <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1189 + <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1190 + <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1191 + </fieldset>
1192 + <fieldset>
1193 + <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1194 + <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1195 + <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1196 + <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1197 + <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1198 + <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1199 + <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1200 + <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1201 + <?php endif; ?>
1202 + </fieldset>
967 1203
968 - <!-- Preload all variant images. Javascript will show the active one. LP 2025-12-16 -->
969 - <div class="vipps-button-settings-express-demo-container vipps-button-settings-img-container">
970 - <?php foreach(array_keys($variants) as $variant): ?>
971 - <img
972 - class="vipps-button-settings-express-demo"
973 - id="vipps-button-settings-express-demo-<?php echo $variant; ?>"
974 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
975 - style="display: <?php echo ($variant === $init_states['express']['variant'] ? 'block' : 'none') ;?>;"
976 - >
977 - <?php endforeach; ?>
978 - </div>
979 - </div>
1204 + <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1205 + <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1206 + <?php endif; ?>
980 1207
1208 + <fieldset>
1209 + <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1210 + <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1211 + <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1212 + <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1213 + <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1214 + <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1215 + <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1216 + </fieldset>
1217 + <fieldset>
1218 + <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1219 + <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1220 + <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1221 + <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1222 + </fieldset>
1223 + </div>
981 1224
982 - <!-- mini variant section -->
983 - <div class="vipps-button-settings-section">
984 - <!-- Checkboxes "Use mini version for x page" -->
985 - <label><?php _e('Force mini variant in these contexts:', 'woo-vipps'); ?></label>
986 - <div class="vipps-button-settings-express-force-mini-container">
987 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-product"><?php _e('Product page', 'woo-vipps'); ?></label>
988 - <input name="express[force-mini][product]" type="hidden" value="no">
989 - <input name="express[force-mini][product]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['product'] == "yes") echo "checked";?>>
990 - </div>
1225 + <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1226 + <?php echo $this->get_html_button($init_args); ?>
1227 + </div>
991 1228
992 - <div class="vipps-button-settings-express-force-mini-container">
993 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-catalog"><?php _e('Catalog page', 'woo-vipps'); ?></label>
994 - <input name="express[force-mini][catalog]" type="hidden" value="no">
995 - <input name="express[force-mini][catalog]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['catalog'] == "yes") echo "checked";?>>
996 - </div>
1229 + <script>
1230 + // When inputs change, update the preview args. LP 2026-06-24
1231 + jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
997 1232
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-cart"><?php _e('Cart', 'woo-vipps'); ?></label>
1000 - <input name="express[force-mini][cart]" type="hidden" value="no">
1001 - <input name="express[force-mini][cart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['cart'] == "yes") echo "checked";?>>
1002 - </div>
1233 + let currentContext = '<?php echo $init_context; ?>';
1234 + let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1003 1235
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-minicart"><?php _e('Mini cart', 'woo-vipps'); ?></label>
1006 - <input name="express[force-mini][minicart]" type="hidden" value="no">
1007 - <input name="express[force-mini][minicart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['minicart'] == "yes") echo "checked";?>>
1008 - </div>
1236 + // Updates the actual html inputs from given config. LP 2026-07-01
1237 + function setInputsFromConfig(context, config) {
1238 + const useGlobalConfig = Boolean(config?.["use-global-config"]);
1239 + const isGlobal = "global" === context;
1009 1240
1010 - <!-- mini variant dropdown -->
1011 - <div class="vipps-button-settings-express-mini-demo-container">
1012 - <label for="vippsButtonMiniVariant"><?php _e('Choose variant to use in mini contexts', 'woo-vipps'); ?></label>
1013 - <select id="vippsButtonMiniVariant" name="express[mini-variant]" onChange='changeExpressMiniVariant()'>
1014 - <?php foreach($mini_variants as $key=>$name): ?>
1015 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['mini-variant'] === $key) echo " selected "; ?> >
1016 - <?php echo $name ; ?>
1017 - </option>
1018 - <?php endforeach; ?>
1019 - </select>
1020 - </div>
1241 + // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1242 + jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1021 1243
1022 - <!-- Preload mini variant imgs. LP 2025-12-17 -->
1023 - <div class="vipps-button-settings-express-mini-demo-container vipps-button-settings-img-container">
1024 - <?php foreach(array_keys($mini_variants) as $variant): ?>
1025 - <img
1026 - class="vipps-button-settings-express-mini-demo"
1027 - id="vipps-button-settings-express-mini-demo-<?php echo $variant; ?>"
1028 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
1029 - style="display: <?php echo ($variant === $init_states['express']['mini-variant'] ? 'block' : 'none') ;?>;"
1030 - >
1031 - <?php endforeach; ?>
1032 - </div>
1033 - </div>
1244 + // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1245 + if (!config || (!isGlobal && useGlobalConfig)) {
1246 + config = contextConfigs["global"];
1034 1247
1035 - <!-- END EXPRESS SECTION -->
1036 - </div>
1248 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1037 1249
1038 - <!-- Save button -->
1039 - <div id="vipps-button-settings-save">
1040 - <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1041 - </div>
1250 + // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1251 + jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1252 + } else {
1253 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1254 + jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1255 + }
1042 1256
1043 - </form>
1044 - </div>
1257 + Object.entries(config).forEach(([key, val]) => {
1258 + if ("use-global-config" === key) return;
1259 + const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1260 + const type = inputs.prop('type');
1261 + switch (type) {
1262 + case "checkbox":
1263 + inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1264 + break;
1265 + case "radio":
1266 + inputs.filter(`[value="${val}"]`).prop('checked', true);
1267 + break;
1268 + default:
1269 + console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1270 + }
1271 + });
1045 1272
1046 - <script>
1047 - function changeExpressVariant() {
1048 - const variant = jQuery('#vippsButtonVariant').val().trim();
1049 - // Show the one selected, hide all others. LP 2025-12-16
1050 - jQuery('.vipps-button-settings-express-demo').hide();
1051 - jQuery(`#vipps-button-settings-express-demo-${variant}`).show();
1052 - }
1273 + updatePreview();
1274 + }
1275 + // init the starting config from option. LP 2026-06-25
1276 + setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1053 1277
1054 - function changeExpressMiniVariant() {
1055 - const variant = jQuery('#vippsButtonMiniVariant').val().trim();
1056 - // Show the one selected, hide all others. LP 2025-12-16
1057 - jQuery('.vipps-button-settings-express-mini-demo').hide();
1058 - jQuery(`#vipps-button-settings-express-mini-demo-${variant}`).show();
1059 - }
1060 - </script>
1278 + // Update the preview web component's attributes. LP 2026-06-24
1279 + function updatePreview(event) {
1280 + const args = getPreviewArgs();
1281 + // 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
1282 + // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1283 + if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1284 + const button = jQuery('#vipps-button-express-preview');
1285 + button.attr(args);
1286 + }
1287 +
1288 + function getPreviewArgs() {
1289 + const args = {};
1290 + jQuery('#vipps-button-settings-express-args input').each(function () {
1291 + // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1292 + const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1293 + const attr = matches.length ? matches[matches.length - 1][1] : null;
1294 + if (!attr) {
1295 + console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1296 + return;
1297 + }
1298 + if (this.type === 'checkbox') {
1299 + args[attr] = this.checked;
1300 + } else if (this.checked) {
1301 + args[attr] = this.value;
1302 + }
1303 + });
1304 +
1305 + return args;
1306 + }
1307 +
1308 + // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1309 + function updateContext() {
1310 + const wasGlobal = "global" === currentContext;
1311 + const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1312 +
1313 + // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1314 + if (wasGlobal || !useGlobalConfig) {
1315 + contextConfigs[currentContext] = getPreviewArgs();
1316 + } else {
1317 + contextConfigs[currentContext] = {'use-global-config': true};
1318 + }
1319 +
1320 + // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1321 + const newContext = jQuery("#context").val();
1322 + const newConfig = contextConfigs[newContext];
1323 + setInputsFromConfig(newContext, newConfig);
1324 + currentContext = newContext;
1325 + }
1326 +
1327 + // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1328 + jQuery('#vipps-button-settings-form').on('formdata', e => {
1329 + const formData = e?.originalEvent?.formData;
1330 + if (!formData) return;
1331 +
1332 + // run this to store current context config before posting. LP 2026-06-24
1333 + updateContext();
1334 +
1335 + // now we can delete the current temporary config from post data. LP 2026-06-24
1336 + const keysToDelete = [];
1337 + for (const [key] of formData.entries()) {
1338 + if (key.startsWith("express[tmpConfig][")) {
1339 + keysToDelete.push(key);
1340 + }
1341 + }
1342 + keysToDelete.forEach(key => formData.delete(key));
1343 +
1344 + // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1345 + Object.entries(contextConfigs).forEach(([context, config]) => {
1346 + Object.entries(config).forEach(([key, val]) => {
1347 + formData.append(`express[configs][${context}][${key}]`, val);
1348 + });
1349 + });
1350 + });
1351 + </script>
1061 1352 <?php
1062 1353 }
1063 1354
1064 1355
@@ -1478,12 +1769,22 @@
1478 1769 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1479 1770 array(),
1480 1771 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1481 1772 [
1482 - 'in_footer' => true,
1483 - 'strategy' => 'async',
1773 + 'in_footer' => true,
1774 + 'strategy' => 'async',
1484 1775 ],
1485 1776 );
1777 +
1778 + // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1779 + wp_register_script('vipps-button-webcomponent',
1780 + plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1781 + array(),
1782 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1783 + [
1784 + 'in_footer' => false
1785 + ],
1786 + );
1486 1787 }
1487 1788
1488 1789 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1489 1790 public function script_add_vippslocale () {
@@ -1502,8 +1803,9 @@
1502 1803 $this->script_add_vippslocale();
1503 1804
1504 1805 wp_enqueue_script('vipps-gw');
1505 1806 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1807 + wp_enqueue_script('vipps-button-webcomponent');
1506 1808 }
1507 1809
1508 1810
1509 1811 public function add_shortcodes() {
@@ -1515,8 +1817,11 @@
1515 1817 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1516 1818 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1517 1819 // Legacy vipps-badge shortcode. LP 19.11.2024
1518 1820 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1821 +
1822 + // special page handling, previously a fake page. LP 2026-08-25
1823 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1519 1824 }
1520 1825
1521 1826
1522 1827 public function log ($what,$type='info') {
@@ -1542,8 +1847,10 @@
1542 1847 }
1543 1848
1544 1849 // Show express button option on checkout form. LP 2026-03-23
1545 1850 public function checkout_before_customer_details_express () {
1851 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1852 + do_action('woo_vipps_checkout_before_customer_details_express');
1546 1853 $gw = $this->gateway();
1547 1854 if (!$gw->show_express_checkout()) return;
1548 1855 $this->express_checkout_section_html();
1549 1856 }
@@ -1553,9 +1860,9 @@
1553 1860 $header_text = __('Express Checkout', 'woo-vipps');
1554 1861 $header = "<legend class='express-header'>$header_text</legend>";
1555 1862 $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1556 1863 echo "<fieldset class='$div_classes'>$header";
1557 - $this->cart_express_checkout_button_html();
1864 + $this->checkout_express_checkout_button_html();
1558 1865 echo '</fieldset>';
1559 1866 }
1560 1867
1561 1868 public function express_checkout_banner() {
@@ -1582,8 +1889,27 @@
1582 1889 <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1583 1890 <?php
1584 1891 }
1585 1892
1893 + public function checkout_express_checkout_button() {
1894 + $gw = $this->gateway();
1895 +
1896 + if ($gw->show_express_checkout()){
1897 + return $this->checkout_express_checkout_button_html();
1898 + }
1899 + }
1900 +
1901 + public function checkout_express_checkout_button_html() {
1902 + $url = $this->express_checkout_url();
1903 + $url = wp_nonce_url($url,'express','sec');
1904 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1905 + $method = $this->get_payment_method_name();
1906 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1907 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1908 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1909 + echo $html;
1910 + }
1911 +
1586 1912 // Show the express button if reasonable to do so
1587 1913 public function cart_express_checkout_button() {
1588 1914 $gw = $this->gateway();
1589 1915
@@ -1602,22 +1928,46 @@
1602 1928
1603 1929 public function cart_express_checkout_button_html($minicart = false) {
1604 1930 $url = $this->express_checkout_url();
1605 1931 $url = wp_nonce_url($url,'express','sec');
1606 - $page = $minicart ? 'minicart' : 'cart';
1607 - $imgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo($page));
1932 + $context = $minicart ? 'minicart' : 'cart';
1933 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1608 1934 $method = $this->get_payment_method_name();
1609 1935 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1610 - $button = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'><img alt='$title' border=0 src='$imgurl'></a>";
1611 - $button = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1612 - echo $button;
1936 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1937 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1938 + echo $html;
1613 1939 }
1614 1940
1615 1941 // 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
1616 1942 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1617 1943 public function buy_now_button_shortcode ($atts) {
1618 - $args = shortcode_atts( array( 'id' => '','variant'=>'','sku' => '',), $atts );
1619 - return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode') . "</div>";
1944 + // The new web component button args. LP 2026-07-02
1945 + $button_args = $this->get_html_button_default_attrs();
1946 + unset($button_args['brand']);
1947 +
1948 + // Variant exists for the product variant. LP 2026-07-02
1949 + if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
1950 + unset($button_args['variant']);
1951 +
1952 + $args = shortcode_atts(
1953 + array(...$button_args,
1954 + 'id' => '','variant'=> '','sku' => '',
1955 + ),
1956 + $atts,
1957 + );
1958 +
1959 + // Variant exists for the product variant. LP 2026-07-02
1960 + $button_args = $args;
1961 + if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
1962 + unset($button_args['button_variant']);
1963 + unset($button_args['sku']);
1964 + unset($button_args['id']);
1965 + // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1966 + // "Note also that there is a bug in the library, and it currently only renders one language per page."
1967 + // 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
1968 +
1969 + return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
1620 1970 }
1621 1971
1622 1972 // 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
1623 1973 public function express_checkout_button_shortcode() {
@@ -2286,77 +2636,69 @@
2286 2636 return null;
2287 2637 }
2288 2638 }
2289 2639
2640 + // Special pages, and some callbacks. IOK 2018-05-18
2641 + public function template_redirect() {
2290 2642
2291 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2292 - public function pre_handle_404($current, $query) {
2293 - if (!is_admin()) {
2294 - $special = $this->is_special_page();
2295 - if ($special) {
2296 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2297 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2298 - return true;
2299 - }
2643 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2644 + // We redirect these to the new location.
2645 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2646 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2647 + $url = static::get_special_page_url();
2648 + $_GET['action'] = 'buy_product';
2649 + $q = build_query($_GET);
2650 + wp_redirect($url . "?" . $q, 302);
2651 + exit();
2300 2652 }
2301 - return $current;
2302 - }
2303 2653
2304 - // Special pages, and some callbacks. IOK 2018-05-18
2305 - public function template_redirect() {
2306 - global $post;
2307 - // Handle special callbacks
2308 - $special = $this->is_special_page() ;
2309 -
2310 - if ($special) {
2654 + if (static::is_special_page()) {
2655 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2311 2656 remove_filter('template_redirect', 'redirect_canonical', 10);
2312 - do_action('woo_vipps_before_handling_special_page', $special);
2657 + // dont cache special page. LP 2026-08-25
2658 + $this->nocache();
2659 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2660 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2661 + }
2662 + }
2313 2663
2314 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2315 - // to intercept express checkout etc. IOK 2022-03-18
2316 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2317 - $this->$special();
2318 - }
2319 - }
2664 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2665 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2666 + global $wp_query;
2667 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2320 2668
2321 - $consentremoval = $this->is_consent_removal();
2322 - if ($consentremoval) {
2323 - remove_filter('template_redirect', 'redirect_canonical', 10);
2324 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2325 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2326 - $this->vipps_consent_removal_callback($consentremoval);
2669 + // In block themes the whole template (header, footer, content) renders inside the main
2670 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2671 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2672 + // the queried page so an earlier title doesn't consume this one-shot filter.
2673 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2674 + switch ($_GET['action'] ?? '') {
2675 + case 'wait_for_payment':
2676 + $title = __('Processing order', 'woo-vipps');
2677 + break;
2678 + case 'do_express_checkout':
2679 + case 'buy_product':
2680 + $title = __('Express Checkout', 'woo-vipps');
2681 + break;
2327 2682 }
2328 2683 }
2684 + return $title;
2329 2685 }
2686 +
2330 2687 // Template handling for special pages. IOK 2018-11-21
2688 + // 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
2331 2689 public function template_include($template) {
2332 - $special = $this->is_special_page() ;
2333 - if ($special) {
2690 + if (static::is_special_page()) {
2334 2691 // Get any special template override from the options IOK 2020-02-18
2335 2692 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2336 2693 $found = locate_template($specific,false,false);
2337 2694 if ($found) $template=$found;
2338 2695
2339 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2696 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2340 2697 }
2341 2698 return $template;
2342 2699 }
2343 2700
2344 -
2345 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2346 - private function is_consent_removal () {
2347 -
2348 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2349 - if ( !get_option('permalink_structure')) {
2350 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2351 - return false;
2352 - }
2353 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2354 - return @$_REQUEST['callback'];
2355 - }
2356 - return false;
2357 - }
2358 -
2359 2701 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2360 2702 // the user if using Express Checkout IOK 2020-10-09
2361 2703 public function woocommerce_before_thankyou ($orderid) {
2362 2704 $order = wc_get_order($orderid);
@@ -2361,9 +2703,9 @@
2361 2703 public function woocommerce_before_thankyou ($orderid) {
2362 2704 $order = wc_get_order($orderid);
2363 2705 if ($order) {
2364 2706 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2365 - // -- 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
2707 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2366 2708 $this->maybe_log_in_user($order);
2367 2709 $order->delete_meta_data('_vipps_limited_session');
2368 2710 $order->save();
2369 2711
@@ -2424,9 +2766,8 @@
2424 2766
2425 2767 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2426 2768 // IOK 2025-11-19
2427 2769 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2428 -
2429 2770 }
2430 2771
2431 2772 public function get_payment_method_name() {
2432 2773 return $this->gateway()->get_option('payment_method_name');
@@ -2446,14 +2787,13 @@
2446 2787 public function after_setup_theme() {
2447 2788 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2448 2789 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2449 2790
2450 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2791 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2451 2792 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2452 2793 // is important.
2453 2794 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2454 2795
2455 -
2456 2796 // Callbacks use the Woo API IOK 2018-05-18
2457 2797 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2458 2798 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2459 2799
@@ -2464,9 +2804,9 @@
2464 2804 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2465 2805 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2466 2806
2467 2807 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2468 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2808 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2469 2809 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2470 2810
2471 2811 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2472 2812 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
@@ -2471,12 +2811,10 @@
2471 2811 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2472 2812 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2473 2813
2474 2814
2475 - // Special pages and callbacks handled by template_redirect
2476 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2815 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2477 2816 add_action('template_redirect', array($this,'template_redirect'),1);
2478 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2479 2817
2480 2818 // Allow overriding their templates
2481 2819 add_filter('template_include', array($this,'template_include'), 10, 1);
2482 2820
@@ -2583,9 +2921,8 @@
2583 2921 $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());
2584 2922 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2585 2923 $this->vippsJSConfig['vippslocale'] = get_locale();
2586 2924 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2587 - $this->vippsJSConfig['logoSvgUrl'] = $this->get_payment_logo('buy-now-block');
2588 2925
2589 2926
2590 2927 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2591 2928 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
@@ -2763,14 +3100,14 @@
2763 3100
2764 3101 $raw_post = @file_get_contents( 'php://input' );
2765 3102 $result = @json_decode($raw_post,true);
2766 3103
2767 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3104 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2768 3105 // .. and the epayment webhooks 2023-12-19
2769 3106 $ischeckout = false;
2770 3107 $iswebhook = false;
2771 3108 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2772 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3109 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2773 3110 if ($callback == 'checkout') {
2774 3111 $ischeckout = true;
2775 3112 }
2776 3113 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3184,10 +3521,10 @@
3184 3521 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3185 3522 exit();
3186 3523 }
3187 3524
3188 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3189 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3525 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3526 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3190 3527 // This needs to be done before "calculate totals".
3191 3528 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3192 3529 $ischeckout = $order->get_meta('_vipps_checkout');
3193 3530
@@ -3363,9 +3700,9 @@
3363 3700 ), 'debug');
3364 3701
3365 3702 }
3366 3703
3367 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3704 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3368 3705 // 'dynamic pricing' 2023-01-26
3369 3706 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3370 3707 $taxrate = 0;
3371 3708 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3491,9 +3828,9 @@
3491 3828 $vippsmethod['shippingMethod'] = $rate->get_label();
3492 3829 $vippsmethod['shippingMethodId'] = $key;
3493 3830 $vippsmethods[]=$vippsmethod;
3494 3831
3495 - // Metadata and settings stored for later use for Vipps Checkout
3832 + // Metadata and settings stored for later use for Checkout
3496 3833 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3497 3834 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3498 3835 // finalize the order. IOK 2025-08-15
3499 3836 $ratemap[$key]=$rate;
@@ -3511,9 +3848,9 @@
3511 3848 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3512 3849 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3513 3850 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3514 3851
3515 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3852 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3516 3853 // IOK 2025-08-15 and new Express yet another slightly different format.
3517 3854 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3518 3855 if ($ischeckout) {
3519 3856 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -3889,17 +4226,8 @@
3889 4226 header("X-Accel-Expires: 0");
3890 4227 }
3891 4228
3892 4229
3893 -
3894 - // Handle DELETE on a vipps consent removal callback
3895 - public function vipps_consent_removal_callback ($callback) {
3896 - Vipps::nocache();
3897 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3898 - // to escape here when the API is changed. IOK 2020-10-14
3899 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3900 - }
3901 -
3902 4230 public function woocommerce_payment_gateways($methods) {
3903 4231 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
3904 4232 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3905 4233 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -3974,9 +4302,9 @@
3974 4302 if ($currentstatus != 'initiated') {
3975 4303 $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');
3976 4304 return;
3977 4305 }
3978 - $this->check_status_of_pending_order($o, false, false);
4306 + $this->check_status_of_pending_order($o, false);
3979 4307 }
3980 4308 }
3981 4309
3982 4310 // 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 -
@@ -3981,30 +4309,35 @@
3981 4309
3982 4310 // 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 -
3983 4311 // e.g. wp-cron. IOK 2021-06-21
3984 4312 // Stop restoring session in wp-cron too. IOK 2021-08-23
3985 - public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3986 - $express = $order->get_meta('_vipps_express_checkout');
3987 - $vippstatus = $order->get_meta('_vipps_status');
3988 - if ($express && $maybe_restore_session) {
3989 - $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3990 - $this->callback_restore_session($order->get_id());
3991 - }
4313 + // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4314 + public function check_status_of_pending_order($order, $allow_retry=true) {
3992 4315 $gw = $this->gateway();
3993 4316
3994 4317 $order_status = null;
3995 4318 try {
3996 4319 $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()));
3997 - $order_status = $gw->callback_check_order_status($order, $allow_retry);
4320 +
4321 + // Poll status and correct woo status. LP 2026-05-19
4322 + $order_data = $gw->get_payment_details($order);
4323 +
4324 + // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4325 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4326 + /* translators: company name */
4327 + $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4328 + return;
4329 + }
4330 +
4331 + $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4332 + $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4333 + $order_status = $order->get_status();
4334 +
3998 4335 $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');
3999 4336 } catch (Exception $e) {
4000 4337 $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');
4001 4338 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
4002 4339 }
4003 - // Ensure we don't keep using an old session for more than one order here.
4004 - if ($express && $maybe_restore_session) {
4005 - $this->callback_destroy_session();
4006 - }
4007 4340 return $order_status;
4008 4341 }
4009 4342
4010 4343 // 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
@@ -4017,20 +4350,40 @@
4017 4350 }
4018 4351 }
4019 4352
4020 4353 public function activate () {
4021 - static::maybe_add_cron_event();
4022 - $gw = $this->gateway();
4354 + static::maybe_add_cron_event();
4355 + $gw = $this->gateway();
4023 4356
4024 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4025 - if ($gw->get_option('orderprefix') == 'Woo') {
4026 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4027 - }
4028 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4029 - $gw->initialize_webhooks();
4030 - $this->payment_method_name = $gw->get_option('payment_method_name');
4031 - }
4357 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4358 + if ($gw->get_option('orderprefix') == 'Woo') {
4359 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4360 + }
4361 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4362 + $gw->initialize_webhooks();
4363 + $this->payment_method_name = $gw->get_option('payment_method_name');
4032 4364
4365 +
4366 + // Check if the special page is noted and actually does exist
4367 + $special = static::get_special_page_id();
4368 + if ($special) {
4369 + $special_page = get_post($special);
4370 + if ($special_page && 'trash' !== $special_page->post_status) {
4371 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4372 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4373 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4374 + wp_update_post([
4375 + 'ID' => $special,
4376 + 'post_content' => $new_content,
4377 + ]);
4378 + }
4379 + } else {
4380 + delete_option('woocommerce_vipps_special_page_page_id');
4381 + }
4382 + }
4383 +
4384 + }
4385 +
4033 4386 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4034 4387 public static function deactivate() {
4035 4388 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4036 4389 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4043,9 +4396,9 @@
4043 4396 // Delete all settings if checked in settings menu. LP 2025-10-06
4044 4397 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4045 4398 if ($should_delete) {
4046 4399 // Delete options.
4047 - $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4400 + $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'];
4048 4401 foreach($options as $option) {
4049 4402 delete_option($option);
4050 4403 }
4051 4404 }
@@ -4083,9 +4436,10 @@
4083 4436 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4084 4437 private function maybe_set_vipps_as_default() {
4085 4438 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4086 4439 $gw = $this->gateway();
4087 - if ($gw->get_option('vippsdefault')=='yes') {
4440 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4441 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4088 4442 WC()->session->set('chosen_payment_method', $gw->id);
4089 4443 }
4090 4444 }
4091 4445
@@ -4190,13 +4544,13 @@
4190 4544 if (is_user_logged_in()) return;
4191 4545 if (!$order || ! self::is_vipps_order($order)) return;
4192 4546
4193 4547 // We *do* want to log in express checkout customers, but not those that
4194 - // use the Vipps Checkout solution - those can change their emails in the
4548 + // use the Checkout solution - those can change their emails in the
4195 4549 // checkout screen. IOK 2021-09-03
4196 4550 $do_login = $order->get_meta('_vipps_express_checkout');
4197 4551
4198 - // We will not log in Vipps Checkout users unless the option for that is true
4552 + // We will not log in Checkout users unless the option for that is true
4199 4553 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4200 4554 $do_login = false;
4201 4555 }
4202 4556
@@ -4231,9 +4585,9 @@
4231 4585
4232 4586 // Both Checkout and Express Checkout have the below value set to true
4233 4587 if (!$order->get_meta('_vipps_express_checkout')) return;
4234 4588
4235 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4589 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4236 4590 // IOK 2023-07-27
4237 4591 $ischeckout = $order->get_meta('_vipps_checkout');
4238 4592 if ($ischeckout) {
4239 4593 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4618,9 +4972,9 @@
4618 4972 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4619 4973 }
4620 4974 }
4621 4975
4622 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4976 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4623 4977 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4624 4978 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4625 4979 // 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
4626 4980 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4691,46 +5045,37 @@
4691 5045 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4692 5046 return false;
4693 5047 }
4694 5048
4695 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4696 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4697 - private function make_vipps_url($what) {
4698 - if ( !get_option('permalink_structure')) {
4699 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4700 - }
4701 - return trailingslashit(home_url($what, 'https'));
5049 + // 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
5050 + private function make_special_page_url($action) {
5051 + return add_query_arg('action', $action, $this->get_special_page_url());
4702 5052 }
5053 +
4703 5054 public function payment_return_url() {
4704 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5055 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4705 5056 }
4706 5057 public function express_checkout_url() {
4707 - return $this->make_vipps_url('vipps-express-checkout');
5058 + return $this->make_special_page_url('do_express_checkout');
4708 5059 }
4709 5060 public function buy_product_url() {
4710 - return $this->make_vipps_url('vipps-buy-product');
5061 + return $this->make_special_page_url('buy_product');
4711 5062 }
4712 5063
4713 - // Return the method in the Vipps
4714 - public function is_special_page() {
4715 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4716 - $method = null;
4717 - if ( get_option('permalink_structure')) {
4718 - foreach($specials as $special=>$specialmethod) {
4719 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4720 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4721 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4722 - $method = $specialmethod; break;
4723 - }
4724 - }
4725 - } else {
4726 - if (isset($_GET['VippsSpecialPage'])) {
4727 - $method = @$specials[$_GET['VippsSpecialPage']];
4728 - }
4729 - }
4730 - return $method;
5064 + public static function is_special_page() {
5065 + $id = static::get_special_page_id();
5066 + return $id && is_page($id);
4731 5067 }
4732 5068
5069 + public static function get_special_page_id() {
5070 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5071 + return $id > 0 ? $id : null;
5072 + }
5073 +
5074 + public static function get_special_page_url() {
5075 + return get_permalink(static::get_special_page_id());
5076 + }
5077 +
4733 5078 // Just create a spinner and a overlay.
4734 5079 public function spinner () {
4735 5080 $flavour = sanitize_title($this->get_payment_method_name());
4736 5081 ob_start();
@@ -4751,164 +5096,22 @@
4751 5096 return apply_filters('woo_vipps_spinner', ob_get_clean());
4752 5097 }
4753 5098
4754 5099
4755 - // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4756 - // Fallbacks to defaults for each payment method. LP 2025-12-15
4757 - public function get_express_logo($payment_method, $lang, $variant) {
4758 - $base = plugins_url('img', __FILE__);
4759 -
4760 - // 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
4761 - $img_map = [
4762 - "vipps" => [
4763 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4764 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4765 - "en" => [
4766 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4767 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4768 - "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4769 - "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4770 - "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4771 - "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4772 - "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4773 - "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4774 -
4775 - ],
4776 - "no" => [
4777 - "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4778 - "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4779 - "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4780 - "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4781 - "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4782 - "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4783 - "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4784 - "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4785 - ],
4786 - "se" => [
4787 - "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4788 - "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4789 - "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4790 - "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4791 - "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4792 - "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4793 - "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4794 - "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4795 -
4796 - ],
4797 - ],
4798 - "mobilepay" => [
4799 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4800 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4801 - "en" => [
4802 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4803 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4804 - "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4805 - "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4806 - "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4807 - "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4808 - "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4809 - "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4810 -
4811 - ],
4812 - "dk" => [
4813 - "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4814 - "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4815 - "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4816 - "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4817 - "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4818 - "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4819 - "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4820 - "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4821 - ],
4822 - "fi" => [
4823 - "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4824 - "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4825 - "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4826 - "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4827 - "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4828 - "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4829 - "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4830 - "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4831 -
4832 - ],
4833 - ],
4834 -
4835 - ];
4836 -
4837 - $payment = strtolower($payment_method);
4838 - if ($lang === 'store') $lang = $this->get_customer_language();
4839 -
4840 - // Dont give a default if payment method not found. LP 2025-12-12
4841 - if (!array_key_exists($payment, $img_map)) {
4842 - return null;
4843 - }
4844 - $payment_map = $img_map[$payment];
4845 -
4846 - $img = null;
4847 - if (array_key_exists($lang, $payment_map)
4848 - && is_array($payment_map[$lang])
4849 - && array_key_exists($variant, $payment_map[$lang])) {
4850 - $img = @$payment_map[$lang][$variant];
4851 - }
4852 -
4853 - // Default fallback behaviour
4854 - if (!$img) {
4855 - $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4856 -
4857 - // First try getting default for payment method + language. LP 2026-01-16
4858 - if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4859 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4860 - $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');
4861 - $img = @$payment_map[$lang][$default];
4862 - }
4863 -
4864 - // If not found, then try global default for payment method. LP 2026-01-16
4865 - if (!$img) {
4866 - $img = @$payment_map[$default];
4867 - }
4868 -
4869 - // Found no logo at all, log this. LP 2026-01-16
4870 - if (!$img) {
4871 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4872 - $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');
4873 - }
4874 - }
4875 - return $img;
5100 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5101 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5102 + public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5103 + return $this->get_html_button_for_context($context);
4876 5104 }
4877 5105
5106 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
4878 5107 // Get payment logo based on payment method, then language NT 2023-11-30
4879 - // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4880 - public function get_payment_logo($page = null) {
4881 - $lang = $this->get_customer_language();
4882 - $payment_method = $this->get_payment_method_name();
4883 - $variant = $this->get_express_logo_page_variant($page);
4884 - $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4885 - return $logo_url;
5108 + // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5109 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5110 + public function get_payment_logo($context = 'global') {
5111 + return $this->get_express_logo(null, null, null, $context);
4886 5112 }
4887 5113
4888 - /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4889 - private function get_express_logo_page_variant($page = null) {
4890 - $options = get_option('vipps_button_options');
4891 -
4892 - // Init defaults, use mini version by default in below pages. LP 2025-12-17
4893 - $use_mini = in_array($page, ['catalog']);
4894 - $variant = "";
4895 -
4896 - // Find correct variant from button settings. LP 2025-12-17
4897 - if (is_array($options) && array_key_exists('express', $options)) {
4898 - if (array_key_exists($page, $options['express']['force-mini'])) {
4899 - $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4900 - }
4901 - $key = $use_mini ? 'mini-variant' : 'variant';
4902 - $variant = sanitize_title($options['express'][$key]) ?? '';
4903 - }
4904 -
4905 - if (!$variant) {
4906 - $variant = $use_mini ? "default-mini" : "default";
4907 - }
4908 - return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4909 - }
4910 -
4911 5114 // Get express banner logo based on payment method. LP 2025-09-03
4912 5115 private function get_express_banner_logo() {
4913 5116 $payment_method = $this->get_payment_method_name();
4914 5117
@@ -4919,10 +5122,12 @@
4919 5122 }
4920 5123 return null;
4921 5124 }
4922 5125
4923 - // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4924 - public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
5126 + // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5127 + // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5128 + // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5129 + public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
4925 5130 $disabled = $disabled ? 'disabled' : '';
4926 5131 $data = array();
4927 5132
4928 5133 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
@@ -4937,9 +5142,8 @@
4937 5142 if ($sku) $data['product_sku'] = $sku;
4938 5143 if ($product_id) $data['product_id'] = $product_id;
4939 5144 if ($variation_id) $data['variation_id'] = $variation_id;
4940 5145
4941 -
4942 5146 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4943 5147 foreach($data as $key=>$value) {
4944 5148 $value = esc_attr($value);
4945 5149 $buttoncode .= " data-$key='$value' ";
@@ -4946,14 +5150,18 @@
4946 5150 }
4947 5151
4948 5152 $payment_method = $this->get_payment_method_name();
4949 5153 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4950 - $short = str_ends_with($logo_variant, 'mini');
4951 - $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4952 5154
4953 - $message =" <img border=0 src='$logo' alt='$payment_method'/>";
5155 + if (is_array($button_args_override) && $button_args_override) {
5156 + $button_args = $button_args_override;
5157 + } else {
5158 + $button_args = $this->get_html_button_attrs_for_context($context);
5159 + }
5160 + $short = ($button_args['compact'] ?? 'false') === 'true';
5161 + $button = $this->get_html_button($button_args);
4954 5162
4955 -# Extra classes, if passed IOK 2019-02-26
5163 + # Extra classes, if passed IOK 2019-02-26
4956 5164 if (is_array($classes)) {
4957 5165 $classes = join(" ", $classes);
4958 5166 }
4959 5167 if ($classes) $classes = " $classes";
@@ -4958,19 +5166,15 @@
4958 5166 }
4959 5167 if ($classes) $classes = " $classes";
4960 5168 if ($short) $classes = "short $classes";
4961 5169
4962 - $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
5170 + $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5171 +
5172 +
5173 +
4963 5174 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4964 5175 }
4965 5176
4966 - // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4967 - public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4968 - $logo_lang = $this->get_customer_language();
4969 - $logo_variant = $this->get_express_logo_page_variant($page);
4970 - return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4971 - }
4972 -
4973 5177 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4974 5178 public function single_product_buy_now_button () {
4975 5179 $gw = $this->gateway();
4976 5180 $how = $gw->get_option('singleproductexpress');
@@ -5051,43 +5255,90 @@
5051 5255 }
5052 5256
5053 5257
5054 5258
5055 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5259 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5056 5260 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5261 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5057 5262 public function woocommerce_create_pages ($data) {
5263 + // Vipps Checkout page
5058 5264 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5059 - if (!$vipps_checkout_activated) return $data;
5265 + if ($vipps_checkout_activated) {
5266 + $data['vipps_checkout'] = array(
5267 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5268 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5269 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5270 + );
5271 + }
5060 5272
5061 - $data['vipps_checkout'] = array(
5062 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5063 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5064 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5065 - );
5066 -
5273 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5274 + $data['vipps_special_page'] = [
5275 + 'name' => 'vipps-payment', // slug
5276 + /* translators: company name */
5277 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5278 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5279 + ];
5067 5280 return $data;
5068 5281 }
5069 5282
5070 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5283 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5284 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5071 5285 public function maybe_create_vipps_pages () {
5286 + $make_pages = false;
5287 +
5288 + // Vipps Checkout page. LP 2026-08-18
5072 5289 $checkoutid = wc_get_page_id('vipps_checkout');
5073 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5074 - if ($makeit) {
5290 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5075 5291 delete_option('woocommerce_vipps_checkout_page_id');
5292 + $make_pages = true;
5076 5293 }
5077 5294
5078 - if ($makeit) {
5079 - WC_Install::create_pages();
5295 + // vipps special page, previously a fake page. LP 2026-08-18
5296 + $builtin_special_page_id = static::get_special_page_id();
5297 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5298 + delete_option('woocommerce_vipps_special_page_page_id');
5299 + $make_pages = true;
5080 5300 }
5301 +
5302 + if ($make_pages) {
5303 + WC_Install::create_pages();
5304 + }
5081 5305 }
5082 5306
5307 + public function vipps_special_page_shortcode($atts, $content) {
5308 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5309 + if (is_admin()) return;
5310 + if (wp_doing_ajax()) return;
5311 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5312 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5083 5313
5314 + $action = $_GET['action'] ?? '';
5315 + $html = "";
5316 + switch ($action) {
5317 + case 'wait_for_payment':
5318 + $html = $this->vipps_wait_for_payment();
5319 + break;
5320 + case 'do_express_checkout':
5321 + $html = $this->vipps_express_checkout();
5322 + break;
5323 + case 'buy_product':
5324 + $html = $this->vipps_buy_product();
5325 + break;
5326 + default:
5327 + $html = '';
5328 + }
5329 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5330 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5331 +
5332 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5333 + return $html;
5334 + }
5335 +
5336 +
5084 5337 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5085 5338 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5086 5339 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5087 5340 public function vipps_buy_product() {
5088 - status_header(200,'OK');
5089 - Vipps::nocache();
5090 5341
5091 5342 add_filter('body_class', function ($classes) {
5092 5343 $classes[] = 'vipps-express-checkout';
5093 5344 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
@@ -5123,9 +5374,9 @@
5123 5374
5124 5375 if (!$productinfo) {
5125 5376 $title = __("Product is no longer available",'woo-vipps');
5126 5377 $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');
5127 - return $this->fakepage($title,$content);
5378 + return $this->special_page_html($title,$content);
5128 5379 }
5129 5380
5130 5381 // Pass the productinfo to the express checkout form
5131 5382 $args = array();
@@ -5142,15 +5393,13 @@
5142 5393 }
5143 5394 $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5144 5395 }
5145 5396
5146 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5397 + return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5147 5398 }
5148 5399
5149 5400 // 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.
5150 5401 public function vipps_express_checkout() {
5151 - status_header(200,'OK');
5152 - Vipps::nocache();
5153 5402 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5154 5403 // IOK 2018-05-28
5155 5404 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5156 5405
@@ -5177,9 +5426,9 @@
5177 5426 });
5178 5427
5179 5428 do_action('woo_vipps_express_checkout_page');
5180 5429
5181 - $this->print_express_checkout_page(true, 'do_express_checkout');
5430 + return $this->express_checkout_page_html(true, 'do_express_checkout');
5182 5431 }
5183 5432
5184 5433 // This method tries to ensure that a customer does not 'lose' the return page and
5185 5434 // starts ordering the same products twice. IOK 2020-01-22
@@ -5274,9 +5523,10 @@
5274 5523 return $orderspec;
5275 5524 }
5276 5525
5277 5526 // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5278 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5527 + // Returns the html. LP 2026-08-27
5528 + protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5279 5529 $gw = $this->gateway();
5280 5530
5281 5531 $expressCheckoutMessages = array();
5282 5532 $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
@@ -5287,11 +5537,12 @@
5287 5537 wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5288 5538 wp_enqueue_script('vipps-express-checkout');
5289 5539 // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5290 5540 // to actually perform the express checkout.
5291 - $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5541 + $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5292 5542
5293 5543
5544 +
5294 5545 $orderspec = $this->get_orderspec_from_arguments($productinfo);
5295 5546 if (empty($orderspec)) {
5296 5547 $orderspec = $this->get_orderspec_from_cart();
5297 5548 }
@@ -5358,10 +5609,9 @@
5358 5609
5359 5610 if ($execute) {
5360 5611 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5361 5612 $content .= "<div id='vipps-status-message'></div>";
5362 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5363 - return;
5613 + return $this->special_page_html('', $content);
5364 5614 } else {
5365 5615 $content .= $askForConfirmationHTML;
5366 5616 $content .= $extraHTML;
5367 5617 $content .= $termsHTML;
@@ -5366,23 +5616,18 @@
5366 5616 $content .= $extraHTML;
5367 5617 $content .= $termsHTML;
5368 5618 $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5369 5619 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5370 - $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>";
5620 + $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5371 5621 $content .= "<div id='vipps-status-message'></div>";
5372 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5373 - return;
5622 + return $this->special_page_html('', $content);
5374 5623 }
5375 5624 }
5376 5625
5377 5626
5378 -
5379 - public function vipps_wait_for_payment() {
5380 - status_header(200,'OK');
5381 - Vipps::nocache();
5382 -
5627 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5628 + private function handle_payment_poll_and_redirect () {
5383 5629 $orderid = WC()->session->get('_vipps_pending_order');
5384 -
5385 5630 $order = null;
5386 5631 $gw = $this->gateway();
5387 5632
5388 5633 // Failsafe for when the session disappears IOK 2018-11-19
@@ -5394,9 +5639,9 @@
5394 5639 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5395 5640 // simulating the session with that.
5396 5641 // IOK 2019-11-19, changed to using GET 2023-01-23
5397 5642 if ($no_session && $limited_session) {
5398 - $orderid = intval(@$_GET['id']);
5643 + $orderid = intval($_GET['id'] ?? false);
5399 5644 }
5400 5645 if ($orderid) {
5401 5646 clean_post_cache($orderid);
5402 5647 $order = wc_get_order($orderid);
@@ -5415,11 +5660,8 @@
5415 5660 $session->set('_vipps_pending_order', $orderid);
5416 5661 }
5417 5662 }
5418 5663
5419 -
5420 - do_action('woo_vipps_wait_for_payment_page',$order);
5421 -
5422 5664 $deleted_order=0;
5423 5665 if ($orderid && !$order) {
5424 5666 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5425 5667 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
@@ -5444,9 +5686,9 @@
5444 5686 clean_post_cache($orderid);
5445 5687 $order = wc_get_order($orderid); // Reload order object
5446 5688 }
5447 5689 } else {
5448 - // No need to do anyting here. IOK 2020-01-26
5690 + // No need to do anyting here. IOK 2020-01-26
5449 5691 }
5450 5692
5451 5693 $payment = 'notchecked';
5452 5694 if ($do_poll) {
@@ -5462,9 +5704,8 @@
5462 5704 exit();
5463 5705 }
5464 5706
5465 5707 // We are done, but in failure. Don't poll.
5466 - $content = "";
5467 5708 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5468 5709
5469 5710 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5470 5711 // 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
@@ -5472,8 +5713,9 @@
5472 5713 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5473 5714 wp_redirect($failure_redirect);
5474 5715 exit();
5475 5716 }
5717 +
5476 5718 if ($status == 'cancelled' || $payment == 'cancelled') {
5477 5719 $this->maybe_restore_cart($orderid,'failed');
5478 5720 if ($failure_redirect){
5479 5721 wp_redirect($failure_redirect);
@@ -5478,27 +5720,45 @@
5478 5720 if ($failure_redirect){
5479 5721 wp_redirect($failure_redirect);
5480 5722 exit();
5481 5723 }
5724 + } else {
5725 + // If not, enqueue the status checker IOK 2026-09-21
5726 + 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');
5727 + }
5728 +
5729 + // Communicate this to the shortcode IOK 2026-09-21
5730 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5731 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5732 + });
5733 +
5734 + }
5735 +
5736 + public function vipps_wait_for_payment() {
5737 +
5738 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5739 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5740 +
5741 + $orderid = $data['orderid'] ?? 0;
5742 + $status = $data['status'] ?? "";
5743 + $payment = $data['payment'] ?? "";
5744 +
5745 + $order = wc_get_order($orderid);
5746 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5747 +
5748 + do_action('woo_vipps_wait_for_payment_page',$order);
5749 + $gw = $this->gateway();
5750 +
5751 + $content = "";
5752 + if ($status == 'cancelled' || $payment == 'cancelled') {
5482 5753 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5483 5754 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5484 5755 $content .= "</div>";
5485 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5486 -
5487 - return;
5756 + return $this->special_page_html('', $content);
5488 5757 }
5489 5758
5490 5759 // 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
5491 -
5492 5760 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5493 - 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');
5494 -
5495 - // 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
5496 - // and check that the order matches (and is 'pending') (and exists)
5497 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5498 - $vippsstatus = $order->get_meta('_vipps_status');
5499 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5500 -
5501 5761 $signal = $this->callbackSignal($order);
5502 5762 $content = "";
5503 5763 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5504 5764
@@ -5506,15 +5766,16 @@
5506 5766 $signalurl = $this->callbackSignalURL($signal);
5507 5767
5508 5768 $content .= "</p></div>";
5509 5769
5510 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5511 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5512 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5770 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5771 +
5772 + // Carry the order status to the checking script IOK 2026-09-21
5773 + $content .= "<form id='vippsdata'>";
5513 5774 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5514 5775 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5515 5776 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5516 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5777 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5517 5778 $content .= "</form>";
5518 5779
5519 5780
5520 5781 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
@@ -5531,94 +5792,22 @@
5531 5792 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5532 5793 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5533 5794 $content .= "</div>";
5534 5795
5796 + return $this->special_page_html('', $content);
5797 + }
5535 5798
5536 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5799 + // Returns formatted html for the vipps special page. LP 2026-08-27
5800 + public function special_page_html($header, $content) {
5801 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5802 + $html = <<<EOF
5803 + $header_html
5804 + <div class="vipps-special-page-content">$content</div>
5805 + EOF;
5806 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5537 5807 }
5538 5808
5539 5809
5540 -
5541 - public function fakepage($title,$content) {
5542 - global $wp, $wp_query;
5543 - // We don't want this here.
5544 - remove_filter ('the_content', 'wpautop');
5545 -
5546 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5547 - $wp_post = null;
5548 - if ($specialid) {
5549 - $wp_post = get_post($specialid);
5550 - if ($wp_post) {
5551 - $wp_post->post_title = $title;
5552 - $wp_post->post_content = $content;
5553 - // Normalize a bit
5554 - $wp_post->filter = 'raw'; // important
5555 - $wp_post->post_status = 'publish';
5556 - $wp_post->comment_status= 'closed';
5557 - $wp_post->ping_status= 'closed';
5558 - } else {
5559 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5560 - }
5561 - }
5562 - if (!$wp_post || is_wp_error($wp_post)) {
5563 - $post = new stdClass();
5564 - $post->ID = -99;
5565 - $post->post_author = 1;
5566 - $post->post_date = current_time( 'mysql' );
5567 - $post->post_date_gmt = current_time( 'mysql', 1 );
5568 - $post->post_title = $title;
5569 - $post->post_content = $content;
5570 - $post->post_status = 'publish';
5571 - $post->comment_status = 'closed';
5572 - $post->ping_status = 'closed';
5573 - $post->post_name = 'vippsconfirm-fake-page-name';
5574 - $post->post_type = 'page';
5575 - $post->filter = 'raw'; // important
5576 - $wp_post = new WP_Post($post);
5577 - wp_cache_add( -99, $wp_post, 'posts' );
5578 - }
5579 -
5580 - // Update the main query
5581 - $wp_query->post = $wp_post;
5582 - $wp_query->posts = array( $wp_post );
5583 - $wp_query->queried_object = $wp_post;
5584 - $wp_query->queried_object_id = $wp_post->ID;
5585 - $wp_query->found_posts = 1;
5586 - $wp_query->post_count = 1;
5587 - $wp_query->max_num_pages = 1;
5588 - $wp_query->is_page = true;
5589 - $wp_query->is_singular = true;
5590 - $wp_query->is_single = false;
5591 - $wp_query->is_attachment = false;
5592 - $wp_query->is_archive = false;
5593 - $wp_query->is_category = false;
5594 - $wp_query->is_tag = false;
5595 - $wp_query->is_tax = false;
5596 - $wp_query->is_author = false;
5597 - $wp_query->is_date = false;
5598 - $wp_query->is_year = false;
5599 - $wp_query->is_month = false;
5600 - $wp_query->is_day = false;
5601 - $wp_query->is_time = false;
5602 - $wp_query->is_search = false;
5603 - $wp_query->is_feed = false;
5604 - $wp_query->is_comment_feed = false;
5605 - $wp_query->is_trackback = false;
5606 - $wp_query->is_home = false;
5607 - $wp_query->is_embed = false;
5608 - $wp_query->is_404 = false;
5609 - $wp_query->is_paged = false;
5610 - $wp_query->is_admin = false;
5611 - $wp_query->is_preview = false;
5612 - $wp_query->is_robots = false;
5613 - $wp_query->is_posts_page = false;
5614 - $wp_query->is_post_type_archive = false;
5615 - // Update globals
5616 - $GLOBALS['wp_query'] = $wp_query;
5617 - $wp->register_globals();
5618 - return $wp_post;
5619 - }
5620 -
5621 5810 // Support the interactivity API with data about our cart IOK 2026-02-23
5622 5811 public function woo_vipps_store_api_cart_data() {
5623 5812 // 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
5624 5813
@@ -5624,13 +5813,15 @@
5624 5813
5625 5814 $checkout_page = $this->gateway()->vipps_checkout_available();
5626 5815 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5627 5816 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5817 +
5628 5818 $cart_data = array(
5629 5819 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5630 5820 'cart_supports_checkout' => (bool) $checkout_page,
5631 5821 'checkout_url' => $checkout_url,
5632 5822 );
5823 +
5633 5824 return $cart_data;
5634 5825 }
5635 5826
5636 5827 public function woo_vipps_store_api_cart_schema() {
@@ -5652,8 +5843,117 @@
5652 5843 ),
5653 5844 );
5654 5845 }
5655 5846
5847 + // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5848 + // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5849 + private function init_button_options() {
5850 + /* New structure as of now
5851 + * [
5852 + * 'version' => x.x,
5853 + * 'express' => [
5854 + * 'version' => x.x, used to migrate from previous iterations
5855 + * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5856 + * 'global' => ['compact' => ..., 'verb' => ..., ...],
5857 + * 'cart' => [...],
5858 + * 'product' => [...],
5859 + * 'checkout' => [...],
5860 + * ...
5861 + * ],
5862 + * 'product_configs' => [ overrides for specific products
5863 + * 1532 => ['compact' => ..., 'verb' => ..., ...],
5864 + * ...
5865 + * ],
5866 + * ],
5867 + * ]
5868 + */
5869 + $options = get_option('vipps_button_options2');
5870 + if (!empty($options)) return;
5871 +
5872 + $old_options = get_option('vipps_button_options');
5873 + $default_config = $this->get_html_button_default_attrs();
5874 + unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5875 + $default_compact = array_replace($default_config, ['compact' => 'true']);
5876 +
5877 + $default_options = [
5878 + 'version' => $this->button_options_version,
5879 + 'express' => [
5880 + 'version' => $this->button_options_express_version,
5881 + 'configs' => [
5882 + 'global' => $default_config,
5883 + // Need compact version by default for below pages. LP 2026-07-07
5884 + 'catalog' => $default_compact,
5885 + 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5886 + ],
5887 + 'product_configs' => [],
5888 + ],
5889 + ];
5890 +
5891 + $new_options = $default_options;
5892 +
5893 + //Actually, we have some options from the old structure IOK 2026-07-15
5894 + if (!empty($old_options)) {
5895 + $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5896 + unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5897 + }
5898 +
5899 + // Migrate context/page mini override to new context config. LP 2026-06-26
5900 + if (is_array($old_options['express']['force-mini'] ?? null)) {
5901 + foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5902 + if ("yes" === $use_mini) {
5903 + $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5904 + unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5905 + $config['compact'] = 'true';
5906 + $new_options['express']['configs'][$context] = $config;
5907 + }
5908 + }
5909 + }
5910 +
5911 + if ($this->get_payment_method_name() !== 'MobilePay') {
5912 + // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5913 + foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5914 + if ('fi' === ($config['language'] ?? '')) {
5915 + $config['language'] = 'store';
5916 + $new_options['express']['configs'][$context] = $config;
5917 + }
5918 + }
5919 + }
5920 +
5921 + /* translators: placeholders are arrays */
5922 + $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5923 +
5924 +
5925 +
5926 + update_option('vipps_button_options2', $new_options);
5927 + }
5928 +
5929 + // Old variant string => new config array. LP 2026-06-26
5930 + public function migrate_button_variant_to_config($variant_slug) {
5931 + if (!is_string($variant_slug)) return [];
5932 + $config = $this->get_html_button_default_attrs();
5933 + $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5934 + $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5935 + if (str_contains($variant_slug, 'buy-now')) {
5936 + $config['verb'] = 'buy';
5937 + } else if (str_contains($variant_slug, 'express')) {
5938 + $config['verb'] = 'express';
5939 + }
5940 + return $config;
5941 + }
5942 +
5943 + // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5944 + public function get_express_logo_variants() {
5945 + return [
5946 + 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5947 + 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5948 + 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5949 + 'express-pill' => __('Express pill', 'woo-vipps'),
5950 + 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5951 + 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5952 + ];
5953 + }
5954 +
5955 +
5656 5956 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5657 5957 public static function order_is_vipps_retryable($order_id) {
5658 5958 $order = wc_get_order($order_id);
5659 5959 if (!$order) return false;
@@ -5662,6 +5962,20 @@
5662 5962 $shipping_set = $order->get_meta('_vipps_shipping_set');
5663 5963
5664 5964 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5665 5965 return $nonexpress_epayment || $shipping_set;
5966 + }
5967 +
5968 + /** Returns the plugin's rest api namespace including the version.
5969 + * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
5970 + public static function get_rest_namespace($version = 'latest') {
5971 + $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
5972 + return self::REST_NAMESPACE_BASE . "/$version";
5973 + }
5974 +
5975 + /** Returns the plugin's rest api url.
5976 + * $version accepts 'latest', but you probably don't want to do that.
5977 + * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
5978 + public static function get_rest_url($version, $route) {
5979 + return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5666 5980 }
5667 5981 }