PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.1
Pay with Vipps and MobilePay for WooCommerce v6.3.1
6.3.1 6.2.6 6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 All 190 releases
← All changes | payment/Vipps.class.php +1416 -1132 6.0.2 → 6.3.1 View file →
@@ -36,8 +36,12 @@
36 36 }
37 37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38 38
39 39 class Vipps {
40 + /* Rest api consts. LP 2026-03-30 */
41 + private const REST_NAMESPACE_BASE = 'woo-vipps';
42 + private const REST_CURRENT_VERSION = 'v1'; // don't use this directly, use methods get_rest_namespace() and get_rest_url(). LP 2026-04-22
43 +
40 44 private static $instance = null;
41 45
42 46 /* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */
43 47 public static $installed_gateways = [];
@@ -50,12 +54,12 @@
50 54
51 55 // True if HPOS is being used
52 56 public $HPOSActive = null;
53 57
54 - // used in the fake locking mechanism using transients
55 - private $lockKey = null;
58 + public $vippsJSConfig = array();
56 59
57 - public $vippsJSConfig = array();
60 + public $button_options_version = '2.0';
61 + public $button_options_express_version = '2.0';
58 62
59 63 // IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction
60 64 public static function CompanyName() {
61 65 return __("Vipps MobilePay", 'woo-vipps');
@@ -63,9 +67,9 @@
63 67 public static function CheckoutName($order=null) {
64 68 return "Vipps MobilePay Checkout"; // Do not translate
65 69 }
66 70 public static function ExpressCheckoutName($order=null) {
67 - return __("Vipps Express Checkout", 'woo-vipps');
71 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
68 72 }
69 73 public static function LoginName() {
70 74 return __("Login with Vipps", 'woo-vipps');
71 75 }
@@ -110,16 +114,22 @@
110 114 add_action('wp_footer', array($Vipps,'footer'));
111 115 }
112 116 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
113 117 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
114 - add_action('init',array($Vipps,'init'));
118 + add_action( 'init',array($Vipps,'init'));
119 + add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
115 120 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
116 121 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
117 122 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
123 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
119 124 // 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 125 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
121 126 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
127 +
128 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
129 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
130 + // is important.
131 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
122 132 }
123 133
124 134 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
125 135 public function woocommerce_blocks_loaded() {
@@ -215,8 +225,9 @@
215 225 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
216 226 // needs these to be defined in the backend. IOK 2024-04-16
217 227 add_action('wp_loaded', array($this, 'wp_register_scripts'));
218 228 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
229 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
219 230
220 231 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
221 232 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
222 233
@@ -232,16 +243,8 @@
232 243
233 244 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
234 245 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
235 246
236 - // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
237 - add_action('rest_api_init', function() {
238 - register_rest_route('woo-vipps/v1', '/express-products', [
239 - 'methods' => 'GET',
240 - 'callback' => [$this, 'rest_express_checkout_products'],
241 - 'permission_callback' => '__return_true',
242 - ]);
243 - });
244 247
245 248 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
246 249 // action scheduler would be better, but we can't do that just yet because of backwards
247 250 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -264,17 +267,65 @@
264 267 // because it is self-updating or because it has been deactivated just now or something, we won't have access to it.
265 268 // Therefore test it first. IOK 2022-12-08
266 269 $gw = $this->gateway();
267 270
268 - // This is a developer-mode level feature because flock() is not portable. This ensures callbacks and shopreturns do not
269 - // simultaneously update the orders, in particular not the express checkout order lines wrt shipping. IOK 2020-05-19
270 - if ($gw && $gw->get_option('use_flock') == 'yes') {
271 - add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
272 - add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
273 - }
271 + // Set default button options, migrating any older setup IOK 2026-07-15
272 + $this->init_button_options();
274 273
274 + /*
275 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
276 + where we earlier used just a fake page with no real page id, unless especially configured.
277 + We therefore need to add code to maintain this special page.
278 +
279 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
280 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
281 + */
282 +
283 + // Delete special page id option when its deleted or trashed, so that we dont have to load
284 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
285 + $delete_special_page_id = function($post_id, $post = null) {
286 + if (static::get_special_page_id() === $post_id) {
287 + delete_option('woocommerce_vipps_special_page_page_id');
288 + }
289 + };
290 + add_action('delete_post', $delete_special_page_id, 10, 2);
291 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
292 +
293 + $this->ensure_special_page_exists();
294 +
295 +
296 + // We want this special page to have a certain title and maybe special scripts and so on,
297 + // this gets run in template redirect for these pages.
298 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
299 +
300 + // Add an admin interface for this page as well IOK 2026-09-11
301 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
275 302 }
276 303
304 +
305 + public function rest_api_init () {
306 +
307 + // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
308 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
309 + 'methods' => 'GET',
310 + 'callback' => [$this, 'rest_express_checkout_products'],
311 + 'permission_callback' => '__return_true',
312 + ]);
313 +
314 + // Start a single product express checkout process. IOK 2026-08-25
315 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
316 + 'methods' => 'POST',
317 + 'callback' => [$this, 'rest_do_single_product_express_checkout'],
318 + 'permission_callback' => '__return_true',
319 + ]);
320 + // And one for the cart. IOK 2026-09-04
321 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
322 + 'methods' => 'POST',
323 + 'callback' => [$this, 'rest_do_express_checkout'],
324 + 'permission_callback' => '__return_true',
325 + ]);
326 + }
327 +
277 328 public function admin_init () {
278 329 $gw = $this->gateway();
279 330 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
280 331 $adminSettings = VippsAdminSettings::instance();
@@ -301,10 +352,8 @@
301 352 // Styling etc
302 353 add_action('admin_head', array($this, 'admin_head'));
303 354
304 355 // Scripts
305 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
306 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
307 356 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
308 357
309 358 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
310 359 add_action('current_screen', function ($screen) {
@@ -392,9 +441,77 @@
392 441 }
393 442 }
394 443 }
395 444
445 +
446 + /** Ensure we have a special page for payment flows
447 + *
448 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
449 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
450 + **/
451 + public function ensure_special_page_exists() {
452 + if (static::get_special_page_id()) return;
453 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
396 454
455 + // 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
456 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
457 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
458 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
459 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
460 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
461 +
462 + // Ensure this page has the necessary shortcode. LP 2026-09-01
463 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
464 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
465 + wp_update_post([
466 + 'ID' => $old_special_page_id,
467 + 'post_content' => $new_content,
468 + ]);
469 + }
470 + } else {
471 + // Create special page if its missing. LP 2026-09-01
472 + $this->maybe_create_vipps_pages();
473 + }
474 + }
475 +
476 + // Admin interface for the special page on woo/advanced/pages
477 + public function woocommerce_settings_pages ($settings) {
478 + $i = -1;
479 + foreach($settings as $entry) {
480 + $i++;
481 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
482 + break;
483 + }
484 + }
485 + if ($i > 0) {
486 + $vippspagesettings = array(
487 + array(
488 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
489 + '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') ,
490 + 'id' => 'woocommerce_vipps_special_page_page_id',
491 + 'type' => 'single_select_page_with_search',
492 + 'default' => '',
493 + 'class' => 'wc-page-search',
494 + 'css' => 'min-width:300px;',
495 + 'args' => array(
496 + 'exclude' =>
497 + array(
498 + wc_get_page_id( 'myaccount' ),
499 + wc_get_page_id( 'checkout' ),
500 + wc_get_page_id( 'cart' ),
501 + ),
502 + ),
503 + 'desc_tip' => true,
504 + 'autoload' => false,
505 + ));
506 + array_splice($settings, $i, 0, $vippspagesettings);
507 + }
508 +
509 + return $settings;
510 + }
511 +
512 +
513 +
397 514 // Runs on init, adds the Vipps badge feature if activated
398 515 public function maybe_add_vipps_badge_feature () {
399 516 $badge_options = get_option('vipps_badge_options');
400 517 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -723,8 +840,11 @@
723 840
724 841 // Get current brand and language
725 842 $current_brand = strtolower($this->get_payment_method_name());
726 843 $current_language = $this->get_customer_language();
844 + if ('se' === $current_language) $current_language = 'sv';
845 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
846 + if ('dk' === $current_language) $current_language = 'da';
727 847
728 848 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
729 849 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
730 850 'purple'=> __('Purple', 'woo-vipps')];
@@ -787,9 +907,9 @@
787 907
788 908 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
789 909 <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 910 <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>
911 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
792 912 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
793 913 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
794 914 </p>
795 915
@@ -816,21 +936,24 @@
816 936 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
817 937 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
818 938 }
819 939
820 - $options = get_option('vipps_button_options');
821 - if (isset($_POST['express']['variant'])) {
822 - $options['express']['variant'] = sanitize_title($_POST['express']['variant']);
940 + $old = get_option('vipps_button_options2', []);
941 + $new = $old;
942 + if (isset($_POST['express']['configs'])) {
943 + foreach ($_POST['express']['configs'] as $ctx => $config) {
944 + $sanitized_ctx = sanitize_title($ctx);
945 + $sanitized_config = map_deep($config, 'sanitize_title');
946 +
947 + // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
948 + if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
949 + $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
950 + } else {
951 + $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
952 + }
953 + }
823 954 }
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);
955 + update_option('vipps_button_options2', $new);
833 956 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
834 957 exit();
835 958 }
836 959
@@ -864,9 +987,12 @@
864 987 public function vipps_mobilepay_badge_shortcode($atts) {
865 988 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
866 989
867 990 $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();
991 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
992 + if ('se' === $language) $language = 'sv';
993 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
994 + if ('dk' === $language) $language = 'da';
869 995 $id = sanitize_title($args['id']);
870 996 $class = sanitize_text_field($args['class']);
871 997
872 998 $attributes = [];
@@ -882,13 +1008,18 @@
882 1008 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
883 1009 }
884 1010
885 1011 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1012 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
886 1013 public function vipps_badge_shortcode($atts) {
887 1014 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
888 1015
889 1016 $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();
1017 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1018 + if ('se' === $language) $language = 'sv';
1019 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1020 + if ('dk' === $language) $language = 'da';
1021 +
891 1022 $id = sanitize_title($args['id']);
892 1023 $class = sanitize_text_field($args['class']);
893 1024
894 1025 $attributes = [];
@@ -902,163 +1033,341 @@
902 1033
903 1034 return "<vipps-badge $badgeatts></vipps-badge>";
904 1035 }
905 1036
906 - public function get_express_logo_variants() {
1037 + public function get_html_button_default_attrs() {
907 1038 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'),
1039 + 'language' => 'store',
1040 + 'variant' => 'primary',
1041 + 'rounded' => 'false',
1042 + 'verb' => 'buy',
1043 + 'stretched' => 'false',
1044 + 'compact' => 'false',
1045 + '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 1046 ];
915 1047 }
916 1048
1049 + public function get_html_button_attrs_for_context($context = 'global') {
1050 + $options = get_option('vipps_button_options2', []);
1051 + if (!is_string($context)) $context = 'global';
1052 +
1053 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1054 + $gutenberg = false;
1055 + if ($context == 'checkout_gutenberg') {
1056 + $context = 'checkout';
1057 + $gutenberg = true;
1058 + }
1059 + if ($context == 'cart_gutenberg') {
1060 + $context = 'cart';
1061 + $gutenberg = true;
1062 + }
917 1063
918 - public function button_menu_page() {
919 - if (!current_user_can('manage_woocommerce')) {
920 - wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1064 + $config = $options['express']['configs'][$context] ?? [];
1065 + $use_global = !$config || ($config['use-global-config'] ?? false);
1066 + if ($use_global) {
1067 + $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
921 1068 }
1069 +
1070 + // see above.
1071 + if ($gutenberg) {
1072 + $config['stretched']='true';
1073 + }
1074 + return $config;
1075 + }
1076 +
1077 + public function get_html_button_for_context($context = 'global') {
1078 + return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1079 + }
1080 +
1081 + // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1082 + // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1083 + public function get_html_button($attrs = []) {
922 1084 $payment_method = $this->get_payment_method_name();
923 - $lang = $this->get_customer_language();
924 - $button_options = get_option('vipps_button_options');
1085 + $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1086 + $attrs['brand'] = strtolower($payment_method);
1087 + $attrs['type'] = 'button'; // static
925 1088
926 - $variants = $this->get_express_logo_variants();
927 - $mini_variants = array_filter($variants, fn($key) => str_ends_with($key, 'mini'), ARRAY_FILTER_USE_KEY);
1089 + // Support using store language
1090 + if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1091 + // Don't support these login verbs. LP 2026-06-04
1092 + if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1093 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1094 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1095 + // Fix swedish too. LP 2026-10-06
1096 + if ('se' === $attrs['language']) $attrs['language'] = 'sv';
928 1097
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 - ],
940 - ];
1098 + $escaped_attrs = [];
1099 + foreach($attrs as $k => $v) {
1100 + $escaped_attrs[$k] = esc_attr($v);
1101 + }
941 1102
1103 + // id attribute
1104 + $id = $escaped_attrs['id'] ?? '';
1105 + $id_str = $id ? "id='$id'" : '';
1106 +
1107 + // class attribute
1108 + $class_str = '';
1109 + if (isset($attrs['class'])) {
1110 + if (is_array($attrs['class'])) {
1111 + $class_str = implode(' ', $attrs['class']);
1112 + } else if (is_string($attrs['class'])) {
1113 + $class_str = $attrs['class'];
1114 + }
1115 + }
1116 +
1117 + // The html
1118 + $html = <<<EOF
1119 +<vipps-mobilepay-button
1120 + $id_str
1121 + $class_str
1122 + type="{$escaped_attrs['type']}"
1123 + brand="{$escaped_attrs['brand']}"
1124 + language="{$escaped_attrs['language']}"
1125 + variant="{$escaped_attrs['variant']}"
1126 + rounded="{$escaped_attrs['rounded']}"
1127 + verb="{$escaped_attrs['verb']}"
1128 + stretched="{$escaped_attrs['stretched']}"
1129 + compact="{$escaped_attrs['compact']}"
1130 +></vipps-mobilepay-button>
1131 +EOF;
1132 + return apply_filters('woo_vipps_html_button', $html, $attrs);
1133 + }
1134 +
1135 + public function button_menu_page() {
1136 + if (!current_user_can('manage_woocommerce')) {
1137 + wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1138 + }
1139 + wp_enqueue_script('vipps-button-webcomponent');
942 1140 ?>
943 1141 <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">
1142 + <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1143 + <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1144 + <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1145 + <input type="hidden" name="action" value="update_vipps_button_settings" />
1146 + <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
947 1147
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'); ?>
1148 + <!-- Express section -->
1149 + <?php $this->button_menu_express_section(); ?>
953 1150
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>
1151 + <!-- submit button -->
1152 + <div id="vipps-button-settings-save">
1153 + <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
966 1154 </div>
1155 + </form>
1156 + </div>
1157 + <?php
1158 + }
967 1159
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>
1160 + private function button_menu_express_section() {
1161 + $options = get_option('vipps_button_options2', []);
1162 + $express = $options['express'] ?? [];
1163 + $configs = $express['configs'] ?? [];
980 1164
981 1165
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>
1166 + $contexts = [
1167 + 'global' => __('Global', 'woo-vipps'),
1168 + 'product' => __('Product', 'woo-vipps'),
1169 + 'catalog' => __('Catalog', 'woo-vipps'),
1170 + 'cart' => __('Cart', 'woo-vipps'),
1171 + 'minicart' => __('Mini cart', 'woo-vipps'),
1172 + 'checkout' => __('Checkout', 'woo-vipps'),
1173 + ];
1174 + $init_context = 'global';
1175 + $init_config = $configs[$init_context] ?? [];
991 1176
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>
1177 + // html button args
1178 + $init_args = $init_config;
1179 + $init_args['id'] = 'vipps-button-express-preview';
997 1180
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>
1181 + ?>
1182 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1183 + <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
1003 1184
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>
1185 + <!-- Context dropdown -->
1186 + <div id="vipps-button-settings-express-context">
1187 + <label>
1188 + <?php _e('Config context', 'woo-vipps'); ?>
1189 + </label>
1190 + <select id="context" onChange='updateContext()'>
1191 + <?php foreach($contexts as $key => $label): ?>
1192 + <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1193 + <?php echo $label ; ?>
1194 + </option>
1195 + <?php endforeach; ?>
1196 + </select>
1197 + <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>
1198 + </div>
1199 +
1009 1200
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>
1201 + <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1202 + On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1203 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1204 + <fieldset>
1205 + <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1206 + <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1207 + <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1208 + </fieldset>
1209 + <fieldset>
1210 + <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1211 + <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1212 + <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1213 + <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1214 + <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1215 + <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1216 + <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1217 + <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1218 + <?php endif; ?>
1219 + </fieldset>
1021 1220
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>
1221 + <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1222 + <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1223 + <?php endif; ?>
1034 1224
1035 - <!-- END EXPRESS SECTION -->
1225 + <fieldset>
1226 + <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1227 + <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1228 + <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1229 + <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1230 + <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1231 + <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1232 + <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1233 + </fieldset>
1234 + <fieldset>
1235 + <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1236 + <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1237 + <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1238 + <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1239 + </fieldset>
1036 1240 </div>
1037 1241
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>
1042 -
1043 - </form>
1242 + <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1243 + <?php echo $this->get_html_button($init_args); ?>
1044 1244 </div>
1045 1245
1046 1246 <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 - }
1247 + // When inputs change, update the preview args. LP 2026-06-24
1248 + jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
1053 1249
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>
1250 + let currentContext = '<?php echo $init_context; ?>';
1251 + let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1252 +
1253 + // Updates the actual html inputs from given config. LP 2026-07-01
1254 + function setInputsFromConfig(context, config) {
1255 + const useGlobalConfig = Boolean(config?.["use-global-config"]);
1256 + const isGlobal = "global" === context;
1257 +
1258 + // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1259 + jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1260 +
1261 + // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1262 + if (!config || (!isGlobal && useGlobalConfig)) {
1263 + config = contextConfigs["global"];
1264 +
1265 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1266 +
1267 + // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1268 + jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1269 + } else {
1270 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1271 + jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1272 + }
1273 +
1274 + Object.entries(config).forEach(([key, val]) => {
1275 + if ("use-global-config" === key) return;
1276 + const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1277 + const type = inputs.prop('type');
1278 + switch (type) {
1279 + case "checkbox":
1280 + inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1281 + break;
1282 + case "radio":
1283 + inputs.filter(`[value="${val}"]`).prop('checked', true);
1284 + break;
1285 + default:
1286 + console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1287 + }
1288 + });
1289 +
1290 + updatePreview();
1291 + }
1292 + // init the starting config from option. LP 2026-06-25
1293 + setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1294 +
1295 + // Update the preview web component's attributes. LP 2026-06-24
1296 + function updatePreview(event) {
1297 + const args = getPreviewArgs();
1298 + // 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
1299 + // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1300 + if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1301 + const button = jQuery('#vipps-button-express-preview');
1302 + button.attr(args);
1303 + }
1304 +
1305 + function getPreviewArgs() {
1306 + const args = {};
1307 + jQuery('#vipps-button-settings-express-args input').each(function () {
1308 + // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1309 + const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1310 + const attr = matches.length ? matches[matches.length - 1][1] : null;
1311 + if (!attr) {
1312 + console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1313 + return;
1314 + }
1315 + if (this.type === 'checkbox') {
1316 + args[attr] = this.checked;
1317 + } else if (this.checked) {
1318 + args[attr] = this.value;
1319 + }
1320 + });
1321 +
1322 + return args;
1323 + }
1324 +
1325 + // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1326 + function updateContext() {
1327 + const wasGlobal = "global" === currentContext;
1328 + const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1329 +
1330 + // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1331 + if (wasGlobal || !useGlobalConfig) {
1332 + contextConfigs[currentContext] = getPreviewArgs();
1333 + } else {
1334 + contextConfigs[currentContext] = {'use-global-config': true};
1335 + }
1336 +
1337 + // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1338 + const newContext = jQuery("#context").val();
1339 + const newConfig = contextConfigs[newContext];
1340 +
1341 + setInputsFromConfig(newContext, newConfig);
1342 + currentContext = newContext;
1343 + }
1344 +
1345 + // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1346 + jQuery('#vipps-button-settings-form').on('formdata', e => {
1347 + const formData = e?.originalEvent?.formData;
1348 + if (!formData) return;
1349 +
1350 + // run this to store current context config before posting. LP 2026-06-24
1351 + updateContext();
1352 +
1353 + // now we can delete the current temporary config from post data. LP 2026-06-24
1354 + const keysToDelete = [];
1355 + for (const [key] of formData.entries()) {
1356 + if (key.startsWith("express[tmpConfig][")) {
1357 + keysToDelete.push(key);
1358 + }
1359 + }
1360 + keysToDelete.forEach(key => formData.delete(key));
1361 +
1362 + // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1363 + Object.entries(contextConfigs).forEach(([context, config]) => {
1364 + Object.entries(config).forEach(([key, val]) => {
1365 + formData.append(`express[configs][${context}][${key}]`, val);
1366 + });
1367 + });
1368 + });
1369 + </script>
1061 1370 <?php
1062 1371 }
1063 1372
1064 1373
@@ -1392,12 +1701,14 @@
1392 1701 <?php
1393 1702 }
1394 1703 // Scripts used in the backend
1395 1704 public function admin_enqueue_scripts($hook) {
1705 +
1706 + wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1707 + $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1708 + wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1396 1709 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1397 - $this->script_add_vippslocale();
1398 -
1399 - wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1710 + $this->script_add_vippslocale('vipps-admin');
1400 1711 wp_enqueue_script('vipps-admin');
1401 1712
1402 1713 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1403 1714 wp_enqueue_style('vipps-fonts');
@@ -1467,47 +1778,132 @@
1467 1778
1468 1779 public function wp_register_scripts () {
1469 1780 // We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues
1470 1781 // (which it only is if Gutenberg is active) or not provided at all, add it now.
1471 - if (!wp_script_is( 'wp-hooks', 'registered')) {
1472 - wp_register_script('wp-hooks', plugins_url('/compat/hooks.min.js', __FILE__));
1473 - }
1474 - wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true');
1782 + wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks', 'wp-api-fetch','vipps-widget-sdk'),filemtime(dirname(__FILE__) . "/js/vipps.js"), true);
1475 1783
1476 1784 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1477 - wp_register_script('vipps-onsite-messageing','https://checkout.vipps.no/on-site-messaging/v1/vipps-osm.js',array(),WOO_VIPPS_VERSION,
1478 - array(
1479 - 'in_footer' => true,
1480 - 'strategy' => 'async',
1481 - ));
1785 + wp_register_script('vipps-onsite-messageing',
1786 + plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1787 + array(),
1788 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1789 + [
1790 + 'in_footer' => true,
1791 + 'strategy' => 'async',
1792 + ],
1793 + );
1482 1794
1795 + add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1796 + if ($handle == 'vipps-widget-sdk') {
1797 + $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1798 + return $tag;
1799 + }
1800 + return $tag;
1801 + },10,3);
1802 +
1803 + wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1804 + array('vipps-button-webcomponent'),
1805 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1806 + ['in_footer' => true]
1807 + );
1808 +
1809 + // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1810 + wp_register_script('vipps-button-webcomponent',
1811 + plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1812 + array(),
1813 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1814 + [
1815 + 'in_footer' => false
1816 + ]
1817 + );
1483 1818 }
1484 1819
1485 1820 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1486 - public function script_add_vippslocale () {
1821 + public function script_add_vippslocale ($handle) {
1487 1822 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1823 + $name = $this->get_payment_method_name();
1488 1824 $strings = array(
1489 - 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1490 - 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1491 - 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1825 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1826 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1827 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1828 + 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1829 + 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1830 + 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1831 + 'cancel'=> __("Cancel", 'woo-vipps'),
1832 + 'close'=> __("Close", 'woo-vipps'),
1833 + 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1834 + 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1835 + 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1836 + 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1837 + 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1838 + 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1839 + 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1840 + 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1841 + 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1842 + 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1492 1843 );
1493 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1844 + wp_localize_script($handle, 'VippsLocale', $strings);
1494 1845 }
1495 1846
1496 1847 public function wp_enqueue_scripts() {
1848 + // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1849 + $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1497 1850 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1498 1851 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1499 - $this->script_add_vippslocale();
1852 + $this->script_add_vippslocale('vipps-gw');
1500 1853
1501 1854 wp_enqueue_script('vipps-gw');
1502 1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1856 + wp_enqueue_script('vipps-button-webcomponent');
1503 1857 }
1504 1858
1859 + // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1860 + // the pay-for-order screen. IOK 2026-09-15
1861 + public function enqueue_classic_checkout_scripts () {
1862 + if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1863 + return;
1864 + }
1865 + // Order-pay is rendered by the classic form even with a Blocks checkout page.
1866 + // It must bypass the check for the parent checkout page's block content.
1867 + if ( ! is_checkout_pay_page() ) {
1868 + $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1869 + $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1870 + ? $utils::is_checkout_block_default()
1871 + : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1505 1872
1873 + if ( $uses_checkout_block ) {
1874 + return;
1875 + }
1876 + }
1877 +
1878 + // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1879 + $relative_path = 'js/vipps-classic-checkout.js';
1880 + wp_enqueue_script(
1881 + 'vipps-classic-checkout',
1882 + plugins_url( $relative_path, __FILE__ ),
1883 + array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1884 + filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1885 + true
1886 + );
1887 +
1888 + if ( is_checkout_pay_page() ) {
1889 + $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1890 + wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1891 + 'orderId' => $order ? $order->get_id() : 0,
1892 + 'orderKey' => $order ? $order->get_order_key() : '',
1893 + 'billingEmail' => $order ? $order->get_billing_email() : '',
1894 + 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1895 + 'nonce' => wp_create_nonce( 'wc_store_api' ),
1896 + 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1897 + 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1898 + ) ) . ';', 'before' );
1899 + }
1900 + }
1901 +
1902 +
1506 1903 public function add_shortcodes() {
1507 1904 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1508 1905 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1509 - add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode'));
1510 1906
1511 1907 // Badges, if using shortcodes
1512 1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1513 1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1512,8 +1908,11 @@
1512 1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1513 1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1514 1910 // Legacy vipps-badge shortcode. LP 19.11.2024
1515 1911 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1912 +
1913 + // special page handling, previously a fake page. LP 2026-08-25
1914 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1516 1915 }
1517 1916
1518 1917
1519 1918 public function log ($what,$type='info') {
@@ -1539,8 +1938,10 @@
1539 1938 }
1540 1939
1541 1940 // Show express button option on checkout form. LP 2026-03-23
1542 1941 public function checkout_before_customer_details_express () {
1942 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1943 + do_action('woo_vipps_checkout_before_customer_details_express');
1543 1944 $gw = $this->gateway();
1544 1945 if (!$gw->show_express_checkout()) return;
1545 1946 $this->express_checkout_section_html();
1546 1947 }
@@ -1548,39 +1949,14 @@
1548 1949 public function express_checkout_section_html() {
1549 1950 $payment_method = $this->get_payment_method_name();
1550 1951 $header_text = __('Express Checkout', 'woo-vipps');
1551 1952 $header = "<legend class='express-header'>$header_text</legend>";
1552 - $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1953 + $div_classes = "legacy-checkout express $payment_method";
1553 1954 echo "<fieldset class='$div_classes'>$header";
1554 - $this->cart_express_checkout_button_html();
1955 + $this->checkout_express_checkout_button_html();
1555 1956 echo '</fieldset>';
1556 1957 }
1557 1958
1558 - public function express_checkout_banner() {
1559 - $gw = $this->gateway();
1560 - if (!$gw->show_express_checkout()) return;
1561 - return $this->express_checkout_banner_html();
1562 - }
1563 -
1564 - public function express_checkout_banner_html() {
1565 - $url = $this->express_checkout_url();
1566 - $url = wp_nonce_url($url,'express','sec');
1567 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1568 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1569 - $logo = $this->get_express_banner_logo();
1570 - $payment_method = $this->get_payment_method_name();
1571 -
1572 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1573 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1574 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1575 -
1576 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1577 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1578 - ?>
1579 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1580 - <?php
1581 - }
1582 -
1583 1959 // Show the express button if reasonable to do so
1584 1960 public function cart_express_checkout_button() {
1585 1961 $gw = $this->gateway();
1586 1962
@@ -1592,29 +1968,71 @@
1592 1968 public function minicart_express_checkout_button() {
1593 1969 $gw = $this->gateway();
1594 1970
1595 1971 if ($gw->show_express_checkout()){
1596 - return $this->cart_express_checkout_button_html(true);
1972 + return $this->cart_express_checkout_button_html('minicart');
1597 1973 }
1598 1974 }
1599 1975
1600 - public function cart_express_checkout_button_html($minicart = false) {
1601 - $url = $this->express_checkout_url();
1602 - $url = wp_nonce_url($url,'express','sec');
1603 - $page = $minicart ? 'minicart' : 'cart';
1604 - $imgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo($page));
1976 + // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1977 + // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1978 + public function add_checkout_button_for_classic () {
1979 + $button = $this->get_html_button_for_context('checkout');
1980 + $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1981 + echo $submit;
1982 + }
1983 +
1984 + public function cart_express_checkout_button_html($context= 'cart') {
1985 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1605 1986 $method = $this->get_payment_method_name();
1606 1987 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1607 - $button = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'><img alt='$title' border=0 src='$imgurl'></a>";
1608 - $button = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1609 - echo $button;
1988 + $url = "#";
1989 + $sec = wp_create_nonce('express');
1990 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1991 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1992 + echo $html;
1610 1993 }
1611 1994
1995 + public function checkout_express_checkout_button_html() {
1996 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1997 + $method = $this->get_payment_method_name();
1998 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1999 + $url = "#";
2000 + $sec = wp_create_nonce('express');
2001 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
2002 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
2003 + echo $html;
2004 + }
2005 +
1612 2006 // 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
1613 2007 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1614 2008 public function buy_now_button_shortcode ($atts) {
1615 - $args = shortcode_atts( array( 'id' => '','variant'=>'','sku' => '',), $atts );
1616 - return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode') . "</div>";
2009 + // The new web component button args. LP 2026-07-02
2010 + $button_args = $this->get_html_button_default_attrs();
2011 + unset($button_args['brand']);
2012 +
2013 + // Variant exists for the product variant. LP 2026-07-02
2014 + if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
2015 + unset($button_args['variant']);
2016 +
2017 + $args = shortcode_atts(
2018 + array(...$button_args,
2019 + 'id' => '','variant'=> '','sku' => '',
2020 + ),
2021 + $atts,
2022 + );
2023 +
2024 + // Variant exists for the product variant. LP 2026-07-02
2025 + $button_args = $args;
2026 + if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
2027 + unset($button_args['button_variant']);
2028 + unset($button_args['sku']);
2029 + unset($button_args['id']);
2030 + // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
2031 + // "Note also that there is a bug in the library, and it currently only renders one language per page."
2032 + // 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
2033 +
2034 + return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
1617 2035 }
1618 2036
1619 2037 // 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
1620 2038 public function express_checkout_button_shortcode() {
@@ -1620,19 +2038,11 @@
1620 2038 public function express_checkout_button_shortcode() {
1621 2039 $gw = $this->gateway();
1622 2040 if (!$gw->cart_supports_express_checkout()) return;
1623 2041 ob_start();
1624 - $this->cart_express_checkout_button_html('shortcode');
2042 + $this->cart_express_checkout_button_html('cart');
1625 2043 return ob_get_clean();
1626 2044 }
1627 - // Show a banner normally shown for non-logged-in-users at the checkout page. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1628 - public function express_checkout_banner_shortcode() {
1629 - $gw = $this->gateway();
1630 - if (!$gw->cart_supports_express_checkout()) return;
1631 - ob_start();
1632 - $this->express_checkout_banner_html();
1633 - return ob_get_clean();
1634 - }
1635 2045
1636 2046 // Manage the various product meta fields
1637 2047 public function process_product_meta ($id, $post) {
1638 2048 // This is for the 'buy now' button
@@ -2158,82 +2568,9 @@
2158 2568 if ($ok) return $callbackdir;
2159 2569 return null;
2160 2570 }
2161 2571
2162 - // Unfortunately, we cannot do any form of portable locking, and we may get callbacks from Vipps arriving at the same moment as we check the status at Vipps,
2163 - // which in the very worst case, for Express Checkout orders, may lead to a double shipping line. Changing this to a queue system is non-trivial, because some of
2164 - // the operations done when modifying the order actually requires the customers session to be active. This operation will make conflicts a litte less probable
2165 - // by implementing something that isn't quite a lock, and the filter may be used to implement proper locking, using e.g. flock, where this can be used
2166 - // (non-distributed environments using unix on standard filesystems. IOK 2020-05-15
2167 - // Returns true if lock succeeds, or false.
2168 - public function lockOrder($order) {
2169 - $orderid = $order->get_id();
2170 - if (has_filter('woo_vipps_lock_order')) {
2171 - $ok = apply_filters('woo_vipps_lock_order', $order);
2172 - if (!$ok) return false;
2173 - } else {
2174 - if(get_transient('order_lock_'.$orderid)) return false;
2175 - $this->lockKey = uniqid();
2176 - set_transient('order_lock_' . $orderid, $this->lockKey, 30);
2177 - }
2178 - add_action('shutdown', function () use ($order) { global $Vipps; $Vipps->unlockOrder($order); });
2179 - return true;
2180 - }
2181 - // If the order is locked, it means it is in the process of being finalized, so for instance, we do *not* want to abandon it
2182 - // in checkout.
2183 - public function isLocked ($order) {
2184 - $orderid = $order->get_id();
2185 - $locked = get_transient('order_lock_'.$orderid);
2186 - return apply_filters('woo_vipps_order_locked', $locked, $order);
2187 - }
2188 - public function unlockOrder($order) {
2189 - $orderid = $order->get_id();
2190 - if (has_action('woo_vipps_unlock_order')) {
2191 - do_action('woo_vipps_unlock_order', $order);
2192 - } else {
2193 - if(get_transient('order_lock_'.$orderid) == $this->lockKey) {
2194 - delete_transient('order_lock_'.$orderid);
2195 - }
2196 - }
2197 - }
2198 2572
2199 - // Functions using flock() and files to lock orders. This is only guaranteed to work on certain setups, ie, non-distributed setups
2200 - // using Unix with normal filesystems (not NFS).
2201 - public function flock_lock_order($order) {
2202 - global $_orderlocks;
2203 - if (!$_orderlocks) $_orderlocks = array();
2204 - $dir = $this->callbackDir();
2205 - if (!$dir) {
2206 - $this->log(__("Cannot use flock() to lock orders: cannot create or write to directory", "woo-vipps"), 'error');
2207 - return true;
2208 - }
2209 - $fname = '.ht-vipps-lock-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction'));
2210 - $path = $dir . DIRECTORY_SEPARATOR . $fname;
2211 - touch($path);
2212 - if (!is_writable($path)) {
2213 - $this->log(__("Cannot use flock() to lock orders: cannot create lockfiles ", "woo-vipps"), 'error');
2214 - return true;
2215 - }
2216 - $handle = fopen($path, 'w+');
2217 - if (flock($handle, LOCK_EX | LOCK_NB)) {
2218 - $_orderlocks[$order->get_id()] = array($handle,$path);
2219 - return true;
2220 - }
2221 - return false;
2222 - }
2223 - public function flock_unlock_order($order) {
2224 - $orderid=$order->get_id();
2225 - global $_orderlocks;
2226 - if (!$_orderlocks) return;
2227 - if (!isset($_orderlocks[$orderid])) return;
2228 - list($handle, $path) = $_orderlocks[$orderid];
2229 - unset($_orderlocks[$orderid]);
2230 - flock($handle, LOCK_UN);
2231 - fclose($handle);
2232 - @unlink($path);
2233 - }
2234 -
2235 -
2236 2573 // Because the prefix used to create the Vipps order id is editable
2237 2574 // by the user, we will store that as a meta and use this for callbacks etc.
2238 2575 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2239 2576 // debug filters and similar.
@@ -2283,77 +2620,93 @@
2283 2620 return null;
2284 2621 }
2285 2622 }
2286 2623
2624 + // Special pages, and some callbacks. IOK 2018-05-18
2625 + public function template_redirect() {
2287 2626
2288 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2289 - public function pre_handle_404($current, $query) {
2290 - if (!is_admin()) {
2291 - $special = $this->is_special_page();
2292 - if ($special) {
2293 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2294 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2295 - return true;
2296 - }
2627 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2628 + // We redirect these to the new location.
2629 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2630 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2631 + $url = static::get_special_page_url();
2632 + $_GET['action'] = 'buy_product';
2633 + $q = build_query($_GET);
2634 + wp_redirect($url . "?" . $q, 302);
2635 + exit();
2297 2636 }
2298 - return $current;
2637 +
2638 + if (static::is_special_page()) {
2639 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2640 + remove_filter('template_redirect', 'redirect_canonical', 10);
2641 + // dont cache special page. LP 2026-08-25
2642 + $this->nocache();
2643 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2644 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2645 + }
2299 2646 }
2300 2647
2301 - // Special pages, and some callbacks. IOK 2018-05-18
2302 - public function template_redirect() {
2303 - global $post;
2304 - // Handle special callbacks
2305 - $special = $this->is_special_page() ;
2648 + // Ran in template redirect for the special page. IOK 2026-09-2
2649 + public function pre_special_page_actions ($action) {
2650 + // Change title dynamically depending on action. LP 2026-09-02
2651 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2306 2652
2307 - if ($special) {
2308 - remove_filter('template_redirect', 'redirect_canonical', 10);
2309 - do_action('woo_vipps_before_handling_special_page', $special);
2653 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2654 + // we start producing content IOK 2026-09-21
2655 + if ($action == 'wait_for_payment') {
2656 + $this->handle_payment_poll_and_redirect();
2657 + }
2310 2658
2311 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2312 - // to intercept express checkout etc. IOK 2022-03-18
2313 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2314 - $this->$special();
2315 - }
2659 + // Some validation is required for this action
2660 + if ($action == 'do_express_checkout') {
2661 + $this->vipps_express_checkout_consistency_check();
2316 2662 }
2663 + // These two actions require an extra script
2664 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2665 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2666 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2667 + ['in_footer'=>true]
2668 + );
2669 + }
2670 + }
2317 2671
2318 - $consentremoval = $this->is_consent_removal();
2319 - if ($consentremoval) {
2320 - remove_filter('template_redirect', 'redirect_canonical', 10);
2321 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2322 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2323 - $this->vipps_consent_removal_callback($consentremoval);
2672 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2673 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2674 + global $wp_query;
2675 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2676 +
2677 + // In block themes the whole template (header, footer, content) renders inside the main
2678 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2679 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2680 + // the queried page so an earlier title doesn't consume this one-shot filter.
2681 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2682 + switch ($_GET['action'] ?? '') {
2683 + case 'wait_for_payment':
2684 + $title = __('Processing order', 'woo-vipps');
2685 + break;
2686 + case 'do_express_checkout':
2687 + case 'buy_product':
2688 + $title = __('Express Checkout', 'woo-vipps');
2689 + break;
2324 2690 }
2325 2691 }
2692 + return $title;
2326 2693 }
2694 +
2327 2695 // Template handling for special pages. IOK 2018-11-21
2696 + // 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
2328 2697 public function template_include($template) {
2329 - $special = $this->is_special_page() ;
2330 - if ($special) {
2698 + if (static::is_special_page()) {
2331 2699 // Get any special template override from the options IOK 2020-02-18
2332 2700 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2333 2701 $found = locate_template($specific,false,false);
2334 2702 if ($found) $template=$found;
2335 2703
2336 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2704 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2337 2705 }
2338 2706 return $template;
2339 2707 }
2340 2708
2341 -
2342 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2343 - private function is_consent_removal () {
2344 -
2345 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2346 - if ( !get_option('permalink_structure')) {
2347 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2348 - return false;
2349 - }
2350 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2351 - return @$_REQUEST['callback'];
2352 - }
2353 - return false;
2354 - }
2355 -
2356 2709 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2357 2710 // the user if using Express Checkout IOK 2020-10-09
2358 2711 public function woocommerce_before_thankyou ($orderid) {
2359 2712 $order = wc_get_order($orderid);
@@ -2358,9 +2711,9 @@
2358 2711 public function woocommerce_before_thankyou ($orderid) {
2359 2712 $order = wc_get_order($orderid);
2360 2713 if ($order) {
2361 2714 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2362 - // -- 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
2715 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2363 2716 $this->maybe_log_in_user($order);
2364 2717 $order->delete_meta_data('_vipps_limited_session');
2365 2718 $order->save();
2366 2719
@@ -2421,9 +2774,8 @@
2421 2774
2422 2775 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2423 2776 // IOK 2025-11-19
2424 2777 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2425 -
2426 2778 }
2427 2779
2428 2780 public function get_payment_method_name() {
2429 2781 return $this->gateway()->get_option('payment_method_name');
@@ -2443,14 +2795,13 @@
2443 2795 public function after_setup_theme() {
2444 2796 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2445 2797 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2446 2798
2447 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2799 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2448 2800 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2449 2801 // is important.
2450 2802 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2451 2803
2452 -
2453 2804 // Callbacks use the Woo API IOK 2018-05-18
2454 2805 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2455 2806 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2456 2807
@@ -2461,19 +2812,21 @@
2461 2812 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2462 2813 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2463 2814
2464 2815 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2465 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2816 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2466 2817 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2467 2818
2468 2819 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2469 2820 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2470 2821
2822 + // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2823 + // is Vipps
2824 + add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2825 + add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2471 2826
2472 - // Special pages and callbacks handled by template_redirect
2473 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2827 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2474 2828 add_action('template_redirect', array($this,'template_redirect'),1);
2475 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2476 2829
2477 2830 // Allow overriding their templates
2478 2831 add_filter('template_include', array($this,'template_include'), 10, 1);
2479 2832
@@ -2480,21 +2833,8 @@
2480 2833 // Ajax endpoints for checking the order status while waiting for confirmation
2481 2834 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2482 2835 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2483 2836
2484 -
2485 - // Buying a single product directly using express checkout IOK 2018-09-28
2486 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2487 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2488 -
2489 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2490 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2491 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2492 -
2493 - // Same thing, but for single products IOK 2018-05-28
2494 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2495 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2496 -
2497 2837 // Handle the cancel unpaid order action when the "hold stock" times out.
2498 2838 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2499 2839 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2500 2840 // For Checkout the rules are different though.
@@ -2573,9 +2913,8 @@
2573 2913 $this->vippsJSConfig = array();
2574 2914 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2575 2915 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2576 2916 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2577 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2578 2917 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2579 2918 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2580 2919 $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());
2581 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2580,11 +2919,17 @@
2580 2919 $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());
2581 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2582 2921 $this->vippsJSConfig['vippslocale'] = get_locale();
2583 2922 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2584 - $this->vippsJSConfig['logoSvgUrl'] = $this->get_payment_logo('buy-now-block');
2585 -
2923 + $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
2924 + $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2925 + $wc_lang = $this->get_html_button_attrs_for_context()['language'];
2926 + if ('store' === $wc_lang) $wc_lang = $this->get_customer_language();
2927 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
2928 + if ('dk' === $wc_lang) $wc_lang = 'da';
2929 + $this->vippsJSConfig['webcomponentLanguage'] = $wc_lang;
2586 2930
2931 +
2587 2932 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2588 2933 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2589 2934 // Ensure gateways are loaded at this point IOK 2026-05-27
2590 2935 require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
@@ -2731,9 +3076,10 @@
2731 3076 return;
2732 3077 }
2733 3078
2734 3079 $captured = intval($order->get_meta('_vipps_captured'));
2735 - $capremain = intval($order->get_meta('_vipps_capture_remaining'));
3080 + // noncapturable should never be greater than capture remaining, so this *should* not be negative. LP 2026-06-12
3081 + $capremain = intval($order->get_meta('_vipps_capture_remaining')) - intval($order->get_meta('_vipps_noncapturable'));
2736 3082 if ($captured && (!$capremain || $capremain < 2)) {
2737 3083 print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>";
2738 3084 return;
2739 3085 }
@@ -2759,14 +3105,14 @@
2759 3105
2760 3106 $raw_post = @file_get_contents( 'php://input' );
2761 3107 $result = @json_decode($raw_post,true);
2762 3108
2763 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3109 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2764 3110 // .. and the epayment webhooks 2023-12-19
2765 3111 $ischeckout = false;
2766 3112 $iswebhook = false;
2767 3113 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2768 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3114 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2769 3115 if ($callback == 'checkout') {
2770 3116 $ischeckout = true;
2771 3117 }
2772 3118 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3180,10 +3526,10 @@
3180 3526 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3181 3527 exit();
3182 3528 }
3183 3529
3184 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3185 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3530 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3531 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3186 3532 // This needs to be done before "calculate totals".
3187 3533 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3188 3534 $ischeckout = $order->get_meta('_vipps_checkout');
3189 3535
@@ -3359,9 +3705,9 @@
3359 3705 ), 'debug');
3360 3706
3361 3707 }
3362 3708
3363 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3709 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3364 3710 // 'dynamic pricing' 2023-01-26
3365 3711 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3366 3712 $taxrate = 0;
3367 3713 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3455,9 +3801,9 @@
3455 3801 $shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null;
3456 3802
3457 3803 $tax = $rate->get_shipping_tax() ?: 0;
3458 3804 $cost = $rate->get_cost() ?: 0;
3459 - $label = $rate->get_label();
3805 + $label = html_entity_decode($rate->get_label());
3460 3806
3461 3807 if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) {
3462 3808 $has_free_shipping = true;
3463 3809 }
@@ -3483,13 +3829,13 @@
3483 3829 $shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,''));
3484 3830 $shippingcost = max($shippingcostA, $shippingcostB);
3485 3831
3486 3832 $vippsmethod['shippingCost'] = $shippingcost;
3487 - $vippsmethod['shippingMethod'] = $rate->get_label();
3833 + $vippsmethod['shippingMethod'] = html_entity_decode($rate->get_label());
3488 3834 $vippsmethod['shippingMethodId'] = $key;
3489 3835 $vippsmethods[]=$vippsmethod;
3490 3836
3491 - // Metadata and settings stored for later use for Vipps Checkout
3837 + // Metadata and settings stored for later use for Checkout
3492 3838 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3493 3839 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3494 3840 // finalize the order. IOK 2025-08-15
3495 3841 $ratemap[$key]=$rate;
@@ -3507,9 +3853,9 @@
3507 3853 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3508 3854 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3509 3855 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3510 3856
3511 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3857 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3512 3858 // IOK 2025-08-15 and new Express yet another slightly different format.
3513 3859 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3514 3860 if ($ischeckout) {
3515 3861 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -3799,9 +4145,9 @@
3799 4145 WC()->cart->calculate_totals();
3800 4146 WC()->cart->set_session();
3801 4147 return true;
3802 4148 } catch (Exception $e) {
3803 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4149 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
3804 4150 return false;
3805 4151 }
3806 4152 }
3807 4153
@@ -3830,9 +4176,9 @@
3830 4176 $tax = $rate->get_shipping_tax() ?: 0;
3831 4177 $cost = $rate->get_cost() ?: 0;
3832 4178
3833 4179 $method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
3834 - $method['shippingMethod'] = $rate->get_label();
4180 + $method['shippingMethod'] = html_entity_decode($rate->get_label());
3835 4181 // We may not really need the tax stashed here, but just to be sure.
3836 4182 $method['shippingMethodId'] = $rate->get_id() . ";" . $tax;
3837 4183 $methods[]= $method;
3838 4184
@@ -3885,17 +4231,8 @@
3885 4231 header("X-Accel-Expires: 0");
3886 4232 }
3887 4233
3888 4234
3889 -
3890 - // Handle DELETE on a vipps consent removal callback
3891 - public function vipps_consent_removal_callback ($callback) {
3892 - Vipps::nocache();
3893 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3894 - // to escape here when the API is changed. IOK 2020-10-14
3895 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3896 - }
3897 -
3898 4235 public function woocommerce_payment_gateways($methods) {
3899 4236 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
3900 4237 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3901 4238 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -3920,9 +4257,11 @@
3920 4257 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
3921 4258 return $url;
3922 4259 }
3923 4260 $url = $this->express_checkout_url();
3924 - $url = wp_nonce_url($url,'express','sec');
4261 + // At this point, there is always a query argument here. IOK 2026-09-21
4262 + $nonce = wp_create_nonce('express');
4263 + $url = $url . "&sec=$nonce";
3925 4264
3926 4265 return $url;
3927 4266 }
3928 4267
@@ -3970,9 +4309,9 @@
3970 4309 if ($currentstatus != 'initiated') {
3971 4310 $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');
3972 4311 return;
3973 4312 }
3974 - $this->check_status_of_pending_order($o, false, false);
4313 + $this->check_status_of_pending_order($o, false);
3975 4314 }
3976 4315 }
3977 4316
3978 4317 // 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 -
@@ -3977,30 +4316,35 @@
3977 4316
3978 4317 // 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 -
3979 4318 // e.g. wp-cron. IOK 2021-06-21
3980 4319 // Stop restoring session in wp-cron too. IOK 2021-08-23
3981 - public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3982 - $express = $order->get_meta('_vipps_express_checkout');
3983 - $vippstatus = $order->get_meta('_vipps_status');
3984 - if ($express && $maybe_restore_session) {
3985 - $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3986 - $this->callback_restore_session($order->get_id());
3987 - }
4320 + // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4321 + public function check_status_of_pending_order($order, $allow_retry=true) {
3988 4322 $gw = $this->gateway();
3989 4323
3990 4324 $order_status = null;
3991 4325 try {
3992 4326 $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()));
3993 - $order_status = $gw->callback_check_order_status($order, $allow_retry);
4327 +
4328 + // Poll status and correct woo status. LP 2026-05-19
4329 + $order_data = $gw->get_payment_details($order);
4330 +
4331 + // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4332 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4333 + /* translators: company name */
4334 + $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4335 + return;
4336 + }
4337 +
4338 + $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4339 + $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4340 + $order_status = $order->get_status();
4341 +
3994 4342 $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');
3995 4343 } catch (Exception $e) {
3996 4344 $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');
3997 4345 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
3998 4346 }
3999 - // Ensure we don't keep using an old session for more than one order here.
4000 - if ($express && $maybe_restore_session) {
4001 - $this->callback_destroy_session();
4002 - }
4003 4347 return $order_status;
4004 4348 }
4005 4349
4006 4350 // 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
@@ -4013,20 +4357,40 @@
4013 4357 }
4014 4358 }
4015 4359
4016 4360 public function activate () {
4017 - static::maybe_add_cron_event();
4018 - $gw = $this->gateway();
4361 + static::maybe_add_cron_event();
4362 + $gw = $this->gateway();
4019 4363
4020 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4021 - if ($gw->get_option('orderprefix') == 'Woo') {
4022 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4023 - }
4024 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4025 - $gw->initialize_webhooks();
4026 - $this->payment_method_name = $gw->get_option('payment_method_name');
4027 - }
4364 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4365 + if ($gw->get_option('orderprefix') == 'Woo') {
4366 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4367 + }
4368 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4369 + $gw->initialize_webhooks();
4370 + $this->payment_method_name = $gw->get_option('payment_method_name');
4028 4371
4372 +
4373 + // Check if the special page is noted and actually does exist
4374 + $special = static::get_special_page_id();
4375 + if ($special) {
4376 + $special_page = get_post($special);
4377 + if ($special_page && 'trash' !== $special_page->post_status) {
4378 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4379 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4380 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4381 + wp_update_post([
4382 + 'ID' => $special,
4383 + 'post_content' => $new_content,
4384 + ]);
4385 + }
4386 + } else {
4387 + delete_option('woocommerce_vipps_special_page_page_id');
4388 + }
4389 + }
4390 +
4391 + }
4392 +
4029 4393 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4030 4394 public static function deactivate() {
4031 4395 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4032 4396 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4039,9 +4403,9 @@
4039 4403 // Delete all settings if checked in settings menu. LP 2025-10-06
4040 4404 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4041 4405 if ($should_delete) {
4042 4406 // Delete options.
4043 - $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4407 + $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'];
4044 4408 foreach($options as $option) {
4045 4409 delete_option($option);
4046 4410 }
4047 4411 }
@@ -4079,9 +4443,10 @@
4079 4443 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4080 4444 private function maybe_set_vipps_as_default() {
4081 4445 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4082 4446 $gw = $this->gateway();
4083 - if ($gw->get_option('vippsdefault')=='yes') {
4447 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4448 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4084 4449 WC()->session->set('chosen_payment_method', $gw->id);
4085 4450 }
4086 4451 }
4087 4452
@@ -4093,28 +4458,12 @@
4093 4458 $order = wc_get_order($order->get_id());
4094 4459 $order_status = $order->get_status();
4095 4460
4096 4461 if ($order_status != 'pending') return $order_status;
4097 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4098 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4099 - if ($order_status == 'pending') {
4100 - if (WC()->session) {
4101 - $now = time();
4102 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4103 - if (!$then) {
4104 - $then = $now;
4105 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4106 - }
4107 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4108 - return $order_status;
4109 - }
4110 - } else {
4111 - // No session shouldn't be possible, but if it is..
4112 - return $order_status;
4113 - }
4114 - }
4462 +
4463 + $gw = $this->gateway();
4115 4464 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4116 - return $this->check_status_of_pending_order($order);
4465 + $newstatus = $gw->poll_and_check_order_status($order);
4117 4466 }
4118 4467
4119 4468 // In some situations we have to empty the cart when the user goes to Vipps, so
4120 4469 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4182,17 +4531,18 @@
4182 4531
4183 4532 // Maybe log in user
4184 4533 // It is done on the thank-you page of the order, and only for express checkout.
4185 4534 function maybe_log_in_user ($order) {
4535 +
4186 4536 if (is_user_logged_in()) return;
4187 4537 if (!$order || ! self::is_vipps_order($order)) return;
4188 4538
4189 4539 // We *do* want to log in express checkout customers, but not those that
4190 - // use the Vipps Checkout solution - those can change their emails in the
4540 + // use the Checkout solution - those can change their emails in the
4191 4541 // checkout screen. IOK 2021-09-03
4192 4542 $do_login = $order->get_meta('_vipps_express_checkout');
4193 4543
4194 - // We will not log in Vipps Checkout users unless the option for that is true
4544 + // We will not log in Checkout users unless the option for that is true
4195 4545 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4196 4546 $do_login = false;
4197 4547 }
4198 4548
@@ -4227,9 +4577,9 @@
4227 4577
4228 4578 // Both Checkout and Express Checkout have the below value set to true
4229 4579 if (!$order->get_meta('_vipps_express_checkout')) return;
4230 4580
4231 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4581 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4232 4582 // IOK 2023-07-27
4233 4583 $ischeckout = $order->get_meta('_vipps_checkout');
4234 4584 if ($ischeckout) {
4235 4585 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4329,129 +4679,233 @@
4329 4679 }
4330 4680 if (!$o) return;
4331 4681 if (!$o->get_meta('_vipps_single_product_express')) return;
4332 4682 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4333 - if ($failed) WC()->cart->empty_cart();
4683 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4684 + WC()->cart->empty_cart();
4334 4685 $this->restore_cart($o);
4335 4686 }
4336 4687
4337 4688
4338 - public function ajax_vipps_buy_single_product () {
4339 - Vipps::nocache();
4340 - static::set_locale_if_in_header();
4341 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4342 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4343 - $session = WC()->session;
4344 - if (!$session->has_session()) {
4345 - $session->set_customer_session_cookie(true);
4689 + // Actually create a express checkout order object, with no shipping or personal information, returning information about
4690 + // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4691 + // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4692 + private function create_and_process_express_order() {
4693 + $result = null;
4694 + $gw = $this->gateway();
4695 + try {
4696 + $orderid = $gw->create_partial_order();
4697 + do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4698 + } catch (Exception $e) {
4699 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4700 + return $result;
4701 + }
4702 + if (!$orderid) {
4703 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4704 + return $result;
4346 4705 }
4347 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4348 4706
4349 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4350 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4351 - $url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url());
4707 + try {
4708 + $this->maybe_add_static_shipping($gw,$orderid);
4709 + } catch (Exception $e) {
4710 + $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4711 + $this->log($e->getMessage(),'error');
4712 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4713 + return $result;
4714 + }
4352 4715
4353 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4354 - wp_send_json($result);
4355 - exit();
4716 + // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4717 + $ok = $gw->process_payment($orderid);
4718 + if ($ok && $ok['result'] == 'success') {
4719 + $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4720 + return $result;
4721 + }
4722 + $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4723 + return $result;
4356 4724 }
4357 4725
4358 - public function ajax_do_express_checkout () {
4359 - check_ajax_referer('do_express','sec');
4360 - Vipps::nocache();
4361 - static::set_locale_if_in_header();
4726 + // This creates a simple hash for the 'current order' which we will store in the session if we proceed to checkout. We use this to
4727 + // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4728 + public function create_order_hash($args=null) {
4729 + // If we have no arguments, we'll hash the cart.
4730 + if (empty($args)) {
4731 + $cartitems = WC()->cart->get_cart();
4732 + $orderspec = array();
4733 + foreach($cartitems as $item => $values) {
4734 + $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4735 + }
4736 + $args = $orderspec;
4737 + }
4738 + return md5(serialize($args));
4739 + }
4740 +
4741 +
4742 + // This method may provide HTML form elements to ask a user questions after starting
4743 + // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4744 + // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4745 + public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4746 + $elements = [];
4747 + $html = "";
4748 +
4749 + // First, let's check if we need to confirm the purchase.
4750 + $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4751 + if ($last_express_purchase_hash) {
4752 + list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4753 + $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4754 + if ($hash == $current_hash && (time() <= $cutoff )) {
4755 + $order = wc_get_order($orderid);
4756 + $status = $order ? $order->get_status() : false;
4757 + // IOK TODO/FIXME actually, if the order is pending/failed/cancelled and *identical* to our current productinfo, we could plausibly do a restart here. Would probably require careful checking though, and
4758 + // a different flow. IOK 2026-09-17
4759 + if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4760 + $header = __("Are you sure?",'woo-vipps');
4761 + $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
4762 + $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4763 + $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
4764 + }
4765 + }
4766 + }
4767 +
4362 4768 $gw = $this->gateway();
4769 + $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4770 + $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4771 + $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4363 4772
4364 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4365 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4366 - wp_send_json($result);
4367 - exit();
4773 + if ($askForTerms) {
4774 + $termsHTML = '';
4775 + // Include shop terms
4776 + ob_start();
4777 + wc_get_template('checkout/terms.php');
4778 + $termsHTML = ob_get_clean();
4779 + $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4780 + $elements['terms'] = $termsHTML;
4368 4781 }
4369 4782
4783 + // Custom fields
4784 + ob_start();
4785 + do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4786 + $extra_fields = ob_get_clean();
4787 + if (!empty($extra_fields)) {
4788 + $elements['extra'] = $extra_fields;
4789 + }
4370 4790
4371 -
4791 + if (!empty($elements)) {
4792 + $html = join("\n", array_values($elements));
4793 + $msg = join(",", array_keys($elements));
4794 + return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4795 + }
4372 4796
4797 + return false;
4798 +
4799 + }
4800 +
4801 + public function rest_do_express_checkout ($request) {
4802 + Vipps::nocache();
4803 + check_ajax_referer('express', 'sec');
4804 + static::set_locale_if_in_header();
4805 + $args = $request->get_json_params();
4806 + if (!$args) {
4807 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4808 + }
4809 +
4810 + // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4811 + if ( is_null( WC()->cart ) ) {
4812 + WC()->frontend_includes();
4813 + if ( ! WC()->session instanceof WC_Session ) {
4814 + WC()->session = new WC_Session_Handler();
4815 + WC()->session->init();
4816 + }
4817 + if (is_null( WC()->customer)) {
4818 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4819 + }
4820 + WC()->cart = new WC_Cart();
4821 + WC()->cart->get_cart_from_session();
4822 + }
4823 +
4824 +
4825 + $gw = $this->gateway();
4826 + if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4827 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4828 + return $result;
4829 + }
4373 4830 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4374 4831 $toolate = false;
4375 4832 $msg = "";
4376 4833 $valid = WC()->cart->check_cart_item_validity();
4377 4834 if ( is_wp_error( $valid) ) {
4378 - $toolate = true;
4379 - $msg = "<br>" . $valid->get_error_message();
4835 + $toolate = true;
4836 + $msg = "<br>" . $valid->get_error_message();
4380 4837 }
4381 4838 $stock = WC()->cart->check_cart_item_stock();
4382 - if ( is_wp_error( $stock) ) {
4383 - $toolate = true;
4384 - $msg = "<br>" . $stock->get_error_message();
4385 - }
4839 + if ( is_wp_error( $stock) ) {
4840 + $toolate = true;
4841 + $msg = "<br>" . $stock->get_error_message();
4842 + }
4386 4843
4387 4844 if ($toolate) {
4388 4845 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4389 - wp_send_json($result);
4390 - exit();
4846 + return $result;
4391 4847 }
4392 4848
4393 - try {
4394 - $orderid = $gw->create_partial_order();
4395 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4396 - } catch (Exception $e) {
4397 - $this->log($e->getMessage(),'error');
4398 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4399 - wp_send_json($result);
4400 - exit();
4401 - }
4402 - if (!$orderid) {
4403 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4404 - wp_send_json($result);
4405 - exit();
4849 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4850 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4851 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4852 + $cookies = $args['cookies'] ?? [];
4853 + foreach($cookies as $key => $value) {
4854 + if (!isset($_COOKIE[$key])) {
4855 + $_COOKIE[$key] = $value;
4856 + }
4406 4857 }
4858 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4859 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4860 + $others =$args['post'] ?? [];
4861 + foreach($args['post'] as $key=>$value) {
4862 + $_POST[$key] = $value;
4863 + }
4407 4864
4408 - try {
4409 - $this->maybe_add_static_shipping($gw,$orderid);
4410 - } catch (Exception $e) {
4411 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4412 - $this->log($e->getMessage(),'error');
4413 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4414 - wp_send_json($result);
4415 - exit();
4865 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4866 + $current_hash = $this->create_order_hash();
4867 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4868 + if (!$confirmation) {
4869 + $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4870 + if (!empty($result)) {
4871 + return $result;
4872 + }
4416 4873 }
4417 -
4418 - $ok = $gw->process_payment($orderid);
4419 - if ($ok && $ok['result'] == 'success') {
4420 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4421 - wp_send_json($result);
4422 - exit();
4874 +
4875 + $result = $this->create_and_process_express_order();
4876 + if ($result['ok'] == 1) {
4877 + $orderid = $result['orderid'];
4878 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4879 + WC()->session->save_data();
4423 4880 }
4424 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4425 - wp_send_json($result);
4426 - exit();
4881 + return $result;
4882 +
4427 4883 }
4428 4884
4429 - // Same as ajax_do_express_checkout, but for a single product/variation. Duplicate code because we want to manipulate the cart differently here. IOK 2018-09-25
4430 - public function ajax_do_single_product_express_checkout() {
4431 - check_ajax_referer('do_express','sec');
4432 - Vipps::nocache();
4885 +
4886 + // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4887 + public function rest_do_single_product_express_checkout ($request) {
4888 + Vipps::nocache();
4433 4889 static::set_locale_if_in_header();
4434 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4435 - $gw = $this->gateway();
4436 -
4437 - if (!$gw->express_checkout_available()) {
4438 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4439 - wp_send_json($result);
4440 - exit();
4890 + $args = $request->get_json_params();
4891 + if (!$args) {
4892 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4441 4893 }
4894 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4442 4895
4896 + // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4897 + $varid = intval($args['variation_id'] ?? 0);
4898 + $prodid = intval($args['product_id'] ?? 0);
4899 + $sku = sanitize_text_field($args['sku'] ?? "");
4900 + $quantity = max(1, intval($args['quantity'] ?? 0));
4443 4901
4444 - // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4445 - // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4446 - $varid = intval(@$_POST['variation_id']);
4447 - $prodid = intval(@$_POST['product_id']);
4448 - $sku = sanitize_text_field(@$_POST['sku']);
4449 - $quant = intval(@$_POST['quantity']);
4450 4902
4451 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4452 - $variations = array();
4453 - foreach ($_POST as $key => $value ) {
4903 + // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4904 + // We just need to sanitize them.
4905 + $variations = [];
4906 + $invars = $args['post'] ?? [];
4907 + foreach ($invars as $key => $value) {
4454 4908 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4455 4909 continue;
4456 4910 }
4457 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4456,15 +4910,94 @@
4456 4910 }
4457 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4458 4912 }
4459 4913
4460 - $product = null;
4461 - $variant = null;
4462 - $parent = null;
4463 - $parentid = null;
4464 - $quantity = 1;
4465 - if ($quant && $quant>1) $quantity=$quant;
4914 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4915 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4916 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4917 + $cookies = $args['cookies'] ?? [];
4918 + foreach($cookies as $key => $value) {
4919 + if (!isset($_COOKIE[$key])) {
4920 + $_COOKIE[$key] = $value;
4921 + }
4922 + }
4466 4923
4924 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4925 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4926 + $others =$args['post'] ?? [];
4927 + foreach($args['post'] as $key=>$value) {
4928 + $_POST[$key] = $value;
4929 + }
4930 +
4931 + // Since this is the REST api, we need to load the cart manually here. *Not* loading the cart could be an option but unpredictable. IOK 2026-08-27
4932 + if ( is_null( WC()->cart ) ) {
4933 + WC()->frontend_includes();
4934 + if ( ! WC()->session instanceof WC_Session ) {
4935 + WC()->session = new WC_Session_Handler();
4936 + WC()->session->init();
4937 +
4938 + // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
4939 + if (! WC()->session->get_session_cookie()) {
4940 + $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
4941 + add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
4942 + try {
4943 + WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
4944 + } finally {
4945 + remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
4946 + }
4947 + }
4948 + }
4949 + if (is_null( WC()->customer)) {
4950 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4951 + }
4952 + WC()->cart = new WC_Cart();
4953 + WC()->cart->get_cart_from_session();
4954 + }
4955 +
4956 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4957 + // We calculate this here so we can add it to the session later. IOK 2026-09-09
4958 + $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
4959 + $current_hash = $this->create_order_hash($orderspec);
4960 +
4961 + // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
4962 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4963 + if (!$confirmation) {
4964 + $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
4965 + if (!empty($result)) {
4966 + $response = new WP_REST_Response($result);
4967 + $response->set_status(200);
4968 + return $response;
4969 + }
4970 + }
4971 +
4972 + // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
4973 + $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
4974 + // And if we're going to express now so let's note the order. IOK 2026-08-27. Now this assumes success, but *basically* I think this is ok.
4975 + // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
4976 + if ($result['ok'] == 1) {
4977 + $orderid = $result['orderid'];
4978 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4979 + WC()->session->save_data();
4980 + }
4981 +
4982 + $response = new WP_REST_Response($result);
4983 + $response->set_status(200);
4984 +
4985 + return $response;
4986 + }
4987 +
4988 + // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
4989 + private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
4990 + require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4991 + $gw = $this->gateway();
4992 +
4993 + if (!$gw->express_checkout_available()) {
4994 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4995 + return $result;
4996 + }
4997 + // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4998 + // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4999 +
4467 5000 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4468 5001 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
4469 5002 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4470 5003 try {
@@ -4477,17 +5010,14 @@
4477 5010 $product = wc_get_product($skuid);
4478 5011 }
4479 5012 } catch (Exception $e) {
4480 5013 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4481 - wp_send_json($result);
4482 - exit();
5014 + return $result;
4483 5015 }
4484 5016
4485 -
4486 5017 if (!$product) {
4487 5018 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4488 - wp_send_json($result);
4489 - exit();
5019 + return $result;
4490 5020 }
4491 5021
4492 5022 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4493 5023 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4494,34 +5024,30 @@
4494 5024
4495 5025 // This can't really happen, but if it did..
4496 5026 if ($prodid && $parentid && ($prodid != $parentid)) {
4497 5027 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4498 - wp_send_json($result);
4499 - exit();
5028 + return $result;
4500 5029 }
4501 5030 if (!$gw->product_supports_express_checkout($product)) {
4502 5031 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4503 - wp_send_json($result);
4504 - exit();
5032 + return $result;
4505 5033 }
4506 5034
4507 5035 // Somebody addded the wrong SKU
4508 5036 if ($product->get_type() == 'variable'){
4509 5037 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4510 - wp_send_json($result);
4511 - exit();
5038 + return $result;
4512 5039 }
4513 5040 // Final check of availability
4514 5041 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4515 5042 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4516 - wp_send_json($result);
4517 - exit();
5043 + return $result;
4518 5044 }
4519 5045
4520 5046 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4521 -
4522 5047 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4523 5048 // because some plugins actually override this.
5049 + // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
4524 5050 $current_cart = clone WC()->cart;
4525 5051 WC()->cart->empty_cart();
4526 5052
4527 5053 if ($parent && $parent->get_type() == 'variable') {
@@ -4528,50 +5054,22 @@
4528 5054 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4529 5055 } else {
4530 5056 WC()->cart->add_to_cart($product->get_id(),$quantity);
4531 5057 }
5058 + WC()->session->save_data();
4532 5059
4533 - try {
4534 - $orderid = $gw->create_partial_order();
4535 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4536 - } catch (Exception $e) {
4537 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4538 - wp_send_json($result);
4539 - exit();
4540 - }
5060 + $result = $this->create_and_process_express_order();
4541 5061
4542 - if (!$orderid) {
4543 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4544 - wp_send_json($result);
4545 - exit();
5062 + if ($result['ok'] ?? false) {
5063 + // Single product purchase, so save any contents of the real cart
5064 + $orderid = $result['orderid'];
5065 + $order = wc_get_order($orderid);
5066 + $order->update_meta_data('_vipps_single_product_express',true);
5067 + $order->save();
5068 + $this->save_cart($order,$current_cart);
4546 5069 }
4547 5070
4548 - try {
4549 - $this->maybe_add_static_shipping($gw,$orderid);
4550 - } catch (Exception $e) {
4551 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4552 - $this->log($e->getMessage(),'error');
4553 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4554 - wp_send_json($result);
4555 - exit();
4556 - }
4557 -
4558 -
4559 - // Single product purchase, so save any contents of the real cart
4560 - $order = wc_get_order($orderid);
4561 - $order->update_meta_data('_vipps_single_product_express',true);
4562 - $order->save();
4563 - $this->save_cart($order,$current_cart);
4564 -
4565 - $ok = $gw->process_payment($orderid);
4566 - if ($ok && $ok['result'] == 'success') {
4567 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4568 - wp_send_json($result);
4569 - exit();
4570 - }
4571 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4572 - wp_send_json($result);
4573 - exit();
5071 + return $result;
4574 5072 }
4575 5073
4576 5074 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4577 5075 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4614,9 +5112,9 @@
4614 5112 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4615 5113 }
4616 5114 }
4617 5115
4618 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5116 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4619 5117 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4620 5118 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4621 5119 // 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
4622 5120 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4637,9 +5135,9 @@
4637 5135 $transaction = sanitize_text_field(@$_POST['transaction']);
4638 5136
4639 5137 $sessionorders= WC()->session->get('_vipps_session_orders');
4640 5138 if (!isset($sessionorders[$orderid])) {
4641 - wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5139 + wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
4642 5140 }
4643 5141
4644 5142 $order = wc_get_order($orderid);
4645 5143 if (!$order) {
@@ -4687,46 +5185,37 @@
4687 5185 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4688 5186 return false;
4689 5187 }
4690 5188
4691 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4692 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4693 - private function make_vipps_url($what) {
4694 - if ( !get_option('permalink_structure')) {
4695 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4696 - }
4697 - return trailingslashit(home_url($what, 'https'));
5189 + // 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
5190 + private function make_special_page_url($action) {
5191 + return add_query_arg('action', $action, $this->get_special_page_url());
4698 5192 }
5193 +
4699 5194 public function payment_return_url() {
4700 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5195 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4701 5196 }
4702 5197 public function express_checkout_url() {
4703 - return $this->make_vipps_url('vipps-express-checkout');
5198 + return $this->make_special_page_url('do_express_checkout');
4704 5199 }
4705 5200 public function buy_product_url() {
4706 - return $this->make_vipps_url('vipps-buy-product');
5201 + return $this->make_special_page_url('buy_product');
4707 5202 }
4708 5203
4709 - // Return the method in the Vipps
4710 - public function is_special_page() {
4711 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4712 - $method = null;
4713 - if ( get_option('permalink_structure')) {
4714 - foreach($specials as $special=>$specialmethod) {
4715 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4716 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4717 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4718 - $method = $specialmethod; break;
4719 - }
4720 - }
4721 - } else {
4722 - if (isset($_GET['VippsSpecialPage'])) {
4723 - $method = @$specials[$_GET['VippsSpecialPage']];
4724 - }
4725 - }
4726 - return $method;
5204 + public static function is_special_page() {
5205 + $id = static::get_special_page_id();
5206 + return $id && is_page($id);
4727 5207 }
4728 5208
5209 + public static function get_special_page_id() {
5210 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5211 + return $id > 0 ? $id : null;
5212 + }
5213 +
5214 + public static function get_special_page_url() {
5215 + return get_permalink(static::get_special_page_id());
5216 + }
5217 +
4729 5218 // Just create a spinner and a overlay.
4730 5219 public function spinner () {
4731 5220 $flavour = sanitize_title($this->get_payment_method_name());
4732 5221 ob_start();
@@ -4747,164 +5236,22 @@
4747 5236 return apply_filters('woo_vipps_spinner', ob_get_clean());
4748 5237 }
4749 5238
4750 5239
4751 - // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4752 - // Fallbacks to defaults for each payment method. LP 2025-12-15
4753 - public function get_express_logo($payment_method, $lang, $variant) {
4754 - $base = plugins_url('img', __FILE__);
4755 -
4756 - // 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
4757 - $img_map = [
4758 - "vipps" => [
4759 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4760 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4761 - "en" => [
4762 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4763 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4764 - "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4765 - "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4766 - "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4767 - "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4768 - "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4769 - "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4770 -
4771 - ],
4772 - "no" => [
4773 - "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4774 - "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4775 - "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4776 - "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4777 - "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4778 - "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4779 - "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4780 - "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4781 - ],
4782 - "se" => [
4783 - "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4784 - "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4785 - "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4786 - "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4787 - "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4788 - "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4789 - "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4790 - "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4791 -
4792 - ],
4793 - ],
4794 - "mobilepay" => [
4795 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4796 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4797 - "en" => [
4798 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4799 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4800 - "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4801 - "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4802 - "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4803 - "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4804 - "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4805 - "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4806 -
4807 - ],
4808 - "dk" => [
4809 - "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4810 - "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4811 - "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4812 - "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4813 - "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4814 - "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4815 - "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4816 - "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4817 - ],
4818 - "fi" => [
4819 - "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4820 - "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4821 - "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4822 - "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4823 - "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4824 - "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4825 - "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4826 - "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4827 -
4828 - ],
4829 - ],
4830 -
4831 - ];
4832 -
4833 - $payment = strtolower($payment_method);
4834 - if ($lang === 'store') $lang = $this->get_customer_language();
4835 -
4836 - // Dont give a default if payment method not found. LP 2025-12-12
4837 - if (!array_key_exists($payment, $img_map)) {
4838 - return null;
4839 - }
4840 - $payment_map = $img_map[$payment];
4841 -
4842 - $img = null;
4843 - if (array_key_exists($lang, $payment_map)
4844 - && is_array($payment_map[$lang])
4845 - && array_key_exists($variant, $payment_map[$lang])) {
4846 - $img = @$payment_map[$lang][$variant];
4847 - }
4848 -
4849 - // Default fallback behaviour
4850 - if (!$img) {
4851 - $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4852 -
4853 - // First try getting default for payment method + language. LP 2026-01-16
4854 - if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4855 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4856 - $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');
4857 - $img = @$payment_map[$lang][$default];
4858 - }
4859 -
4860 - // If not found, then try global default for payment method. LP 2026-01-16
4861 - if (!$img) {
4862 - $img = @$payment_map[$default];
4863 - }
4864 -
4865 - // Found no logo at all, log this. LP 2026-01-16
4866 - if (!$img) {
4867 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4868 - $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');
4869 - }
4870 - }
4871 - return $img;
5240 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5241 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5242 + public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5243 + return $this->get_html_button_for_context($context);
4872 5244 }
4873 5245
5246 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
4874 5247 // Get payment logo based on payment method, then language NT 2023-11-30
4875 - // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4876 - public function get_payment_logo($page = null) {
4877 - $lang = $this->get_customer_language();
4878 - $payment_method = $this->get_payment_method_name();
4879 - $variant = $this->get_express_logo_page_variant($page);
4880 - $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4881 - return $logo_url;
5248 + // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5249 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5250 + public function get_payment_logo($context = 'global') {
5251 + return $this->get_express_logo(null, null, null, $context);
4882 5252 }
4883 5253
4884 - /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4885 - private function get_express_logo_page_variant($page = null) {
4886 - $options = get_option('vipps_button_options');
4887 -
4888 - // Init defaults, use mini version by default in below pages. LP 2025-12-17
4889 - $use_mini = in_array($page, ['catalog']);
4890 - $variant = "";
4891 -
4892 - // Find correct variant from button settings. LP 2025-12-17
4893 - if (is_array($options) && array_key_exists('express', $options)) {
4894 - if (array_key_exists($page, $options['express']['force-mini'])) {
4895 - $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4896 - }
4897 - $key = $use_mini ? 'mini-variant' : 'variant';
4898 - $variant = sanitize_title($options['express'][$key]) ?? '';
4899 - }
4900 -
4901 - if (!$variant) {
4902 - $variant = $use_mini ? "default-mini" : "default";
4903 - }
4904 - return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4905 - }
4906 -
4907 5254 // Get express banner logo based on payment method. LP 2025-09-03
4908 5255 private function get_express_banner_logo() {
4909 5256 $payment_method = $this->get_payment_method_name();
4910 5257
@@ -4915,10 +5262,12 @@
4915 5262 }
4916 5263 return null;
4917 5264 }
4918 5265
4919 - // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4920 - public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
5266 + // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5267 + // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5268 + // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5269 + public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
4921 5270 $disabled = $disabled ? 'disabled' : '';
4922 5271 $data = array();
4923 5272
4924 5273 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
@@ -4933,9 +5282,8 @@
4933 5282 if ($sku) $data['product_sku'] = $sku;
4934 5283 if ($product_id) $data['product_id'] = $product_id;
4935 5284 if ($variation_id) $data['variation_id'] = $variation_id;
4936 5285
4937 -
4938 5286 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4939 5287 foreach($data as $key=>$value) {
4940 5288 $value = esc_attr($value);
4941 5289 $buttoncode .= " data-$key='$value' ";
@@ -4942,14 +5290,18 @@
4942 5290 }
4943 5291
4944 5292 $payment_method = $this->get_payment_method_name();
4945 5293 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4946 - $short = str_ends_with($logo_variant, 'mini');
4947 - $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4948 5294
4949 - $message =" <img border=0 src='$logo' alt='$payment_method'/>";
5295 + if (is_array($button_args_override) && $button_args_override) {
5296 + $button_args = $button_args_override;
5297 + } else {
5298 + $button_args = $this->get_html_button_attrs_for_context($context);
5299 + }
5300 + $short = ($button_args['compact'] ?? 'false') === 'true';
5301 + $button = $this->get_html_button($button_args);
4950 5302
4951 -# Extra classes, if passed IOK 2019-02-26
5303 + # Extra classes, if passed IOK 2019-02-26
4952 5304 if (is_array($classes)) {
4953 5305 $classes = join(" ", $classes);
4954 5306 }
4955 5307 if ($classes) $classes = " $classes";
@@ -4954,19 +5306,15 @@
4954 5306 }
4955 5307 if ($classes) $classes = " $classes";
4956 5308 if ($short) $classes = "short $classes";
4957 5309
4958 - $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
5310 + $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5311 +
5312 +
5313 +
4959 5314 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4960 5315 }
4961 5316
4962 - // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4963 - public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4964 - $logo_lang = $this->get_customer_language();
4965 - $logo_variant = $this->get_express_logo_page_variant($page);
4966 - return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4967 - }
4968 -
4969 5317 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4970 5318 public function single_product_buy_now_button () {
4971 5319 $gw = $this->gateway();
4972 5320 $how = $gw->get_option('singleproductexpress');
@@ -5046,51 +5394,90 @@
5046 5394 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5047 5395 }
5048 5396
5049 5397
5050 -
5051 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5398 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5052 5399 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5400 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5053 5401 public function woocommerce_create_pages ($data) {
5402 + // Vipps Checkout page
5054 5403 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5055 - if (!$vipps_checkout_activated) return $data;
5404 + if ($vipps_checkout_activated) {
5405 + $data['vipps_checkout'] = array(
5406 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5407 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5408 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5409 + );
5410 + }
5056 5411
5057 - $data['vipps_checkout'] = array(
5058 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5059 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5060 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5061 - );
5062 -
5412 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5413 + $data['vipps_special_page'] = [
5414 + 'name' => 'vipps-payment', // slug
5415 + /* translators: company name */
5416 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5417 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5418 + ];
5063 5419 return $data;
5064 5420 }
5065 5421
5066 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5422 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5423 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5067 5424 public function maybe_create_vipps_pages () {
5425 + $make_pages = false;
5426 +
5427 + // Vipps Checkout page. LP 2026-08-18
5068 5428 $checkoutid = wc_get_page_id('vipps_checkout');
5069 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5070 - if ($makeit) {
5429 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5071 5430 delete_option('woocommerce_vipps_checkout_page_id');
5431 + $make_pages = true;
5072 5432 }
5073 5433
5074 - if ($makeit) {
5075 - WC_Install::create_pages();
5434 + // vipps special page, previously a fake page. LP 2026-08-18
5435 + $builtin_special_page_id = static::get_special_page_id();
5436 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5437 + delete_option('woocommerce_vipps_special_page_page_id');
5438 + $make_pages = true;
5076 5439 }
5440 +
5441 + if ($make_pages) {
5442 + WC_Install::create_pages();
5443 + }
5077 5444 }
5078 5445
5446 + public function vipps_special_page_shortcode($atts, $content) {
5447 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5448 + if (is_admin()) return;
5449 + if (wp_doing_ajax()) return;
5450 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5451 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5079 5452
5453 + $action = $_GET['action'] ?? '';
5454 + $html = "";
5455 + switch ($action) {
5456 + case 'wait_for_payment':
5457 + $html = $this->vipps_wait_for_payment();
5458 + break;
5459 + case 'do_express_checkout':
5460 + $html = $this->vipps_express_checkout();
5461 + break;
5462 + case 'buy_product':
5463 + $html = $this->vipps_buy_product();
5464 + break;
5465 + default:
5466 + $html = '';
5467 + }
5468 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5469 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5470 +
5471 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5472 + return $html;
5473 + }
5474 +
5475 +
5080 5476 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5081 5477 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5082 5478 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5083 5479 public function vipps_buy_product() {
5084 - status_header(200,'OK');
5085 - Vipps::nocache();
5086 -
5087 - add_filter('body_class', function ($classes) {
5088 - $classes[] = 'vipps-express-checkout';
5089 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5090 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5091 - });
5092 -
5093 5480 do_action('woo_vipps_express_checkout_page');
5094 5481
5095 5482 $session = WC()->session;
5096 5483 $posted = $session->get('__vipps_buy_product');
@@ -5119,39 +5506,38 @@
5119 5506
5120 5507 if (!$productinfo) {
5121 5508 $title = __("Product is no longer available",'woo-vipps');
5122 5509 $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');
5123 - return $this->fakepage($title,$content);
5510 + return $this->special_page_html($title,$content);
5124 5511 }
5125 5512
5126 5513 // Pass the productinfo to the express checkout form
5127 5514 $args = array();
5128 - $args['quantity'] = 1;
5129 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5130 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5131 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5132 - if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']);
5515 + $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5516 + $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5517 + $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5518 + $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5133 5519
5134 - // For variable products where some of the attributes are "any", we need to add these as well. This is from woos form-handler for these.
5135 - foreach ($productinfo as $key => $value) {
5136 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5137 - continue;
5138 - }
5139 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5140 - }
5520 + $payment_method = $this->get_payment_method_name();
5521 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5522 + $bclass = esc_attr($payment_method);
5141 5523
5142 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5524 + $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5525 + $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5526 + $content .= "<a href='javascript:void(0)' class='single-product button vipps-buy-now $bclass' data-vipps-autostart='true' data-vipps-purchase='single' data-product_id='{$args['product_id']}' data-variation_id='{$args['variation_id']}' data-product_sku='{$args['sku']}' data-quantity='{$args['quantity']}' title='{$btitle}';
5527 + >";
5528 + $content .= $this->get_html_button_for_context('global');
5529 + $content .= "</a>";
5530 + $content .= "</div>";
5531 +
5532 + return $content;
5143 5533 }
5144 5534
5145 - // 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.
5146 - public function vipps_express_checkout() {
5147 - status_header(200,'OK');
5148 - Vipps::nocache();
5535 + public function vipps_express_checkout_consistency_check() {
5149 5536 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5150 5537 // IOK 2018-05-28
5151 5538 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5152 5539
5153 -
5154 5540 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5155 5541 if (!$backurl) $backurl = home_url();
5156 5542
5157 5543 if (!$ok) {
@@ -5165,218 +5551,39 @@
5165 5551 wp_redirect($backurl);
5166 5552 exit();
5167 5553 }
5168 5554
5169 - add_filter('body_class', function ($classes) {
5170 - $classes[] = 'vipps-express-checkout';
5171 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5172 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5173 - });
5174 -
5175 - do_action('woo_vipps_express_checkout_page');
5176 -
5177 - $this->print_express_checkout_page(true, 'do_express_checkout');
5555 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5178 5556 }
5179 5557
5180 - // This method tries to ensure that a customer does not 'lose' the return page and
5181 - // starts ordering the same products twice. IOK 2020-01-22
5182 - protected function validate_express_checkout_orderspec ($orderspec) {
5183 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5184 -
5185 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5186 - $orderset = array();
5187 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5188 -
5189 - // Then get open orders
5190 - $sessionorders = array();
5191 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5192 - if ($sessionorderdata) {
5193 - foreach(array_keys($sessionorderdata) as $oid) {
5194 - $orderobject = wc_get_order($oid);
5195 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5196 - if ($orderobject instanceof WC_Order) {
5197 - $sessionorders[] = $orderobject;
5198 - }
5199 - }
5558 + // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5559 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5560 + public function vipps_express_checkout() {
5561 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5562 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5563 + $content = __('Link expired, please try again', 'woo-vipps');
5564 + return $content;
5200 5565 }
5201 - // Nothing more to do here
5202 - if (empty($sessionorders)) return true;
5566 +
5567 + do_action('woo_vipps_express_checkout_page');
5203 5568
5204 - // And create a similar hash table for each of the open orders
5205 - $openorderdata = array();
5206 - foreach ($sessionorders as $open_order) {
5207 - $status = $open_order->get_status();
5208 - if ($status == 'cancelled' || $status == 'pending') continue;
5209 - $when = strtotime($open_order->get_date_modified());
5210 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5211 - if (time() > $cutoff) {
5212 - continue;
5213 - }
5214 - $orderdata = array();
5215 - foreach($open_order->get_items() as $item) {
5216 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5217 - $orderdata[] = $productspec;
5218 - }
5219 - $openorderdata[]=$orderdata;
5220 - }
5569 + $payment_method = $this->get_payment_method_name();
5570 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5571 + $bclass = esc_attr($payment_method);
5572 + $sec = esc_attr($_REQUEST['sec']);
5573 + $content = "";
5574 + $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5575 + $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5576 + $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5577 + $content .= $this->get_html_button_for_context('global');
5578 + $content .="</a>";
5579 + $content .="</div>";
5221 5580
5222 - // Now: For each entry in the orderhash, check if there is an order that has a) all of them and b) not any more of them.
5223 - foreach($openorderdata as $prevorder) {
5224 - $a = array_diff($prevorder, $orderset);
5225 - $b = array_diff($orderset, $prevorder);
5226 - if (empty($a) && empty($b)) {
5227 - $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
5228 - return false;
5229 - }
5230 - }
5231 - // Else, order is good.
5232 - return true;
5581 + return $content;
5233 5582 }
5234 5583
5235 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5236 - // Return value is like in a cart.
5237 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5238 - protected function get_orderspec_from_arguments ($productinfo) {
5239 - if (!$productinfo) return array();
5240 - $variantid = 0;
5241 - $productid = 0;
5242 - $quantity = intval(@$productinfo['quantity']);
5243 - if (!$quantity) $quantity = 1;
5244 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5245 - $sku = $productinfo['sku'];
5246 - $skuid = wc_get_product_id_by_sku($sku);
5247 - $product = wc_get_product($skuid);
5248 - $parentid = $product ? $product->get_parent_id() : null;
5249 - if ($product) {
5250 - if ($parentid) {
5251 - $variantid = $skuid; $productid = $parentid;
5252 - } else {
5253 - $productid = $skuid;
5254 - }
5255 - }
5256 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5257 - $productid = intval($productinfo['product_id']);
5258 - $variantid = intval(@$productinfo['variation_id']);
5259 - }
5260 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5261 - return array();
5262 - }
5263 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5264 - protected function get_orderspec_from_cart () {
5265 - $cartitems = WC()->cart->get_cart();
5266 - $orderspec = array();
5267 - foreach($cartitems as $item => $values) {
5268 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5269 - }
5270 - return $orderspec;
5271 - }
5272 -
5273 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5274 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5275 - $gw = $this->gateway();
5276 -
5277 - $expressCheckoutMessages = array();
5278 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5279 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5280 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5281 -
5282 - wp_register_script('vipps-express-checkout',plugins_url('js/express-checkout.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/express-checkout.js"), 'true');
5283 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5284 - wp_enqueue_script('vipps-express-checkout');
5285 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5286 - // to actually perform the express checkout.
5287 - $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5288 -
5289 -
5290 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5291 - if (empty($orderspec)) {
5292 - $orderspec = $this->get_orderspec_from_cart();
5293 - }
5294 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5295 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5296 -
5297 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5298 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5299 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5300 -
5301 - $askForConfirmationHTML = '';
5302 - if (!$orderisOK) {
5303 - $header = __("Are you sure?",'woo-vipps');
5304 - $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
5305 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5306 - }
5307 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5308 - $execute = $execute && $orderisOK && !$askForTerms;
5309 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5310 -
5311 - $content = $this->spinner();
5312 -
5313 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5314 - // The form data below is sent on order creation; the sec is also used to poll session status
5315 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5316 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5317 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5318 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5319 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5320 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5321 - ob_start();
5322 - do_action( 'woocommerce_after_order_notes');
5323 - $content .= ob_get_clean();
5324 - }
5325 - $content .= wp_nonce_field('do_express','sec',1,false);
5326 -
5327 - $termsHTML = '';
5328 - if ($askForTerms) {
5329 - // Include shop terms
5330 - ob_start();
5331 - wc_get_template('checkout/terms.php');
5332 - $termsHTML = ob_get_clean();
5333 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5334 - }
5335 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5336 -
5337 - if ($productinfo) {
5338 - foreach($productinfo as $key=>$value) {
5339 - $k = esc_attr($key);
5340 - $v = esc_attr($value);
5341 - $content .= "<input type='hidden' name='$k' value='$v' />";
5342 - }
5343 - }
5344 - ob_start();
5345 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5346 - $content .= ob_get_clean();
5347 - $content .= "</form>";
5348 -
5349 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5350 - $pressTheButtonHTML = "";
5351 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5352 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5353 - }
5354 -
5355 - if ($execute) {
5356 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5357 - $content .= "<div id='vipps-status-message'></div>";
5358 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5359 - return;
5360 - } else {
5361 - $content .= $askForConfirmationHTML;
5362 - $content .= $extraHTML;
5363 - $content .= $termsHTML;
5364 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5365 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5366 - $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>";
5367 - $content .= "<div id='vipps-status-message'></div>";
5368 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5369 - return;
5370 - }
5371 - }
5372 -
5373 -
5374 -
5375 - public function vipps_wait_for_payment() {
5376 - status_header(200,'OK');
5377 - Vipps::nocache();
5378 -
5584 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5585 + private function handle_payment_poll_and_redirect () {
5379 5586 $orderid = WC()->session->get('_vipps_pending_order');
5380 5587
5381 5588 $order = null;
5382 5589 $gw = $this->gateway();
@@ -5390,9 +5597,9 @@
5390 5597 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5391 5598 // simulating the session with that.
5392 5599 // IOK 2019-11-19, changed to using GET 2023-01-23
5393 5600 if ($no_session && $limited_session) {
5394 - $orderid = intval(@$_GET['id']);
5601 + $orderid = intval($_GET['id'] ?? false);
5395 5602 }
5396 5603 if ($orderid) {
5397 5604 clean_post_cache($orderid);
5398 5605 $order = wc_get_order($orderid);
@@ -5407,20 +5614,22 @@
5407 5614 $session = WC()->session;
5408 5615 if (!$session->has_session()) {
5409 5616 $session->set_customer_session_cookie(true);
5410 5617 }
5618 +
5619 + $sessionorders= WC()->session->get('_vipps_session_orders');
5620 + $sessionorders[$orderid] = 1;
5621 + WC()->session->set('_vipps_session_orders',$sessionorders);
5411 5622 $session->set('_vipps_pending_order', $orderid);
5623 + WC()->session->save_data();
5412 5624 }
5413 5625 }
5414 5626
5415 -
5416 - do_action('woo_vipps_wait_for_payment_page',$order);
5417 -
5418 5627 $deleted_order=0;
5419 5628 if ($orderid && !$order) {
5420 5629 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5421 5630 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5422 - $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5631 + $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5423 5632 $deleted_order=1;
5424 5633 }
5425 5634
5426 5635 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5430,12 +5639,13 @@
5430 5639
5431 5640 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5432 5641 $do_poll = true;
5433 5642
5434 - // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5643 + // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5435 5644 if ($do_poll && $status == 'pending') {
5436 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5437 - $newstatus = $gw->callback_check_order_status($order);
5645 + // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5646 + $newstatus = $gw->poll_and_check_order_status($order);
5647 + $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5438 5648 if ($status != $newstatus) {
5439 5649 $status = $newstatus;
5440 5650 clean_post_cache($orderid);
5441 5651 $order = wc_get_order($orderid); // Reload order object
@@ -5440,11 +5650,13 @@
5440 5650 clean_post_cache($orderid);
5441 5651 $order = wc_get_order($orderid); // Reload order object
5442 5652 }
5443 5653 } else {
5444 - // No need to do anyting here. IOK 2020-01-26
5654 + // No need to do anyting here. IOK 2020-01-26
5445 5655 }
5446 5656
5657 + // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5658 + // the payment status at Vipps without polls, which will tell us if the payment succeeded in case people use custom order statuses and so on. IOK 2026-09-29
5447 5659 $payment = 'notchecked';
5448 5660 if ($do_poll) {
5449 5661 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5450 5662 }
@@ -5458,9 +5670,8 @@
5458 5670 exit();
5459 5671 }
5460 5672
5461 5673 // We are done, but in failure. Don't poll.
5462 - $content = "";
5463 5674 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5464 5675
5465 5676 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5466 5677 // 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
@@ -5468,8 +5679,9 @@
5468 5679 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5469 5680 wp_redirect($failure_redirect);
5470 5681 exit();
5471 5682 }
5683 +
5472 5684 if ($status == 'cancelled' || $payment == 'cancelled') {
5473 5685 $this->maybe_restore_cart($orderid,'failed');
5474 5686 if ($failure_redirect){
5475 5687 wp_redirect($failure_redirect);
@@ -5474,27 +5686,46 @@
5474 5686 if ($failure_redirect){
5475 5687 wp_redirect($failure_redirect);
5476 5688 exit();
5477 5689 }
5690 + } else {
5691 + // If not, enqueue the status checker IOK 2026-09-21
5692 + 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');
5693 + }
5694 +
5695 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5696 +
5697 + // Communicate this to the shortcode IOK 2026-09-21
5698 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5699 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5700 + });
5701 +
5702 + }
5703 +
5704 + public function vipps_wait_for_payment() {
5705 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5706 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5707 +
5708 + $orderid = $data['orderid'] ?? 0;
5709 + $status = $data['status'] ?? "";
5710 + $payment = $data['payment'] ?? "";
5711 +
5712 + $order = wc_get_order($orderid);
5713 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5714 +
5715 + do_action('woo_vipps_wait_for_payment_page',$order);
5716 + $gw = $this->gateway();
5717 +
5718 + $content = "";
5719 + if ($status == 'cancelled' || $payment == 'cancelled') {
5478 5720 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5479 5721 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5480 5722 $content .= "</div>";
5481 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5482 -
5483 - return;
5723 + return $this->special_page_html('', $content);
5484 5724 }
5485 5725
5486 5726 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5487 -
5488 5727 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5489 - 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');
5490 -
5491 - // 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
5492 - // and check that the order matches (and is 'pending') (and exists)
5493 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5494 - $vippsstatus = $order->get_meta('_vipps_status');
5495 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5496 -
5497 5728 $signal = $this->callbackSignal($order);
5498 5729 $content = "";
5499 5730 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5500 5731
@@ -5502,18 +5733,18 @@
5502 5733 $signalurl = $this->callbackSignalURL($signal);
5503 5734
5504 5735 $content .= "</p></div>";
5505 5736
5506 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5507 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5508 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5737 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5738 +
5739 + // Carry the order status to the checking script IOK 2026-09-21
5740 + $content .= "<form id='vippsdata'>";
5509 5741 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5510 5742 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5511 5743 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5512 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5744 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5513 5745 $content .= "</form>";
5514 5746
5515 -
5516 5747 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5517 5748 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5518 5749 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5519 5750 $content .= "</div>";
@@ -5527,94 +5758,22 @@
5527 5758 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5528 5759 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5529 5760 $content .= "</div>";
5530 5761
5762 + return $this->special_page_html('', $content);
5763 + }
5531 5764
5532 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5765 + // Returns formatted html for the vipps special page. LP 2026-08-27
5766 + public function special_page_html($header, $content) {
5767 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5768 + $html = <<<EOF
5769 + $header_html
5770 + <div class="vipps-special-page-content">$content</div>
5771 + EOF;
5772 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5533 5773 }
5534 5774
5535 5775
5536 -
5537 - public function fakepage($title,$content) {
5538 - global $wp, $wp_query;
5539 - // We don't want this here.
5540 - remove_filter ('the_content', 'wpautop');
5541 -
5542 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5543 - $wp_post = null;
5544 - if ($specialid) {
5545 - $wp_post = get_post($specialid);
5546 - if ($wp_post) {
5547 - $wp_post->post_title = $title;
5548 - $wp_post->post_content = $content;
5549 - // Normalize a bit
5550 - $wp_post->filter = 'raw'; // important
5551 - $wp_post->post_status = 'publish';
5552 - $wp_post->comment_status= 'closed';
5553 - $wp_post->ping_status= 'closed';
5554 - } else {
5555 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5556 - }
5557 - }
5558 - if (!$wp_post || is_wp_error($wp_post)) {
5559 - $post = new stdClass();
5560 - $post->ID = -99;
5561 - $post->post_author = 1;
5562 - $post->post_date = current_time( 'mysql' );
5563 - $post->post_date_gmt = current_time( 'mysql', 1 );
5564 - $post->post_title = $title;
5565 - $post->post_content = $content;
5566 - $post->post_status = 'publish';
5567 - $post->comment_status = 'closed';
5568 - $post->ping_status = 'closed';
5569 - $post->post_name = 'vippsconfirm-fake-page-name';
5570 - $post->post_type = 'page';
5571 - $post->filter = 'raw'; // important
5572 - $wp_post = new WP_Post($post);
5573 - wp_cache_add( -99, $wp_post, 'posts' );
5574 - }
5575 -
5576 - // Update the main query
5577 - $wp_query->post = $wp_post;
5578 - $wp_query->posts = array( $wp_post );
5579 - $wp_query->queried_object = $wp_post;
5580 - $wp_query->queried_object_id = $wp_post->ID;
5581 - $wp_query->found_posts = 1;
5582 - $wp_query->post_count = 1;
5583 - $wp_query->max_num_pages = 1;
5584 - $wp_query->is_page = true;
5585 - $wp_query->is_singular = true;
5586 - $wp_query->is_single = false;
5587 - $wp_query->is_attachment = false;
5588 - $wp_query->is_archive = false;
5589 - $wp_query->is_category = false;
5590 - $wp_query->is_tag = false;
5591 - $wp_query->is_tax = false;
5592 - $wp_query->is_author = false;
5593 - $wp_query->is_date = false;
5594 - $wp_query->is_year = false;
5595 - $wp_query->is_month = false;
5596 - $wp_query->is_day = false;
5597 - $wp_query->is_time = false;
5598 - $wp_query->is_search = false;
5599 - $wp_query->is_feed = false;
5600 - $wp_query->is_comment_feed = false;
5601 - $wp_query->is_trackback = false;
5602 - $wp_query->is_home = false;
5603 - $wp_query->is_embed = false;
5604 - $wp_query->is_404 = false;
5605 - $wp_query->is_paged = false;
5606 - $wp_query->is_admin = false;
5607 - $wp_query->is_preview = false;
5608 - $wp_query->is_robots = false;
5609 - $wp_query->is_posts_page = false;
5610 - $wp_query->is_post_type_archive = false;
5611 - // Update globals
5612 - $GLOBALS['wp_query'] = $wp_query;
5613 - $wp->register_globals();
5614 - return $wp_post;
5615 - }
5616 -
5617 5776 // Support the interactivity API with data about our cart IOK 2026-02-23
5618 5777 public function woo_vipps_store_api_cart_data() {
5619 5778 // 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
5620 5779
@@ -5620,13 +5779,15 @@
5620 5779
5621 5780 $checkout_page = $this->gateway()->vipps_checkout_available();
5622 5781 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5623 5782 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5783 +
5624 5784 $cart_data = array(
5625 5785 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5626 5786 'cart_supports_checkout' => (bool) $checkout_page,
5627 5787 'checkout_url' => $checkout_url,
5628 5788 );
5789 +
5629 5790 return $cart_data;
5630 5791 }
5631 5792
5632 5793 public function woo_vipps_store_api_cart_schema() {
@@ -5648,8 +5809,117 @@
5648 5809 ),
5649 5810 );
5650 5811 }
5651 5812
5813 + // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5814 + // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5815 + private function init_button_options() {
5816 + /* New structure as of now
5817 + * [
5818 + * 'version' => x.x,
5819 + * 'express' => [
5820 + * 'version' => x.x, used to migrate from previous iterations
5821 + * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5822 + * 'global' => ['compact' => ..., 'verb' => ..., ...],
5823 + * 'cart' => [...],
5824 + * 'product' => [...],
5825 + * 'checkout' => [...],
5826 + * ...
5827 + * ],
5828 + * 'product_configs' => [ overrides for specific products
5829 + * 1532 => ['compact' => ..., 'verb' => ..., ...],
5830 + * ...
5831 + * ],
5832 + * ],
5833 + * ]
5834 + */
5835 + $options = get_option('vipps_button_options2');
5836 + if (!empty($options)) return;
5837 +
5838 + $old_options = get_option('vipps_button_options');
5839 + $default_config = $this->get_html_button_default_attrs();
5840 + unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5841 + $default_compact = array_replace($default_config, ['compact' => 'true']);
5842 +
5843 + $default_options = [
5844 + 'version' => $this->button_options_version,
5845 + 'express' => [
5846 + 'version' => $this->button_options_express_version,
5847 + 'configs' => [
5848 + 'global' => $default_config,
5849 + // Need compact version by default for below pages. LP 2026-07-07
5850 + 'catalog' => $default_compact,
5851 + 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5852 + ],
5853 + 'product_configs' => [],
5854 + ],
5855 + ];
5856 +
5857 + $new_options = $default_options;
5858 +
5859 + //Actually, we have some options from the old structure IOK 2026-07-15
5860 + if (!empty($old_options)) {
5861 + $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5862 + unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5863 + }
5864 +
5865 + // Migrate context/page mini override to new context config. LP 2026-06-26
5866 + if (is_array($old_options['express']['force-mini'] ?? null)) {
5867 + foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5868 + if ("yes" === $use_mini) {
5869 + $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5870 + unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5871 + $config['compact'] = 'true';
5872 + $new_options['express']['configs'][$context] = $config;
5873 + }
5874 + }
5875 + }
5876 +
5877 + if ($this->get_payment_method_name() !== 'MobilePay') {
5878 + // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5879 + foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5880 + if ('fi' === ($config['language'] ?? '')) {
5881 + $config['language'] = 'store';
5882 + $new_options['express']['configs'][$context] = $config;
5883 + }
5884 + }
5885 + }
5886 +
5887 + /* translators: placeholders are arrays */
5888 + $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5889 +
5890 +
5891 +
5892 + update_option('vipps_button_options2', $new_options);
5893 + }
5894 +
5895 + // Old variant string => new config array. LP 2026-06-26
5896 + public function migrate_button_variant_to_config($variant_slug) {
5897 + if (!is_string($variant_slug)) return [];
5898 + $config = $this->get_html_button_default_attrs();
5899 + $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5900 + $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5901 + if (str_contains($variant_slug, 'buy-now')) {
5902 + $config['verb'] = 'buy';
5903 + } else if (str_contains($variant_slug, 'express')) {
5904 + $config['verb'] = 'express';
5905 + }
5906 + return $config;
5907 + }
5908 +
5909 + // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5910 + public function get_express_logo_variants() {
5911 + return [
5912 + 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5913 + 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5914 + 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5915 + 'express-pill' => __('Express pill', 'woo-vipps'),
5916 + 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5917 + 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5918 + ];
5919 + }
5920 +
5921 +
5652 5922 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5653 5923 public static function order_is_vipps_retryable($order_id) {
5654 5924 $order = wc_get_order($order_id);
5655 5925 if (!$order) return false;
@@ -5658,6 +5928,20 @@
5658 5928 $shipping_set = $order->get_meta('_vipps_shipping_set');
5659 5929
5660 5930 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5661 5931 return $nonexpress_epayment || $shipping_set;
5932 + }
5933 +
5934 + /** Returns the plugin's rest api namespace including the version.
5935 + * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
5936 + public static function get_rest_namespace($version = 'latest') {
5937 + $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
5938 + return self::REST_NAMESPACE_BASE . "/$version";
5939 + }
5940 +
5941 + /** Returns the plugin's rest api url.
5942 + * $version accepts 'latest', but you probably don't want to do that.
5943 + * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
5944 + public static function get_rest_url($version, $route) {
5945 + return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5662 5946 }
5663 5947 }