PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.0
Pay with Vipps and MobilePay for WooCommerce v6.3.0
6.2.6 6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 All 189 releases
← All changes | payment/Vipps.class.php +1395 -1039 6.0.4 → 6.3.0 View file →
@@ -36,8 +36,12 @@
36 36 }
37 37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38 38
39 39 class Vipps {
40 + /* Rest api consts. LP 2026-03-30 */
41 + private const REST_NAMESPACE_BASE = 'woo-vipps';
42 + private const REST_CURRENT_VERSION = 'v1'; // don't use this directly, use methods get_rest_namespace() and get_rest_url(). LP 2026-04-22
43 +
40 44 private static $instance = null;
41 45
42 46 /* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */
43 47 public static $installed_gateways = [];
@@ -55,8 +59,11 @@
55 59 private $lockKey = null;
56 60
57 61 public $vippsJSConfig = array();
58 62
63 + public $button_options_version = '2.0';
64 + public $button_options_express_version = '2.0';
65 +
59 66 // IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction
60 67 public static function CompanyName() {
61 68 return __("Vipps MobilePay", 'woo-vipps');
62 69 }
@@ -63,9 +70,9 @@
63 70 public static function CheckoutName($order=null) {
64 71 return "Vipps MobilePay Checkout"; // Do not translate
65 72 }
66 73 public static function ExpressCheckoutName($order=null) {
67 - return __("Vipps Express Checkout", 'woo-vipps');
74 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
68 75 }
69 76 public static function LoginName() {
70 77 return __("Login with Vipps", 'woo-vipps');
71 78 }
@@ -110,16 +117,22 @@
110 117 add_action('wp_footer', array($Vipps,'footer'));
111 118 }
112 119 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
113 120 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
114 - add_action('init',array($Vipps,'init'));
121 + add_action( 'init',array($Vipps,'init'));
122 + add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
115 123 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
116 124 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
117 125 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
126 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
119 127 // not have loaded if the default checkout solution isn't the Checkout block. We'll load it anyway if the user has any local pickup locations
120 128 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
121 129 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
130 +
131 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
132 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
133 + // is important.
134 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
122 135 }
123 136
124 137 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
125 138 public function woocommerce_blocks_loaded() {
@@ -215,8 +228,9 @@
215 228 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
216 229 // needs these to be defined in the backend. IOK 2024-04-16
217 230 add_action('wp_loaded', array($this, 'wp_register_scripts'));
218 231 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
232 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
219 233
220 234 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
221 235 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
222 236
@@ -232,16 +246,8 @@
232 246
233 247 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
234 248 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
235 249
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 250
245 251 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
246 252 // action scheduler would be better, but we can't do that just yet because of backwards
247 253 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -271,10 +277,65 @@
271 277 add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
272 278 add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
273 279 }
274 280
281 + // Set default button options, migrating any older setup IOK 2026-07-15
282 + $this->init_button_options();
283 +
284 + /*
285 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
286 + where we earlier used just a fake page with no real page id, unless especially configured.
287 + We therefore need to add code to maintain this special page.
288 +
289 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
290 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
291 + */
292 +
293 + // Delete special page id option when its deleted or trashed, so that we dont have to load
294 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
295 + $delete_special_page_id = function($post_id, $post = null) {
296 + if (static::get_special_page_id() === $post_id) {
297 + delete_option('woocommerce_vipps_special_page_page_id');
298 + }
299 + };
300 + add_action('delete_post', $delete_special_page_id, 10, 2);
301 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
302 +
303 + $this->ensure_special_page_exists();
304 +
305 +
306 + // We want this special page to have a certain title and maybe special scripts and so on,
307 + // this gets run in template redirect for these pages.
308 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
309 +
310 + // Add an admin interface for this page as well IOK 2026-09-11
311 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
275 312 }
276 313
314 +
315 + public function rest_api_init () {
316 +
317 + // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
318 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
319 + 'methods' => 'GET',
320 + 'callback' => [$this, 'rest_express_checkout_products'],
321 + 'permission_callback' => '__return_true',
322 + ]);
323 +
324 + // Start a single product express checkout process. IOK 2026-08-25
325 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
326 + 'methods' => 'POST',
327 + 'callback' => [$this, 'rest_do_single_product_express_checkout'],
328 + 'permission_callback' => '__return_true',
329 + ]);
330 + // And one for the cart. IOK 2026-09-04
331 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
332 + 'methods' => 'POST',
333 + 'callback' => [$this, 'rest_do_express_checkout'],
334 + 'permission_callback' => '__return_true',
335 + ]);
336 + }
337 +
277 338 public function admin_init () {
278 339 $gw = $this->gateway();
279 340 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
280 341 $adminSettings = VippsAdminSettings::instance();
@@ -301,10 +362,8 @@
301 362 // Styling etc
302 363 add_action('admin_head', array($this, 'admin_head'));
303 364
304 365 // Scripts
305 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
306 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
307 366 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
308 367
309 368 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
310 369 add_action('current_screen', function ($screen) {
@@ -392,9 +451,77 @@
392 451 }
393 452 }
394 453 }
395 454
455 +
456 + /** Ensure we have a special page for payment flows
457 + *
458 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
459 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
460 + **/
461 + public function ensure_special_page_exists() {
462 + if (static::get_special_page_id()) return;
463 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
396 464
465 + // If user had in previous version overriden the fake page with a real one: migrate this page to be the special page. LP 2026-09-01
466 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
467 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
468 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
469 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
470 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
471 +
472 + // Ensure this page has the necessary shortcode. LP 2026-09-01
473 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
474 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
475 + wp_update_post([
476 + 'ID' => $old_special_page_id,
477 + 'post_content' => $new_content,
478 + ]);
479 + }
480 + } else {
481 + // Create special page if its missing. LP 2026-09-01
482 + $this->maybe_create_vipps_pages();
483 + }
484 + }
485 +
486 + // Admin interface for the special page on woo/advanced/pages
487 + public function woocommerce_settings_pages ($settings) {
488 + $i = -1;
489 + foreach($settings as $entry) {
490 + $i++;
491 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
492 + break;
493 + }
494 + }
495 + if ($i > 0) {
496 + $vippspagesettings = array(
497 + array(
498 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
499 + 'desc' => sprintf(__('This page is used for various special pages used by %1$s', 'woo-vipps'), Vipps::CompanyName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_special_page') ,
500 + 'id' => 'woocommerce_vipps_special_page_page_id',
501 + 'type' => 'single_select_page_with_search',
502 + 'default' => '',
503 + 'class' => 'wc-page-search',
504 + 'css' => 'min-width:300px;',
505 + 'args' => array(
506 + 'exclude' =>
507 + array(
508 + wc_get_page_id( 'myaccount' ),
509 + wc_get_page_id( 'checkout' ),
510 + wc_get_page_id( 'cart' ),
511 + ),
512 + ),
513 + 'desc_tip' => true,
514 + 'autoload' => false,
515 + ));
516 + array_splice($settings, $i, 0, $vippspagesettings);
517 + }
518 +
519 + return $settings;
520 + }
521 +
522 +
523 +
397 524 // Runs on init, adds the Vipps badge feature if activated
398 525 public function maybe_add_vipps_badge_feature () {
399 526 $badge_options = get_option('vipps_badge_options');
400 527 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -723,8 +850,11 @@
723 850
724 851 // Get current brand and language
725 852 $current_brand = strtolower($this->get_payment_method_name());
726 853 $current_language = $this->get_customer_language();
854 + if ('se' === $current_language) $current_language = 'sv';
855 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
856 + if ('dk' === $current_language) $current_language = 'da';
727 857
728 858 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
729 859 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
730 860 'purple'=> __('Purple', 'woo-vipps')];
@@ -787,9 +917,9 @@
787 917
788 918 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
789 919 <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 920 <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>
921 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
792 922 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
793 923 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
794 924 </p>
795 925
@@ -816,21 +946,24 @@
816 946 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
817 947 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
818 948 }
819 949
820 - $options = get_option('vipps_button_options');
821 - if (isset($_POST['express']['variant'])) {
822 - $options['express']['variant'] = sanitize_title($_POST['express']['variant']);
950 + $old = get_option('vipps_button_options2', []);
951 + $new = $old;
952 + if (isset($_POST['express']['configs'])) {
953 + foreach ($_POST['express']['configs'] as $ctx => $config) {
954 + $sanitized_ctx = sanitize_title($ctx);
955 + $sanitized_config = map_deep($config, 'sanitize_title');
956 +
957 + // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
958 + if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
959 + $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
960 + } else {
961 + $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
962 + }
963 + }
823 964 }
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);
965 + update_option('vipps_button_options2', $new);
833 966 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
834 967 exit();
835 968 }
836 969
@@ -864,9 +997,12 @@
864 997 public function vipps_mobilepay_badge_shortcode($atts) {
865 998 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
866 999
867 1000 $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();
1001 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1002 + if ('se' === $language) $language = 'sv';
1003 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1004 + if ('dk' === $language) $language = 'da';
869 1005 $id = sanitize_title($args['id']);
870 1006 $class = sanitize_text_field($args['class']);
871 1007
872 1008 $attributes = [];
@@ -882,13 +1018,18 @@
882 1018 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
883 1019 }
884 1020
885 1021 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1022 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
886 1023 public function vipps_badge_shortcode($atts) {
887 1024 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
888 1025
889 1026 $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();
1027 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1028 + if ('se' === $language) $language = 'sv';
1029 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1030 + if ('dk' === $language) $language = 'da';
1031 +
891 1032 $id = sanitize_title($args['id']);
892 1033 $class = sanitize_text_field($args['class']);
893 1034
894 1035 $attributes = [];
@@ -902,163 +1043,339 @@
902 1043
903 1044 return "<vipps-badge $badgeatts></vipps-badge>";
904 1045 }
905 1046
906 - public function get_express_logo_variants() {
1047 + public function get_html_button_default_attrs() {
907 1048 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'),
1049 + 'language' => 'store',
1050 + 'variant' => 'primary',
1051 + 'rounded' => 'false',
1052 + 'verb' => 'buy',
1053 + 'stretched' => 'false',
1054 + 'compact' => 'false',
1055 + '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 1056 ];
915 1057 }
916 1058
1059 + public function get_html_button_attrs_for_context($context = 'global') {
1060 + $options = get_option('vipps_button_options2', []);
1061 + if (!is_string($context)) $context = 'global';
1062 +
1063 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1064 + $gutenberg = false;
1065 + if ($context == 'checkout_gutenberg') {
1066 + $context = 'checkout';
1067 + $gutenberg = true;
1068 + }
1069 + if ($context == 'cart_gutenberg') {
1070 + $context = 'cart';
1071 + $gutenberg = true;
1072 + }
917 1073
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'));
1074 + $config = $options['express']['configs'][$context] ?? [];
1075 + $use_global = !$config || ($config['use-global-config'] ?? false);
1076 + if ($use_global) {
1077 + $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
921 1078 }
1079 +
1080 + // see above.
1081 + if ($gutenberg) {
1082 + $config['stretched']='true';
1083 + }
1084 + return $config;
1085 + }
1086 +
1087 + public function get_html_button_for_context($context = 'global') {
1088 + return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1089 + }
1090 +
1091 + // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1092 + // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1093 + public function get_html_button($attrs = []) {
922 1094 $payment_method = $this->get_payment_method_name();
923 - $lang = $this->get_customer_language();
924 - $button_options = get_option('vipps_button_options');
1095 + $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1096 + $attrs['brand'] = strtolower($payment_method);
1097 + $attrs['type'] = 'button'; // static
925 1098
926 - $variants = $this->get_express_logo_variants();
927 - $mini_variants = array_filter($variants, fn($key) => str_ends_with($key, 'mini'), ARRAY_FILTER_USE_KEY);
1099 + // Support using store language
1100 + if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1101 + // Don't support these login verbs. LP 2026-06-04
1102 + if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1103 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1104 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
928 1105
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 - ];
1106 + $escaped_attrs = [];
1107 + foreach($attrs as $k => $v) {
1108 + $escaped_attrs[$k] = esc_attr($v);
1109 + }
941 1110
1111 + // id attribute
1112 + $id = $escaped_attrs['id'] ?? '';
1113 + $id_str = $id ? "id='$id'" : '';
1114 +
1115 + // class attribute
1116 + $class_str = '';
1117 + if (isset($attrs['class'])) {
1118 + if (is_array($attrs['class'])) {
1119 + $class_str = implode(' ', $attrs['class']);
1120 + } else if (is_string($attrs['class'])) {
1121 + $class_str = $attrs['class'];
1122 + }
1123 + }
1124 +
1125 + // The html
1126 + $html = <<<EOF
1127 +<vipps-mobilepay-button
1128 + $id_str
1129 + $class_str
1130 + type="{$escaped_attrs['type']}"
1131 + brand="{$escaped_attrs['brand']}"
1132 + language="{$escaped_attrs['language']}"
1133 + variant="{$escaped_attrs['variant']}"
1134 + rounded="{$escaped_attrs['rounded']}"
1135 + verb="{$escaped_attrs['verb']}"
1136 + stretched="{$escaped_attrs['stretched']}"
1137 + compact="{$escaped_attrs['compact']}"
1138 +></vipps-mobilepay-button>
1139 +EOF;
1140 + return apply_filters('woo_vipps_html_button', $html, $attrs);
1141 + }
1142 +
1143 + public function button_menu_page() {
1144 + if (!current_user_can('manage_woocommerce')) {
1145 + wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1146 + }
1147 + wp_enqueue_script('vipps-button-webcomponent');
942 1148 ?>
943 1149 <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">
1150 + <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1151 + <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1152 + <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1153 + <input type="hidden" name="action" value="update_vipps_button_settings" />
1154 + <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
947 1155
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'); ?>
1156 + <!-- Express section -->
1157 + <?php $this->button_menu_express_section(); ?>
953 1158
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>
1159 + <!-- submit button -->
1160 + <div id="vipps-button-settings-save">
1161 + <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
966 1162 </div>
1163 + </form>
1164 + </div>
1165 + <?php
1166 + }
967 1167
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>
1168 + private function button_menu_express_section() {
1169 + $options = get_option('vipps_button_options2', []);
1170 + $express = $options['express'] ?? [];
1171 + $configs = $express['configs'] ?? [];
980 1172
981 1173
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>
1174 + $contexts = [
1175 + 'global' => __('Global', 'woo-vipps'),
1176 + 'product' => __('Product', 'woo-vipps'),
1177 + 'catalog' => __('Catalog', 'woo-vipps'),
1178 + 'cart' => __('Cart', 'woo-vipps'),
1179 + 'minicart' => __('Mini cart', 'woo-vipps'),
1180 + 'checkout' => __('Checkout', 'woo-vipps'),
1181 + ];
1182 + $init_context = 'global';
1183 + $init_config = $configs[$init_context] ?? [];
991 1184
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>
1185 + // html button args
1186 + $init_args = $init_config;
1187 + $init_args['id'] = 'vipps-button-express-preview';
997 1188
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>
1189 + ?>
1190 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1191 + <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
1003 1192
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>
1193 + <!-- Context dropdown -->
1194 + <div id="vipps-button-settings-express-context">
1195 + <label>
1196 + <?php _e('Config context', 'woo-vipps'); ?>
1197 + </label>
1198 + <select id="context" onChange='updateContext()'>
1199 + <?php foreach($contexts as $key => $label): ?>
1200 + <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1201 + <?php echo $label ; ?>
1202 + </option>
1203 + <?php endforeach; ?>
1204 + </select>
1205 + <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>
1206 + </div>
1207 +
1009 1208
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>
1209 + <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1210 + On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1211 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1212 + <fieldset>
1213 + <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1214 + <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1215 + <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1216 + </fieldset>
1217 + <fieldset>
1218 + <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1219 + <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1220 + <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1221 + <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1222 + <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1223 + <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1224 + <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1225 + <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1226 + <?php endif; ?>
1227 + </fieldset>
1021 1228
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>
1229 + <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1230 + <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1231 + <?php endif; ?>
1034 1232
1035 - <!-- END EXPRESS SECTION -->
1233 + <fieldset>
1234 + <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1235 + <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1236 + <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1237 + <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1238 + <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1239 + <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1240 + <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1241 + </fieldset>
1242 + <fieldset>
1243 + <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1244 + <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1245 + <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1246 + <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1247 + </fieldset>
1036 1248 </div>
1037 1249
1038 - <!-- Save button -->
1039 - <div id="vipps-button-settings-save">
1040 - <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1041 - </div>
1042 -
1043 - </form>
1250 + <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1251 + <?php echo $this->get_html_button($init_args); ?>
1044 1252 </div>
1045 1253
1046 1254 <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 - }
1255 + // When inputs change, update the preview args. LP 2026-06-24
1256 + jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
1053 1257
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>
1258 + let currentContext = '<?php echo $init_context; ?>';
1259 + let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1260 +
1261 + // Updates the actual html inputs from given config. LP 2026-07-01
1262 + function setInputsFromConfig(context, config) {
1263 + const useGlobalConfig = Boolean(config?.["use-global-config"]);
1264 + const isGlobal = "global" === context;
1265 +
1266 + // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1267 + jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1268 +
1269 + // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1270 + if (!config || (!isGlobal && useGlobalConfig)) {
1271 + config = contextConfigs["global"];
1272 +
1273 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1274 +
1275 + // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1276 + jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1277 + } else {
1278 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1279 + jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1280 + }
1281 +
1282 + Object.entries(config).forEach(([key, val]) => {
1283 + if ("use-global-config" === key) return;
1284 + const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1285 + const type = inputs.prop('type');
1286 + switch (type) {
1287 + case "checkbox":
1288 + inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1289 + break;
1290 + case "radio":
1291 + inputs.filter(`[value="${val}"]`).prop('checked', true);
1292 + break;
1293 + default:
1294 + console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1295 + }
1296 + });
1297 +
1298 + updatePreview();
1299 + }
1300 + // init the starting config from option. LP 2026-06-25
1301 + setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1302 +
1303 + // Update the preview web component's attributes. LP 2026-06-24
1304 + function updatePreview(event) {
1305 + const args = getPreviewArgs();
1306 + // FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1307 + // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1308 + if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1309 + const button = jQuery('#vipps-button-express-preview');
1310 + button.attr(args);
1311 + }
1312 +
1313 + function getPreviewArgs() {
1314 + const args = {};
1315 + jQuery('#vipps-button-settings-express-args input').each(function () {
1316 + // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1317 + const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1318 + const attr = matches.length ? matches[matches.length - 1][1] : null;
1319 + if (!attr) {
1320 + console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1321 + return;
1322 + }
1323 + if (this.type === 'checkbox') {
1324 + args[attr] = this.checked;
1325 + } else if (this.checked) {
1326 + args[attr] = this.value;
1327 + }
1328 + });
1329 +
1330 + return args;
1331 + }
1332 +
1333 + // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1334 + function updateContext() {
1335 + const wasGlobal = "global" === currentContext;
1336 + const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1337 +
1338 + // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1339 + if (wasGlobal || !useGlobalConfig) {
1340 + contextConfigs[currentContext] = getPreviewArgs();
1341 + } else {
1342 + contextConfigs[currentContext] = {'use-global-config': true};
1343 + }
1344 +
1345 + // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1346 + const newContext = jQuery("#context").val();
1347 + const newConfig = contextConfigs[newContext];
1348 +
1349 + setInputsFromConfig(newContext, newConfig);
1350 + currentContext = newContext;
1351 + }
1352 +
1353 + // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1354 + jQuery('#vipps-button-settings-form').on('formdata', e => {
1355 + const formData = e?.originalEvent?.formData;
1356 + if (!formData) return;
1357 +
1358 + // run this to store current context config before posting. LP 2026-06-24
1359 + updateContext();
1360 +
1361 + // now we can delete the current temporary config from post data. LP 2026-06-24
1362 + const keysToDelete = [];
1363 + for (const [key] of formData.entries()) {
1364 + if (key.startsWith("express[tmpConfig][")) {
1365 + keysToDelete.push(key);
1366 + }
1367 + }
1368 + keysToDelete.forEach(key => formData.delete(key));
1369 +
1370 + // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1371 + Object.entries(contextConfigs).forEach(([context, config]) => {
1372 + Object.entries(config).forEach(([key, val]) => {
1373 + formData.append(`express[configs][${context}][${key}]`, val);
1374 + });
1375 + });
1376 + });
1377 + </script>
1061 1378 <?php
1062 1379 }
1063 1380
1064 1381
@@ -1392,12 +1709,14 @@
1392 1709 <?php
1393 1710 }
1394 1711 // Scripts used in the backend
1395 1712 public function admin_enqueue_scripts($hook) {
1713 +
1714 + wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1715 + $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1716 + wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1396 1717 // 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');
1718 + $this->script_add_vippslocale('vipps-admin');
1400 1719 wp_enqueue_script('vipps-admin');
1401 1720
1402 1721 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1403 1722 wp_enqueue_style('vipps-fonts');
@@ -1467,12 +1786,9 @@
1467 1786
1468 1787 public function wp_register_scripts () {
1469 1788 // We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues
1470 1789 // (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');
1790 + wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks', 'wp-api-fetch','vipps-widget-sdk'),filemtime(dirname(__FILE__) . "/js/vipps.js"), true);
1475 1791
1476 1792 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1477 1793 wp_register_script('vipps-onsite-messageing',
1478 1794 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1482,35 +1798,120 @@
1482 1798 'in_footer' => true,
1483 1799 'strategy' => 'async',
1484 1800 ],
1485 1801 );
1802 +
1803 + add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1804 + if ($handle == 'vipps-widget-sdk') {
1805 + $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1806 + return $tag;
1807 + }
1808 + return $tag;
1809 + },10,3);
1810 +
1811 + wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1812 + array('vipps-button-webcomponent'),
1813 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1814 + ['in_footer' => true]
1815 + );
1816 +
1817 + // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1818 + wp_register_script('vipps-button-webcomponent',
1819 + plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1820 + array(),
1821 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1822 + [
1823 + 'in_footer' => false
1824 + ]
1825 + );
1486 1826 }
1487 1827
1488 1828 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1489 - public function script_add_vippslocale () {
1829 + public function script_add_vippslocale ($handle) {
1490 1830 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1831 + $name = $this->get_payment_method_name();
1491 1832 $strings = array(
1492 - 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1493 - 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1494 - 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1833 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1834 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1835 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1836 + 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1837 + 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1838 + 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1839 + 'cancel'=> __("Cancel", 'woo-vipps'),
1840 + 'close'=> __("Close", 'woo-vipps'),
1841 + 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1842 + 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1843 + 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1844 + 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1845 + 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1846 + 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1847 + 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1848 + 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1849 + 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1850 + 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1495 1851 );
1496 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1852 + wp_localize_script($handle, 'VippsLocale', $strings);
1497 1853 }
1498 1854
1499 1855 public function wp_enqueue_scripts() {
1856 + // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1857 + $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1500 1858 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1501 1859 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1502 - $this->script_add_vippslocale();
1860 + $this->script_add_vippslocale('vipps-gw');
1503 1861
1504 1862 wp_enqueue_script('vipps-gw');
1505 1863 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1864 + wp_enqueue_script('vipps-button-webcomponent');
1506 1865 }
1507 1866
1867 + // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1868 + // the pay-for-order screen. IOK 2026-09-15
1869 + public function enqueue_classic_checkout_scripts () {
1870 + if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1871 + return;
1872 + }
1873 + // Order-pay is rendered by the classic form even with a Blocks checkout page.
1874 + // It must bypass the check for the parent checkout page's block content.
1875 + if ( ! is_checkout_pay_page() ) {
1876 + $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1877 + $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1878 + ? $utils::is_checkout_block_default()
1879 + : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1508 1880
1881 + if ( $uses_checkout_block ) {
1882 + return;
1883 + }
1884 + }
1885 +
1886 + // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1887 + $relative_path = 'js/vipps-classic-checkout.js';
1888 + wp_enqueue_script(
1889 + 'vipps-classic-checkout',
1890 + plugins_url( $relative_path, __FILE__ ),
1891 + array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1892 + filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1893 + true
1894 + );
1895 +
1896 + if ( is_checkout_pay_page() ) {
1897 + $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1898 + wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1899 + 'orderId' => $order ? $order->get_id() : 0,
1900 + 'orderKey' => $order ? $order->get_order_key() : '',
1901 + 'billingEmail' => $order ? $order->get_billing_email() : '',
1902 + 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1903 + 'nonce' => wp_create_nonce( 'wc_store_api' ),
1904 + 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1905 + 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1906 + ) ) . ';', 'before' );
1907 + }
1908 + }
1909 +
1910 +
1509 1911 public function add_shortcodes() {
1510 1912 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1511 1913 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1512 - add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode'));
1513 1914
1514 1915 // Badges, if using shortcodes
1515 1916 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1516 1917 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1515,8 +1916,11 @@
1515 1916 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1516 1917 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1517 1918 // Legacy vipps-badge shortcode. LP 19.11.2024
1518 1919 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1920 +
1921 + // special page handling, previously a fake page. LP 2026-08-25
1922 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1519 1923 }
1520 1924
1521 1925
1522 1926 public function log ($what,$type='info') {
@@ -1542,8 +1946,10 @@
1542 1946 }
1543 1947
1544 1948 // Show express button option on checkout form. LP 2026-03-23
1545 1949 public function checkout_before_customer_details_express () {
1950 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1951 + do_action('woo_vipps_checkout_before_customer_details_express');
1546 1952 $gw = $this->gateway();
1547 1953 if (!$gw->show_express_checkout()) return;
1548 1954 $this->express_checkout_section_html();
1549 1955 }
@@ -1553,37 +1959,12 @@
1553 1959 $header_text = __('Express Checkout', 'woo-vipps');
1554 1960 $header = "<legend class='express-header'>$header_text</legend>";
1555 1961 $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1556 1962 echo "<fieldset class='$div_classes'>$header";
1557 - $this->cart_express_checkout_button_html();
1963 + $this->checkout_express_checkout_button_html();
1558 1964 echo '</fieldset>';
1559 1965 }
1560 1966
1561 - public function express_checkout_banner() {
1562 - $gw = $this->gateway();
1563 - if (!$gw->show_express_checkout()) return;
1564 - return $this->express_checkout_banner_html();
1565 - }
1566 -
1567 - public function express_checkout_banner_html() {
1568 - $url = $this->express_checkout_url();
1569 - $url = wp_nonce_url($url,'express','sec');
1570 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1571 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1572 - $logo = $this->get_express_banner_logo();
1573 - $payment_method = $this->get_payment_method_name();
1574 -
1575 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1576 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1577 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1578 -
1579 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1580 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1581 - ?>
1582 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1583 - <?php
1584 - }
1585 -
1586 1967 // Show the express button if reasonable to do so
1587 1968 public function cart_express_checkout_button() {
1588 1969 $gw = $this->gateway();
1589 1970
@@ -1595,29 +1976,71 @@
1595 1976 public function minicart_express_checkout_button() {
1596 1977 $gw = $this->gateway();
1597 1978
1598 1979 if ($gw->show_express_checkout()){
1599 - return $this->cart_express_checkout_button_html(true);
1980 + return $this->cart_express_checkout_button_html('minicart');
1600 1981 }
1601 1982 }
1602 1983
1603 - public function cart_express_checkout_button_html($minicart = false) {
1604 - $url = $this->express_checkout_url();
1605 - $url = wp_nonce_url($url,'express','sec');
1606 - $page = $minicart ? 'minicart' : 'cart';
1607 - $imgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo($page));
1984 + // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1985 + // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1986 + public function add_checkout_button_for_classic () {
1987 + $button = $this->get_html_button_for_context('checkout');
1988 + $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1989 + echo $submit;
1990 + }
1991 +
1992 + public function cart_express_checkout_button_html($context= 'cart') {
1993 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1608 1994 $method = $this->get_payment_method_name();
1609 1995 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1610 - $button = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'><img alt='$title' border=0 src='$imgurl'></a>";
1611 - $button = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1612 - echo $button;
1996 + $url = "#";
1997 + $sec = wp_create_nonce('express');
1998 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1999 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
2000 + echo $html;
1613 2001 }
1614 2002
2003 + public function checkout_express_checkout_button_html() {
2004 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
2005 + $method = $this->get_payment_method_name();
2006 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
2007 + $url = "#";
2008 + $sec = wp_create_nonce('express');
2009 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
2010 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
2011 + echo $html;
2012 + }
2013 +
1615 2014 // A shortcode for a single buy now button. Express checkout must be active; but I don't check for this here, as this button may be
1616 2015 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1617 2016 public function buy_now_button_shortcode ($atts) {
1618 - $args = shortcode_atts( array( 'id' => '','variant'=>'','sku' => '',), $atts );
1619 - return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode') . "</div>";
2017 + // The new web component button args. LP 2026-07-02
2018 + $button_args = $this->get_html_button_default_attrs();
2019 + unset($button_args['brand']);
2020 +
2021 + // Variant exists for the product variant. LP 2026-07-02
2022 + if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
2023 + unset($button_args['variant']);
2024 +
2025 + $args = shortcode_atts(
2026 + array(...$button_args,
2027 + 'id' => '','variant'=> '','sku' => '',
2028 + ),
2029 + $atts,
2030 + );
2031 +
2032 + // Variant exists for the product variant. LP 2026-07-02
2033 + $button_args = $args;
2034 + if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
2035 + unset($button_args['button_variant']);
2036 + unset($button_args['sku']);
2037 + unset($button_args['id']);
2038 + // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
2039 + // "Note also that there is a bug in the library, and it currently only renders one language per page."
2040 + // 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
2041 +
2042 + return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
1620 2043 }
1621 2044
1622 2045 // The express checkout shortcode implementation. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1623 2046 public function express_checkout_button_shortcode() {
@@ -1623,19 +2046,11 @@
1623 2046 public function express_checkout_button_shortcode() {
1624 2047 $gw = $this->gateway();
1625 2048 if (!$gw->cart_supports_express_checkout()) return;
1626 2049 ob_start();
1627 - $this->cart_express_checkout_button_html('shortcode');
2050 + $this->cart_express_checkout_button_html('cart');
1628 2051 return ob_get_clean();
1629 2052 }
1630 - // 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
1631 - public function express_checkout_banner_shortcode() {
1632 - $gw = $this->gateway();
1633 - if (!$gw->cart_supports_express_checkout()) return;
1634 - ob_start();
1635 - $this->express_checkout_banner_html();
1636 - return ob_get_clean();
1637 - }
1638 2053
1639 2054 // Manage the various product meta fields
1640 2055 public function process_product_meta ($id, $post) {
1641 2056 // This is for the 'buy now' button
@@ -2286,77 +2701,93 @@
2286 2701 return null;
2287 2702 }
2288 2703 }
2289 2704
2705 + // Special pages, and some callbacks. IOK 2018-05-18
2706 + public function template_redirect() {
2290 2707
2291 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2292 - public function pre_handle_404($current, $query) {
2293 - if (!is_admin()) {
2294 - $special = $this->is_special_page();
2295 - if ($special) {
2296 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2297 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2298 - return true;
2299 - }
2708 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2709 + // We redirect these to the new location.
2710 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2711 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2712 + $url = static::get_special_page_url();
2713 + $_GET['action'] = 'buy_product';
2714 + $q = build_query($_GET);
2715 + wp_redirect($url . "?" . $q, 302);
2716 + exit();
2300 2717 }
2301 - return $current;
2718 +
2719 + if (static::is_special_page()) {
2720 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2721 + remove_filter('template_redirect', 'redirect_canonical', 10);
2722 + // dont cache special page. LP 2026-08-25
2723 + $this->nocache();
2724 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2725 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2726 + }
2302 2727 }
2303 2728
2304 - // Special pages, and some callbacks. IOK 2018-05-18
2305 - public function template_redirect() {
2306 - global $post;
2307 - // Handle special callbacks
2308 - $special = $this->is_special_page() ;
2729 + // Ran in template redirect for the special page. IOK 2026-09-2
2730 + public function pre_special_page_actions ($action) {
2731 + // Change title dynamically depending on action. LP 2026-09-02
2732 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2309 2733
2310 - if ($special) {
2311 - remove_filter('template_redirect', 'redirect_canonical', 10);
2312 - do_action('woo_vipps_before_handling_special_page', $special);
2734 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2735 + // we start producing content IOK 2026-09-21
2736 + if ($action == 'wait_for_payment') {
2737 + $this->handle_payment_poll_and_redirect();
2738 + }
2313 2739
2314 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2315 - // to intercept express checkout etc. IOK 2022-03-18
2316 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2317 - $this->$special();
2318 - }
2740 + // Some validation is required for this action
2741 + if ($action == 'do_express_checkout') {
2742 + $this->vipps_express_checkout_consistency_check();
2319 2743 }
2744 + // These two actions require an extra script
2745 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2746 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2747 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2748 + ['in_footer'=>true]
2749 + );
2750 + }
2751 + }
2320 2752
2321 - $consentremoval = $this->is_consent_removal();
2322 - if ($consentremoval) {
2323 - remove_filter('template_redirect', 'redirect_canonical', 10);
2324 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2325 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2326 - $this->vipps_consent_removal_callback($consentremoval);
2753 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2754 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2755 + global $wp_query;
2756 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2757 +
2758 + // In block themes the whole template (header, footer, content) renders inside the main
2759 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2760 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2761 + // the queried page so an earlier title doesn't consume this one-shot filter.
2762 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2763 + switch ($_GET['action'] ?? '') {
2764 + case 'wait_for_payment':
2765 + $title = __('Processing order', 'woo-vipps');
2766 + break;
2767 + case 'do_express_checkout':
2768 + case 'buy_product':
2769 + $title = __('Express Checkout', 'woo-vipps');
2770 + break;
2327 2771 }
2328 2772 }
2773 + return $title;
2329 2774 }
2775 +
2330 2776 // Template handling for special pages. IOK 2018-11-21
2777 + // This is legacy - the special page is now a real page, so it can have a special template using standard WP methods. IOK 2026-09-11
2331 2778 public function template_include($template) {
2332 - $special = $this->is_special_page() ;
2333 - if ($special) {
2779 + if (static::is_special_page()) {
2334 2780 // Get any special template override from the options IOK 2020-02-18
2335 2781 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2336 2782 $found = locate_template($specific,false,false);
2337 2783 if ($found) $template=$found;
2338 2784
2339 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2785 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2340 2786 }
2341 2787 return $template;
2342 2788 }
2343 2789
2344 -
2345 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2346 - private function is_consent_removal () {
2347 -
2348 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2349 - if ( !get_option('permalink_structure')) {
2350 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2351 - return false;
2352 - }
2353 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2354 - return @$_REQUEST['callback'];
2355 - }
2356 - return false;
2357 - }
2358 -
2359 2790 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2360 2791 // the user if using Express Checkout IOK 2020-10-09
2361 2792 public function woocommerce_before_thankyou ($orderid) {
2362 2793 $order = wc_get_order($orderid);
@@ -2361,9 +2792,9 @@
2361 2792 public function woocommerce_before_thankyou ($orderid) {
2362 2793 $order = wc_get_order($orderid);
2363 2794 if ($order) {
2364 2795 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2365 - // -- or the same thing for Vipps Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2796 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2366 2797 $this->maybe_log_in_user($order);
2367 2798 $order->delete_meta_data('_vipps_limited_session');
2368 2799 $order->save();
2369 2800
@@ -2424,9 +2855,8 @@
2424 2855
2425 2856 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2426 2857 // IOK 2025-11-19
2427 2858 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2428 -
2429 2859 }
2430 2860
2431 2861 public function get_payment_method_name() {
2432 2862 return $this->gateway()->get_option('payment_method_name');
@@ -2446,14 +2876,13 @@
2446 2876 public function after_setup_theme() {
2447 2877 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2448 2878 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2449 2879
2450 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2880 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2451 2881 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2452 2882 // is important.
2453 2883 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2454 2884
2455 -
2456 2885 // Callbacks use the Woo API IOK 2018-05-18
2457 2886 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2458 2887 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2459 2888
@@ -2464,19 +2893,21 @@
2464 2893 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2465 2894 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2466 2895
2467 2896 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2468 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2897 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2469 2898 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2470 2899
2471 2900 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2472 2901 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2473 2902
2903 + // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2904 + // is Vipps
2905 + add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2906 + add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2474 2907
2475 - // Special pages and callbacks handled by template_redirect
2476 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2908 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2477 2909 add_action('template_redirect', array($this,'template_redirect'),1);
2478 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2479 2910
2480 2911 // Allow overriding their templates
2481 2912 add_filter('template_include', array($this,'template_include'), 10, 1);
2482 2913
@@ -2483,21 +2914,8 @@
2483 2914 // Ajax endpoints for checking the order status while waiting for confirmation
2484 2915 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2485 2916 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2486 2917
2487 -
2488 - // Buying a single product directly using express checkout IOK 2018-09-28
2489 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2490 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2491 -
2492 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2493 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2494 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2495 -
2496 - // Same thing, but for single products IOK 2018-05-28
2497 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2498 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2499 -
2500 2918 // Handle the cancel unpaid order action when the "hold stock" times out.
2501 2919 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2502 2920 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2503 2921 // For Checkout the rules are different though.
@@ -2576,9 +2994,8 @@
2576 2994 $this->vippsJSConfig = array();
2577 2995 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2578 2996 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2579 2997 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2580 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2581 2998 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2582 2999 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2583 3000 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2584 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2583,9 +3000,10 @@
2583 3000 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2584 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2585 3002 $this->vippsJSConfig['vippslocale'] = get_locale();
2586 3003 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2587 - $this->vippsJSConfig['logoSvgUrl'] = $this->get_payment_logo('buy-now-block');
3004 + $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
3005 + $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2588 3006
2589 3007
2590 3008 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2591 3009 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
@@ -2763,14 +3181,14 @@
2763 3181
2764 3182 $raw_post = @file_get_contents( 'php://input' );
2765 3183 $result = @json_decode($raw_post,true);
2766 3184
2767 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3185 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2768 3186 // .. and the epayment webhooks 2023-12-19
2769 3187 $ischeckout = false;
2770 3188 $iswebhook = false;
2771 3189 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2772 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3190 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2773 3191 if ($callback == 'checkout') {
2774 3192 $ischeckout = true;
2775 3193 }
2776 3194 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3184,10 +3602,10 @@
3184 3602 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3185 3603 exit();
3186 3604 }
3187 3605
3188 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3189 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3606 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3607 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3190 3608 // This needs to be done before "calculate totals".
3191 3609 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3192 3610 $ischeckout = $order->get_meta('_vipps_checkout');
3193 3611
@@ -3363,9 +3781,9 @@
3363 3781 ), 'debug');
3364 3782
3365 3783 }
3366 3784
3367 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3785 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3368 3786 // 'dynamic pricing' 2023-01-26
3369 3787 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3370 3788 $taxrate = 0;
3371 3789 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3491,9 +3909,9 @@
3491 3909 $vippsmethod['shippingMethod'] = $rate->get_label();
3492 3910 $vippsmethod['shippingMethodId'] = $key;
3493 3911 $vippsmethods[]=$vippsmethod;
3494 3912
3495 - // Metadata and settings stored for later use for Vipps Checkout
3913 + // Metadata and settings stored for later use for Checkout
3496 3914 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3497 3915 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3498 3916 // finalize the order. IOK 2025-08-15
3499 3917 $ratemap[$key]=$rate;
@@ -3511,9 +3929,9 @@
3511 3929 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3512 3930 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3513 3931 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3514 3932
3515 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3933 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3516 3934 // IOK 2025-08-15 and new Express yet another slightly different format.
3517 3935 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3518 3936 if ($ischeckout) {
3519 3937 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -3803,9 +4221,9 @@
3803 4221 WC()->cart->calculate_totals();
3804 4222 WC()->cart->set_session();
3805 4223 return true;
3806 4224 } catch (Exception $e) {
3807 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4225 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
3808 4226 return false;
3809 4227 }
3810 4228 }
3811 4229
@@ -3889,17 +4307,8 @@
3889 4307 header("X-Accel-Expires: 0");
3890 4308 }
3891 4309
3892 4310
3893 -
3894 - // Handle DELETE on a vipps consent removal callback
3895 - public function vipps_consent_removal_callback ($callback) {
3896 - Vipps::nocache();
3897 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3898 - // to escape here when the API is changed. IOK 2020-10-14
3899 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3900 - }
3901 -
3902 4311 public function woocommerce_payment_gateways($methods) {
3903 4312 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
3904 4313 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3905 4314 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -3924,9 +4333,11 @@
3924 4333 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
3925 4334 return $url;
3926 4335 }
3927 4336 $url = $this->express_checkout_url();
3928 - $url = wp_nonce_url($url,'express','sec');
4337 + // At this point, there is always a query argument here. IOK 2026-09-21
4338 + $nonce = wp_create_nonce('express');
4339 + $url = $url . "&sec=$nonce";
3929 4340
3930 4341 return $url;
3931 4342 }
3932 4343
@@ -3974,9 +4385,9 @@
3974 4385 if ($currentstatus != 'initiated') {
3975 4386 $this->log(sprintf(__("Order %2\$d is 'pending' but its %1\$s order status is '%3\$s' - this means that the order has been erroneously set to 'pending' after completion or cancellation. Will not process further, please check status of order at %1\$s and set to correct status in WooCommerce", 'woo-vipps'), $this->get_payment_method_name(), $o->get_id(), $currentstatus), 'debug');
3976 4387 return;
3977 4388 }
3978 - $this->check_status_of_pending_order($o, false, false);
4389 + $this->check_status_of_pending_order($o, false);
3979 4390 }
3980 4391 }
3981 4392
3982 4393 // Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session -
@@ -3981,30 +4392,35 @@
3981 4392
3982 4393 // Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session -
3983 4394 // e.g. wp-cron. IOK 2021-06-21
3984 4395 // Stop restoring session in wp-cron too. IOK 2021-08-23
3985 - public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3986 - $express = $order->get_meta('_vipps_express_checkout');
3987 - $vippstatus = $order->get_meta('_vipps_status');
3988 - if ($express && $maybe_restore_session) {
3989 - $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3990 - $this->callback_restore_session($order->get_id());
3991 - }
4396 + // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4397 + public function check_status_of_pending_order($order, $allow_retry=true) {
3992 4398 $gw = $this->gateway();
3993 4399
3994 4400 $order_status = null;
3995 4401 try {
3996 4402 $order->add_order_note(sprintf(__("Callback from %1\$s delayed or never happened; order status checked by periodic job", 'woo-vipps'), $this->get_payment_method_name()));
3997 - $order_status = $gw->callback_check_order_status($order, $allow_retry);
4403 +
4404 + // Poll status and correct woo status. LP 2026-05-19
4405 + $order_data = $gw->get_payment_details($order);
4406 +
4407 + // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4408 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4409 + /* translators: company name */
4410 + $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4411 + return;
4412 + }
4413 +
4414 + $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4415 + $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4416 + $order_status = $order->get_status();
4417 +
3998 4418 $this->log(sprintf(__("For order %2\$d order status at %1\$s is %3\$s", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id(), $order_status), 'debug');
3999 4419 } catch (Exception $e) {
4000 4420 $this->log(sprintf(__("Error getting order status at %1\$s for order %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id()), 'error');
4001 4421 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
4002 4422 }
4003 - // Ensure we don't keep using an old session for more than one order here.
4004 - if ($express && $maybe_restore_session) {
4005 - $this->callback_destroy_session();
4006 - }
4007 4423 return $order_status;
4008 4424 }
4009 4425
4010 4426 // This will probably be run in activate, but if the plugin is updated in other ways, will also be run on after_setup_theme. IOK 2020-04-01
@@ -4017,20 +4433,40 @@
4017 4433 }
4018 4434 }
4019 4435
4020 4436 public function activate () {
4021 - static::maybe_add_cron_event();
4022 - $gw = $this->gateway();
4437 + static::maybe_add_cron_event();
4438 + $gw = $this->gateway();
4023 4439
4024 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4025 - if ($gw->get_option('orderprefix') == 'Woo') {
4026 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4027 - }
4028 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4029 - $gw->initialize_webhooks();
4030 - $this->payment_method_name = $gw->get_option('payment_method_name');
4031 - }
4440 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4441 + if ($gw->get_option('orderprefix') == 'Woo') {
4442 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4443 + }
4444 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4445 + $gw->initialize_webhooks();
4446 + $this->payment_method_name = $gw->get_option('payment_method_name');
4032 4447
4448 +
4449 + // Check if the special page is noted and actually does exist
4450 + $special = static::get_special_page_id();
4451 + if ($special) {
4452 + $special_page = get_post($special);
4453 + if ($special_page && 'trash' !== $special_page->post_status) {
4454 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4455 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4456 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4457 + wp_update_post([
4458 + 'ID' => $special,
4459 + 'post_content' => $new_content,
4460 + ]);
4461 + }
4462 + } else {
4463 + delete_option('woocommerce_vipps_special_page_page_id');
4464 + }
4465 + }
4466 +
4467 + }
4468 +
4033 4469 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4034 4470 public static function deactivate() {
4035 4471 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4036 4472 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4043,9 +4479,9 @@
4043 4479 // Delete all settings if checked in settings menu. LP 2025-10-06
4044 4480 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4045 4481 if ($should_delete) {
4046 4482 // Delete options.
4047 - $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4483 + $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', 'vipps_button_options2', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4048 4484 foreach($options as $option) {
4049 4485 delete_option($option);
4050 4486 }
4051 4487 }
@@ -4083,9 +4519,10 @@
4083 4519 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4084 4520 private function maybe_set_vipps_as_default() {
4085 4521 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4086 4522 $gw = $this->gateway();
4087 - if ($gw->get_option('vippsdefault')=='yes') {
4523 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4524 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4088 4525 WC()->session->set('chosen_payment_method', $gw->id);
4089 4526 }
4090 4527 }
4091 4528
@@ -4097,28 +4534,12 @@
4097 4534 $order = wc_get_order($order->get_id());
4098 4535 $order_status = $order->get_status();
4099 4536
4100 4537 if ($order_status != 'pending') return $order_status;
4101 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4102 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4103 - if ($order_status == 'pending') {
4104 - if (WC()->session) {
4105 - $now = time();
4106 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4107 - if (!$then) {
4108 - $then = $now;
4109 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4110 - }
4111 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4112 - return $order_status;
4113 - }
4114 - } else {
4115 - // No session shouldn't be possible, but if it is..
4116 - return $order_status;
4117 - }
4118 - }
4538 +
4539 + $gw = $this->gateway();
4119 4540 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4120 - return $this->check_status_of_pending_order($order);
4541 + $newstatus = $gw->poll_and_check_order_status($order);
4121 4542 }
4122 4543
4123 4544 // In some situations we have to empty the cart when the user goes to Vipps, so
4124 4545 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4186,17 +4607,18 @@
4186 4607
4187 4608 // Maybe log in user
4188 4609 // It is done on the thank-you page of the order, and only for express checkout.
4189 4610 function maybe_log_in_user ($order) {
4611 +
4190 4612 if (is_user_logged_in()) return;
4191 4613 if (!$order || ! self::is_vipps_order($order)) return;
4192 4614
4193 4615 // We *do* want to log in express checkout customers, but not those that
4194 - // use the Vipps Checkout solution - those can change their emails in the
4616 + // use the Checkout solution - those can change their emails in the
4195 4617 // checkout screen. IOK 2021-09-03
4196 4618 $do_login = $order->get_meta('_vipps_express_checkout');
4197 4619
4198 - // We will not log in Vipps Checkout users unless the option for that is true
4620 + // We will not log in Checkout users unless the option for that is true
4199 4621 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4200 4622 $do_login = false;
4201 4623 }
4202 4624
@@ -4231,9 +4653,9 @@
4231 4653
4232 4654 // Both Checkout and Express Checkout have the below value set to true
4233 4655 if (!$order->get_meta('_vipps_express_checkout')) return;
4234 4656
4235 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4657 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4236 4658 // IOK 2023-07-27
4237 4659 $ischeckout = $order->get_meta('_vipps_checkout');
4238 4660 if ($ischeckout) {
4239 4661 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4333,129 +4755,233 @@
4333 4755 }
4334 4756 if (!$o) return;
4335 4757 if (!$o->get_meta('_vipps_single_product_express')) return;
4336 4758 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4337 - if ($failed) WC()->cart->empty_cart();
4759 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4760 + WC()->cart->empty_cart();
4338 4761 $this->restore_cart($o);
4339 4762 }
4340 4763
4341 4764
4342 - public function ajax_vipps_buy_single_product () {
4343 - Vipps::nocache();
4344 - static::set_locale_if_in_header();
4345 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4346 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4347 - $session = WC()->session;
4348 - if (!$session->has_session()) {
4349 - $session->set_customer_session_cookie(true);
4765 + // Actually create a express checkout order object, with no shipping or personal information, returning information about
4766 + // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4767 + // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4768 + private function create_and_process_express_order() {
4769 + $result = null;
4770 + $gw = $this->gateway();
4771 + try {
4772 + $orderid = $gw->create_partial_order();
4773 + do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4774 + } catch (Exception $e) {
4775 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4776 + return $result;
4777 + }
4778 + if (!$orderid) {
4779 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4780 + return $result;
4350 4781 }
4351 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4352 4782
4353 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4354 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4355 - $url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url());
4783 + try {
4784 + $this->maybe_add_static_shipping($gw,$orderid);
4785 + } catch (Exception $e) {
4786 + $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4787 + $this->log($e->getMessage(),'error');
4788 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4789 + return $result;
4790 + }
4356 4791
4357 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4358 - wp_send_json($result);
4359 - exit();
4792 + // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4793 + $ok = $gw->process_payment($orderid);
4794 + if ($ok && $ok['result'] == 'success') {
4795 + $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4796 + return $result;
4797 + }
4798 + $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4799 + return $result;
4360 4800 }
4361 4801
4362 - public function ajax_do_express_checkout () {
4363 - check_ajax_referer('do_express','sec');
4364 - Vipps::nocache();
4365 - static::set_locale_if_in_header();
4802 + // This creates a simple hash for the 'current order' which we will store in the session if we proceed to checkout. We use this to
4803 + // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4804 + public function create_order_hash($args=null) {
4805 + // If we have no arguments, we'll hash the cart.
4806 + if (empty($args)) {
4807 + $cartitems = WC()->cart->get_cart();
4808 + $orderspec = array();
4809 + foreach($cartitems as $item => $values) {
4810 + $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4811 + }
4812 + $args = $orderspec;
4813 + }
4814 + return md5(serialize($args));
4815 + }
4816 +
4817 +
4818 + // This method may provide HTML form elements to ask a user questions after starting
4819 + // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4820 + // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4821 + public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4822 + $elements = [];
4823 + $html = "";
4824 +
4825 + // First, let's check if we need to confirm the purchase.
4826 + $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4827 + if ($last_express_purchase_hash) {
4828 + list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4829 + $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4830 + if ($hash == $current_hash && (time() <= $cutoff )) {
4831 + $order = wc_get_order($orderid);
4832 + $status = $order ? $order->get_status() : false;
4833 + // IOK TODO/FIXME actually, if the order is pending/failed/cancelled and *identical* to our current productinfo, we could plausibly do a restart here. Would probably require careful checking though, and
4834 + // a different flow. IOK 2026-09-17
4835 + if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4836 + $header = __("Are you sure?",'woo-vipps');
4837 + $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
4838 + $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4839 + $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
4840 + }
4841 + }
4842 + }
4843 +
4366 4844 $gw = $this->gateway();
4845 + $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4846 + $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4847 + $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4367 4848
4368 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4369 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4370 - wp_send_json($result);
4371 - exit();
4849 + if ($askForTerms) {
4850 + $termsHTML = '';
4851 + // Include shop terms
4852 + ob_start();
4853 + wc_get_template('checkout/terms.php');
4854 + $termsHTML = ob_get_clean();
4855 + $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4856 + $elements['terms'] = $termsHTML;
4372 4857 }
4373 4858
4859 + // Custom fields
4860 + ob_start();
4861 + do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4862 + $extra_fields = ob_get_clean();
4863 + if (!empty($extra_fields)) {
4864 + $elements['extra'] = $extra_fields;
4865 + }
4374 4866
4375 -
4867 + if (!empty($elements)) {
4868 + $html = join("\n", array_values($elements));
4869 + $msg = join(",", array_keys($elements));
4870 + return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4871 + }
4376 4872
4873 + return false;
4874 +
4875 + }
4876 +
4877 + public function rest_do_express_checkout ($request) {
4878 + Vipps::nocache();
4879 + check_ajax_referer('express', 'sec');
4880 + static::set_locale_if_in_header();
4881 + $args = $request->get_json_params();
4882 + if (!$args) {
4883 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4884 + }
4885 +
4886 + // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4887 + if ( is_null( WC()->cart ) ) {
4888 + WC()->frontend_includes();
4889 + if ( ! WC()->session instanceof WC_Session ) {
4890 + WC()->session = new WC_Session_Handler();
4891 + WC()->session->init();
4892 + }
4893 + if (is_null( WC()->customer)) {
4894 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4895 + }
4896 + WC()->cart = new WC_Cart();
4897 + WC()->cart->get_cart_from_session();
4898 + }
4899 +
4900 +
4901 + $gw = $this->gateway();
4902 + if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4903 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4904 + return $result;
4905 + }
4377 4906 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4378 4907 $toolate = false;
4379 4908 $msg = "";
4380 4909 $valid = WC()->cart->check_cart_item_validity();
4381 4910 if ( is_wp_error( $valid) ) {
4382 - $toolate = true;
4383 - $msg = "<br>" . $valid->get_error_message();
4911 + $toolate = true;
4912 + $msg = "<br>" . $valid->get_error_message();
4384 4913 }
4385 4914 $stock = WC()->cart->check_cart_item_stock();
4386 - if ( is_wp_error( $stock) ) {
4387 - $toolate = true;
4388 - $msg = "<br>" . $stock->get_error_message();
4389 - }
4915 + if ( is_wp_error( $stock) ) {
4916 + $toolate = true;
4917 + $msg = "<br>" . $stock->get_error_message();
4918 + }
4390 4919
4391 4920 if ($toolate) {
4392 4921 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4393 - wp_send_json($result);
4394 - exit();
4922 + return $result;
4395 4923 }
4396 4924
4397 - try {
4398 - $orderid = $gw->create_partial_order();
4399 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4400 - } catch (Exception $e) {
4401 - $this->log($e->getMessage(),'error');
4402 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4403 - wp_send_json($result);
4404 - exit();
4405 - }
4406 - if (!$orderid) {
4407 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4408 - wp_send_json($result);
4409 - exit();
4925 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4926 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4927 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4928 + $cookies = $args['cookies'] ?? [];
4929 + foreach($cookies as $key => $value) {
4930 + if (!isset($_COOKIE[$key])) {
4931 + $_COOKIE[$key] = $value;
4932 + }
4410 4933 }
4934 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4935 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4936 + $others =$args['post'] ?? [];
4937 + foreach($args['post'] as $key=>$value) {
4938 + $_POST[$key] = $value;
4939 + }
4411 4940
4412 - try {
4413 - $this->maybe_add_static_shipping($gw,$orderid);
4414 - } catch (Exception $e) {
4415 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4416 - $this->log($e->getMessage(),'error');
4417 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4418 - wp_send_json($result);
4419 - exit();
4941 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4942 + $current_hash = $this->create_order_hash();
4943 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4944 + if (!$confirmation) {
4945 + $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4946 + if (!empty($result)) {
4947 + return $result;
4948 + }
4420 4949 }
4421 -
4422 - $ok = $gw->process_payment($orderid);
4423 - if ($ok && $ok['result'] == 'success') {
4424 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4425 - wp_send_json($result);
4426 - exit();
4950 +
4951 + $result = $this->create_and_process_express_order();
4952 + if ($result['ok'] == 1) {
4953 + $orderid = $result['orderid'];
4954 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4955 + WC()->session->save_data();
4427 4956 }
4428 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4429 - wp_send_json($result);
4430 - exit();
4957 + return $result;
4958 +
4431 4959 }
4432 4960
4433 - // 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
4434 - public function ajax_do_single_product_express_checkout() {
4435 - check_ajax_referer('do_express','sec');
4436 - Vipps::nocache();
4961 +
4962 + // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4963 + public function rest_do_single_product_express_checkout ($request) {
4964 + Vipps::nocache();
4437 4965 static::set_locale_if_in_header();
4438 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4439 - $gw = $this->gateway();
4440 -
4441 - if (!$gw->express_checkout_available()) {
4442 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4443 - wp_send_json($result);
4444 - exit();
4966 + $args = $request->get_json_params();
4967 + if (!$args) {
4968 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4445 4969 }
4970 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4446 4971
4972 + // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4973 + $varid = intval($args['variation_id'] ?? 0);
4974 + $prodid = intval($args['product_id'] ?? 0);
4975 + $sku = sanitize_text_field($args['sku'] ?? "");
4976 + $quantity = max(1, intval($args['quantity'] ?? 0));
4447 4977
4448 - // 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
4449 - // 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
4450 - $varid = intval(@$_POST['variation_id']);
4451 - $prodid = intval(@$_POST['product_id']);
4452 - $sku = sanitize_text_field(@$_POST['sku']);
4453 - $quant = intval(@$_POST['quantity']);
4454 4978
4455 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4456 - $variations = array();
4457 - foreach ($_POST as $key => $value ) {
4979 + // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4980 + // We just need to sanitize them.
4981 + $variations = [];
4982 + $invars = $args['post'] ?? [];
4983 + foreach ($invars as $key => $value) {
4458 4984 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4459 4985 continue;
4460 4986 }
4461 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4460,15 +4986,94 @@
4460 4986 }
4461 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4462 4988 }
4463 4989
4464 - $product = null;
4465 - $variant = null;
4466 - $parent = null;
4467 - $parentid = null;
4468 - $quantity = 1;
4469 - if ($quant && $quant>1) $quantity=$quant;
4990 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4991 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4992 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4993 + $cookies = $args['cookies'] ?? [];
4994 + foreach($cookies as $key => $value) {
4995 + if (!isset($_COOKIE[$key])) {
4996 + $_COOKIE[$key] = $value;
4997 + }
4998 + }
4470 4999
5000 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
5001 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
5002 + $others =$args['post'] ?? [];
5003 + foreach($args['post'] as $key=>$value) {
5004 + $_POST[$key] = $value;
5005 + }
5006 +
5007 + // Since this is the REST api, we need to load the cart manually here. *Not* loading the cart could be an option but unpredictable. IOK 2026-08-27
5008 + if ( is_null( WC()->cart ) ) {
5009 + WC()->frontend_includes();
5010 + if ( ! WC()->session instanceof WC_Session ) {
5011 + WC()->session = new WC_Session_Handler();
5012 + WC()->session->init();
5013 +
5014 + // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
5015 + if (! WC()->session->get_session_cookie()) {
5016 + $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
5017 + add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
5018 + try {
5019 + WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
5020 + } finally {
5021 + remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
5022 + }
5023 + }
5024 + }
5025 + if (is_null( WC()->customer)) {
5026 + WC()->customer = new WC_Customer( get_current_user_id(), true );
5027 + }
5028 + WC()->cart = new WC_Cart();
5029 + WC()->cart->get_cart_from_session();
5030 + }
5031 +
5032 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
5033 + // We calculate this here so we can add it to the session later. IOK 2026-09-09
5034 + $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
5035 + $current_hash = $this->create_order_hash($orderspec);
5036 +
5037 + // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
5038 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
5039 + if (!$confirmation) {
5040 + $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
5041 + if (!empty($result)) {
5042 + $response = new WP_REST_Response($result);
5043 + $response->set_status(200);
5044 + return $response;
5045 + }
5046 + }
5047 +
5048 + // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
5049 + $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
5050 + // And if we're going to express now so let's note the order. IOK 2026-08-27. Now this assumes success, but *basically* I think this is ok.
5051 + // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
5052 + if ($result['ok'] == 1) {
5053 + $orderid = $result['orderid'];
5054 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
5055 + WC()->session->save_data();
5056 + }
5057 +
5058 + $response = new WP_REST_Response($result);
5059 + $response->set_status(200);
5060 +
5061 + return $response;
5062 + }
5063 +
5064 + // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
5065 + private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
5066 + require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
5067 + $gw = $this->gateway();
5068 +
5069 + if (!$gw->express_checkout_available()) {
5070 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
5071 + return $result;
5072 + }
5073 + // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
5074 + // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
5075 +
4471 5076 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4472 5077 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
4473 5078 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4474 5079 try {
@@ -4481,17 +5086,14 @@
4481 5086 $product = wc_get_product($skuid);
4482 5087 }
4483 5088 } catch (Exception $e) {
4484 5089 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4485 - wp_send_json($result);
4486 - exit();
5090 + return $result;
4487 5091 }
4488 5092
4489 -
4490 5093 if (!$product) {
4491 5094 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4492 - wp_send_json($result);
4493 - exit();
5095 + return $result;
4494 5096 }
4495 5097
4496 5098 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4497 5099 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4498,34 +5100,30 @@
4498 5100
4499 5101 // This can't really happen, but if it did..
4500 5102 if ($prodid && $parentid && ($prodid != $parentid)) {
4501 5103 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4502 - wp_send_json($result);
4503 - exit();
5104 + return $result;
4504 5105 }
4505 5106 if (!$gw->product_supports_express_checkout($product)) {
4506 5107 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4507 - wp_send_json($result);
4508 - exit();
5108 + return $result;
4509 5109 }
4510 5110
4511 5111 // Somebody addded the wrong SKU
4512 5112 if ($product->get_type() == 'variable'){
4513 5113 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4514 - wp_send_json($result);
4515 - exit();
5114 + return $result;
4516 5115 }
4517 5116 // Final check of availability
4518 5117 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4519 5118 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4520 - wp_send_json($result);
4521 - exit();
5119 + return $result;
4522 5120 }
4523 5121
4524 5122 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4525 -
4526 5123 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4527 5124 // because some plugins actually override this.
5125 + // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
4528 5126 $current_cart = clone WC()->cart;
4529 5127 WC()->cart->empty_cart();
4530 5128
4531 5129 if ($parent && $parent->get_type() == 'variable') {
@@ -4532,50 +5130,22 @@
4532 5130 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4533 5131 } else {
4534 5132 WC()->cart->add_to_cart($product->get_id(),$quantity);
4535 5133 }
5134 + WC()->session->save_data();
4536 5135
4537 - try {
4538 - $orderid = $gw->create_partial_order();
4539 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4540 - } catch (Exception $e) {
4541 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4542 - wp_send_json($result);
4543 - exit();
4544 - }
5136 + $result = $this->create_and_process_express_order();
4545 5137
4546 - if (!$orderid) {
4547 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4548 - wp_send_json($result);
4549 - exit();
5138 + if ($result['ok'] ?? false) {
5139 + // Single product purchase, so save any contents of the real cart
5140 + $orderid = $result['orderid'];
5141 + $order = wc_get_order($orderid);
5142 + $order->update_meta_data('_vipps_single_product_express',true);
5143 + $order->save();
5144 + $this->save_cart($order,$current_cart);
4550 5145 }
4551 5146
4552 - try {
4553 - $this->maybe_add_static_shipping($gw,$orderid);
4554 - } catch (Exception $e) {
4555 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4556 - $this->log($e->getMessage(),'error');
4557 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4558 - wp_send_json($result);
4559 - exit();
4560 - }
4561 -
4562 -
4563 - // Single product purchase, so save any contents of the real cart
4564 - $order = wc_get_order($orderid);
4565 - $order->update_meta_data('_vipps_single_product_express',true);
4566 - $order->save();
4567 - $this->save_cart($order,$current_cart);
4568 -
4569 - $ok = $gw->process_payment($orderid);
4570 - if ($ok && $ok['result'] == 'success') {
4571 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4572 - wp_send_json($result);
4573 - exit();
4574 - }
4575 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4576 - wp_send_json($result);
4577 - exit();
5147 + return $result;
4578 5148 }
4579 5149
4580 5150 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4581 5151 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4618,9 +5188,9 @@
4618 5188 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4619 5189 }
4620 5190 }
4621 5191
4622 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5192 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4623 5193 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4624 5194 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4625 5195 // If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06
4626 5196 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4641,9 +5211,9 @@
4641 5211 $transaction = sanitize_text_field(@$_POST['transaction']);
4642 5212
4643 5213 $sessionorders= WC()->session->get('_vipps_session_orders');
4644 5214 if (!isset($sessionorders[$orderid])) {
4645 - wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5215 + wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
4646 5216 }
4647 5217
4648 5218 $order = wc_get_order($orderid);
4649 5219 if (!$order) {
@@ -4691,46 +5261,37 @@
4691 5261 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4692 5262 return false;
4693 5263 }
4694 5264
4695 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4696 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4697 - private function make_vipps_url($what) {
4698 - if ( !get_option('permalink_structure')) {
4699 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4700 - }
4701 - return trailingslashit(home_url($what, 'https'));
5265 + // The various return URLs for special pages of the Vipps stuff. Previously used a fake page and had to check permalink_structure. LP 2026-08-26
5266 + private function make_special_page_url($action) {
5267 + return add_query_arg('action', $action, $this->get_special_page_url());
4702 5268 }
5269 +
4703 5270 public function payment_return_url() {
4704 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5271 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4705 5272 }
4706 5273 public function express_checkout_url() {
4707 - return $this->make_vipps_url('vipps-express-checkout');
5274 + return $this->make_special_page_url('do_express_checkout');
4708 5275 }
4709 5276 public function buy_product_url() {
4710 - return $this->make_vipps_url('vipps-buy-product');
5277 + return $this->make_special_page_url('buy_product');
4711 5278 }
4712 5279
4713 - // Return the method in the Vipps
4714 - public function is_special_page() {
4715 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4716 - $method = null;
4717 - if ( get_option('permalink_structure')) {
4718 - foreach($specials as $special=>$specialmethod) {
4719 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4720 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4721 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4722 - $method = $specialmethod; break;
4723 - }
4724 - }
4725 - } else {
4726 - if (isset($_GET['VippsSpecialPage'])) {
4727 - $method = @$specials[$_GET['VippsSpecialPage']];
4728 - }
4729 - }
4730 - return $method;
5280 + public static function is_special_page() {
5281 + $id = static::get_special_page_id();
5282 + return $id && is_page($id);
4731 5283 }
4732 5284
5285 + public static function get_special_page_id() {
5286 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5287 + return $id > 0 ? $id : null;
5288 + }
5289 +
5290 + public static function get_special_page_url() {
5291 + return get_permalink(static::get_special_page_id());
5292 + }
5293 +
4733 5294 // Just create a spinner and a overlay.
4734 5295 public function spinner () {
4735 5296 $flavour = sanitize_title($this->get_payment_method_name());
4736 5297 ob_start();
@@ -4751,164 +5312,22 @@
4751 5312 return apply_filters('woo_vipps_spinner', ob_get_clean());
4752 5313 }
4753 5314
4754 5315
4755 - // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4756 - // Fallbacks to defaults for each payment method. LP 2025-12-15
4757 - public function get_express_logo($payment_method, $lang, $variant) {
4758 - $base = plugins_url('img', __FILE__);
4759 -
4760 - // A much more concise approach could be to name the variant files directly and do a oneliner, but harder to grep after the files' usage. LP 2025-12-12
4761 - $img_map = [
4762 - "vipps" => [
4763 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4764 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4765 - "en" => [
4766 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4767 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4768 - "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4769 - "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4770 - "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4771 - "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4772 - "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4773 - "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4774 -
4775 - ],
4776 - "no" => [
4777 - "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4778 - "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4779 - "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4780 - "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4781 - "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4782 - "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4783 - "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4784 - "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4785 - ],
4786 - "se" => [
4787 - "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4788 - "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4789 - "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4790 - "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4791 - "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4792 - "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4793 - "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4794 - "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4795 -
4796 - ],
4797 - ],
4798 - "mobilepay" => [
4799 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4800 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4801 - "en" => [
4802 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4803 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4804 - "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4805 - "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4806 - "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4807 - "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4808 - "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4809 - "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4810 -
4811 - ],
4812 - "dk" => [
4813 - "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4814 - "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4815 - "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4816 - "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4817 - "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4818 - "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4819 - "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4820 - "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4821 - ],
4822 - "fi" => [
4823 - "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4824 - "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4825 - "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4826 - "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4827 - "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4828 - "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4829 - "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4830 - "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4831 -
4832 - ],
4833 - ],
4834 -
4835 - ];
4836 -
4837 - $payment = strtolower($payment_method);
4838 - if ($lang === 'store') $lang = $this->get_customer_language();
4839 -
4840 - // Dont give a default if payment method not found. LP 2025-12-12
4841 - if (!array_key_exists($payment, $img_map)) {
4842 - return null;
4843 - }
4844 - $payment_map = $img_map[$payment];
4845 -
4846 - $img = null;
4847 - if (array_key_exists($lang, $payment_map)
4848 - && is_array($payment_map[$lang])
4849 - && array_key_exists($variant, $payment_map[$lang])) {
4850 - $img = @$payment_map[$lang][$variant];
4851 - }
4852 -
4853 - // Default fallback behaviour
4854 - if (!$img) {
4855 - $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4856 -
4857 - // First try getting default for payment method + language. LP 2026-01-16
4858 - if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4859 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4860 - $this->log(sprintf(__('Could not find chosen express logo for payment method %1$s, language %2$s, and variant %3$s, attempting to fall back on language and payment method, else only language.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4861 - $img = @$payment_map[$lang][$default];
4862 - }
4863 -
4864 - // If not found, then try global default for payment method. LP 2026-01-16
4865 - if (!$img) {
4866 - $img = @$payment_map[$default];
4867 - }
4868 -
4869 - // Found no logo at all, log this. LP 2026-01-16
4870 - if (!$img) {
4871 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4872 - $this->log(sprintf(__('Found no express logo fallback for payment method %1$s, language %2$s, and variant %3$s.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4873 - }
4874 - }
4875 - return $img;
5316 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5317 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5318 + public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5319 + return $this->get_html_button_for_context($context);
4876 5320 }
4877 5321
5322 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
4878 5323 // Get payment logo based on payment method, then language NT 2023-11-30
4879 - // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4880 - public function get_payment_logo($page = null) {
4881 - $lang = $this->get_customer_language();
4882 - $payment_method = $this->get_payment_method_name();
4883 - $variant = $this->get_express_logo_page_variant($page);
4884 - $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4885 - return $logo_url;
5324 + // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5325 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5326 + public function get_payment_logo($context = 'global') {
5327 + return $this->get_express_logo(null, null, null, $context);
4886 5328 }
4887 5329
4888 - /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4889 - private function get_express_logo_page_variant($page = null) {
4890 - $options = get_option('vipps_button_options');
4891 -
4892 - // Init defaults, use mini version by default in below pages. LP 2025-12-17
4893 - $use_mini = in_array($page, ['catalog']);
4894 - $variant = "";
4895 -
4896 - // Find correct variant from button settings. LP 2025-12-17
4897 - if (is_array($options) && array_key_exists('express', $options)) {
4898 - if (array_key_exists($page, $options['express']['force-mini'])) {
4899 - $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4900 - }
4901 - $key = $use_mini ? 'mini-variant' : 'variant';
4902 - $variant = sanitize_title($options['express'][$key]) ?? '';
4903 - }
4904 -
4905 - if (!$variant) {
4906 - $variant = $use_mini ? "default-mini" : "default";
4907 - }
4908 - return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4909 - }
4910 -
4911 5330 // Get express banner logo based on payment method. LP 2025-09-03
4912 5331 private function get_express_banner_logo() {
4913 5332 $payment_method = $this->get_payment_method_name();
4914 5333
@@ -4919,10 +5338,12 @@
4919 5338 }
4920 5339 return null;
4921 5340 }
4922 5341
4923 - // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4924 - public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
5342 + // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5343 + // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5344 + // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5345 + public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
4925 5346 $disabled = $disabled ? 'disabled' : '';
4926 5347 $data = array();
4927 5348
4928 5349 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
@@ -4937,9 +5358,8 @@
4937 5358 if ($sku) $data['product_sku'] = $sku;
4938 5359 if ($product_id) $data['product_id'] = $product_id;
4939 5360 if ($variation_id) $data['variation_id'] = $variation_id;
4940 5361
4941 -
4942 5362 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4943 5363 foreach($data as $key=>$value) {
4944 5364 $value = esc_attr($value);
4945 5365 $buttoncode .= " data-$key='$value' ";
@@ -4946,14 +5366,18 @@
4946 5366 }
4947 5367
4948 5368 $payment_method = $this->get_payment_method_name();
4949 5369 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4950 - $short = str_ends_with($logo_variant, 'mini');
4951 - $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4952 5370
4953 - $message =" <img border=0 src='$logo' alt='$payment_method'/>";
5371 + if (is_array($button_args_override) && $button_args_override) {
5372 + $button_args = $button_args_override;
5373 + } else {
5374 + $button_args = $this->get_html_button_attrs_for_context($context);
5375 + }
5376 + $short = ($button_args['compact'] ?? 'false') === 'true';
5377 + $button = $this->get_html_button($button_args);
4954 5378
4955 -# Extra classes, if passed IOK 2019-02-26
5379 + # Extra classes, if passed IOK 2019-02-26
4956 5380 if (is_array($classes)) {
4957 5381 $classes = join(" ", $classes);
4958 5382 }
4959 5383 if ($classes) $classes = " $classes";
@@ -4958,19 +5382,15 @@
4958 5382 }
4959 5383 if ($classes) $classes = " $classes";
4960 5384 if ($short) $classes = "short $classes";
4961 5385
4962 - $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
5386 + $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5387 +
5388 +
5389 +
4963 5390 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4964 5391 }
4965 5392
4966 - // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4967 - public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4968 - $logo_lang = $this->get_customer_language();
4969 - $logo_variant = $this->get_express_logo_page_variant($page);
4970 - return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4971 - }
4972 -
4973 5393 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4974 5394 public function single_product_buy_now_button () {
4975 5395 $gw = $this->gateway();
4976 5396 $how = $gw->get_option('singleproductexpress');
@@ -5050,51 +5470,90 @@
5050 5470 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5051 5471 }
5052 5472
5053 5473
5054 -
5055 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5474 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5056 5475 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5476 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5057 5477 public function woocommerce_create_pages ($data) {
5478 + // Vipps Checkout page
5058 5479 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5059 - if (!$vipps_checkout_activated) return $data;
5480 + if ($vipps_checkout_activated) {
5481 + $data['vipps_checkout'] = array(
5482 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5483 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5484 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5485 + );
5486 + }
5060 5487
5061 - $data['vipps_checkout'] = array(
5062 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5063 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5064 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5065 - );
5066 -
5488 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5489 + $data['vipps_special_page'] = [
5490 + 'name' => 'vipps-payment', // slug
5491 + /* translators: company name */
5492 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5493 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5494 + ];
5067 5495 return $data;
5068 5496 }
5069 5497
5070 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5498 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5499 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5071 5500 public function maybe_create_vipps_pages () {
5501 + $make_pages = false;
5502 +
5503 + // Vipps Checkout page. LP 2026-08-18
5072 5504 $checkoutid = wc_get_page_id('vipps_checkout');
5073 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5074 - if ($makeit) {
5505 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5075 5506 delete_option('woocommerce_vipps_checkout_page_id');
5507 + $make_pages = true;
5076 5508 }
5077 5509
5078 - if ($makeit) {
5079 - WC_Install::create_pages();
5510 + // vipps special page, previously a fake page. LP 2026-08-18
5511 + $builtin_special_page_id = static::get_special_page_id();
5512 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5513 + delete_option('woocommerce_vipps_special_page_page_id');
5514 + $make_pages = true;
5080 5515 }
5516 +
5517 + if ($make_pages) {
5518 + WC_Install::create_pages();
5519 + }
5081 5520 }
5082 5521
5522 + public function vipps_special_page_shortcode($atts, $content) {
5523 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5524 + if (is_admin()) return;
5525 + if (wp_doing_ajax()) return;
5526 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5527 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5083 5528
5529 + $action = $_GET['action'] ?? '';
5530 + $html = "";
5531 + switch ($action) {
5532 + case 'wait_for_payment':
5533 + $html = $this->vipps_wait_for_payment();
5534 + break;
5535 + case 'do_express_checkout':
5536 + $html = $this->vipps_express_checkout();
5537 + break;
5538 + case 'buy_product':
5539 + $html = $this->vipps_buy_product();
5540 + break;
5541 + default:
5542 + $html = '';
5543 + }
5544 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5545 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5546 +
5547 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5548 + return $html;
5549 + }
5550 +
5551 +
5084 5552 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5085 5553 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5086 5554 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5087 5555 public function vipps_buy_product() {
5088 - status_header(200,'OK');
5089 - Vipps::nocache();
5090 -
5091 - add_filter('body_class', function ($classes) {
5092 - $classes[] = 'vipps-express-checkout';
5093 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5094 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5095 - });
5096 -
5097 5556 do_action('woo_vipps_express_checkout_page');
5098 5557
5099 5558 $session = WC()->session;
5100 5559 $posted = $session->get('__vipps_buy_product');
@@ -5123,39 +5582,38 @@
5123 5582
5124 5583 if (!$productinfo) {
5125 5584 $title = __("Product is no longer available",'woo-vipps');
5126 5585 $content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps');
5127 - return $this->fakepage($title,$content);
5586 + return $this->special_page_html($title,$content);
5128 5587 }
5129 5588
5130 5589 // Pass the productinfo to the express checkout form
5131 5590 $args = array();
5132 - $args['quantity'] = 1;
5133 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5134 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5135 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5136 - if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']);
5591 + $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5592 + $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5593 + $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5594 + $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5137 5595
5138 - // 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.
5139 - foreach ($productinfo as $key => $value) {
5140 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5141 - continue;
5142 - }
5143 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5144 - }
5596 + $payment_method = $this->get_payment_method_name();
5597 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5598 + $bclass = esc_attr($payment_method);
5145 5599
5146 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5600 + $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5601 + $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5602 + $content .= "<a href='javascript:void(0)' class='single-product button vipps-buy-now $bclass' data-vipps-autostart='true' data-vipps-purchase='single' data-product_id='{$args['product_id']}' data-variation_id='{$args['variation_id']}' data-product_sku='{$args['sku']}' data-quantity='{$args['quantity']}' title='{$btitle}';
5603 + >";
5604 + $content .= $this->get_html_button_for_context('global');
5605 + $content .= "</a>";
5606 + $content .= "</div>";
5607 +
5608 + return $content;
5147 5609 }
5148 5610
5149 - // This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts.
5150 - public function vipps_express_checkout() {
5151 - status_header(200,'OK');
5152 - Vipps::nocache();
5611 + public function vipps_express_checkout_consistency_check() {
5153 5612 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5154 5613 // IOK 2018-05-28
5155 5614 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5156 5615
5157 -
5158 5616 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5159 5617 if (!$backurl) $backurl = home_url();
5160 5618
5161 5619 if (!$ok) {
@@ -5169,218 +5627,39 @@
5169 5627 wp_redirect($backurl);
5170 5628 exit();
5171 5629 }
5172 5630
5173 - add_filter('body_class', function ($classes) {
5174 - $classes[] = 'vipps-express-checkout';
5175 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5176 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5177 - });
5178 -
5179 - do_action('woo_vipps_express_checkout_page');
5180 -
5181 - $this->print_express_checkout_page(true, 'do_express_checkout');
5631 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5182 5632 }
5183 5633
5184 - // This method tries to ensure that a customer does not 'lose' the return page and
5185 - // starts ordering the same products twice. IOK 2020-01-22
5186 - protected function validate_express_checkout_orderspec ($orderspec) {
5187 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5188 -
5189 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5190 - $orderset = array();
5191 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5192 -
5193 - // Then get open orders
5194 - $sessionorders = array();
5195 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5196 - if ($sessionorderdata) {
5197 - foreach(array_keys($sessionorderdata) as $oid) {
5198 - $orderobject = wc_get_order($oid);
5199 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5200 - if ($orderobject instanceof WC_Order) {
5201 - $sessionorders[] = $orderobject;
5202 - }
5203 - }
5634 + // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5635 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5636 + public function vipps_express_checkout() {
5637 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5638 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5639 + $content = __('Link expired, please try again', 'woo-vipps');
5640 + return $content;
5204 5641 }
5205 - // Nothing more to do here
5206 - if (empty($sessionorders)) return true;
5642 +
5643 + do_action('woo_vipps_express_checkout_page');
5207 5644
5208 - // And create a similar hash table for each of the open orders
5209 - $openorderdata = array();
5210 - foreach ($sessionorders as $open_order) {
5211 - $status = $open_order->get_status();
5212 - if ($status == 'cancelled' || $status == 'pending') continue;
5213 - $when = strtotime($open_order->get_date_modified());
5214 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5215 - if (time() > $cutoff) {
5216 - continue;
5217 - }
5218 - $orderdata = array();
5219 - foreach($open_order->get_items() as $item) {
5220 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5221 - $orderdata[] = $productspec;
5222 - }
5223 - $openorderdata[]=$orderdata;
5224 - }
5645 + $payment_method = $this->get_payment_method_name();
5646 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5647 + $bclass = esc_attr($payment_method);
5648 + $sec = esc_attr($_REQUEST['sec']);
5649 + $content = "";
5650 + $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5651 + $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5652 + $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5653 + $content .= $this->get_html_button_for_context('global');
5654 + $content .="</a>";
5655 + $content .="</div>";
5225 5656
5226 - // 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.
5227 - foreach($openorderdata as $prevorder) {
5228 - $a = array_diff($prevorder, $orderset);
5229 - $b = array_diff($orderset, $prevorder);
5230 - if (empty($a) && empty($b)) {
5231 - $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');
5232 - return false;
5233 - }
5234 - }
5235 - // Else, order is good.
5236 - return true;
5657 + return $content;
5237 5658 }
5238 5659
5239 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5240 - // Return value is like in a cart.
5241 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5242 - protected function get_orderspec_from_arguments ($productinfo) {
5243 - if (!$productinfo) return array();
5244 - $variantid = 0;
5245 - $productid = 0;
5246 - $quantity = intval(@$productinfo['quantity']);
5247 - if (!$quantity) $quantity = 1;
5248 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5249 - $sku = $productinfo['sku'];
5250 - $skuid = wc_get_product_id_by_sku($sku);
5251 - $product = wc_get_product($skuid);
5252 - $parentid = $product ? $product->get_parent_id() : null;
5253 - if ($product) {
5254 - if ($parentid) {
5255 - $variantid = $skuid; $productid = $parentid;
5256 - } else {
5257 - $productid = $skuid;
5258 - }
5259 - }
5260 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5261 - $productid = intval($productinfo['product_id']);
5262 - $variantid = intval(@$productinfo['variation_id']);
5263 - }
5264 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5265 - return array();
5266 - }
5267 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5268 - protected function get_orderspec_from_cart () {
5269 - $cartitems = WC()->cart->get_cart();
5270 - $orderspec = array();
5271 - foreach($cartitems as $item => $values) {
5272 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5273 - }
5274 - return $orderspec;
5275 - }
5276 -
5277 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5278 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5279 - $gw = $this->gateway();
5280 -
5281 - $expressCheckoutMessages = array();
5282 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5283 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5284 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5285 -
5286 - 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');
5287 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5288 - wp_enqueue_script('vipps-express-checkout');
5289 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5290 - // to actually perform the express checkout.
5291 - $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5292 -
5293 -
5294 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5295 - if (empty($orderspec)) {
5296 - $orderspec = $this->get_orderspec_from_cart();
5297 - }
5298 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5299 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5300 -
5301 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5302 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5303 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5304 -
5305 - $askForConfirmationHTML = '';
5306 - if (!$orderisOK) {
5307 - $header = __("Are you sure?",'woo-vipps');
5308 - $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');
5309 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5310 - }
5311 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5312 - $execute = $execute && $orderisOK && !$askForTerms;
5313 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5314 -
5315 - $content = $this->spinner();
5316 -
5317 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5318 - // The form data below is sent on order creation; the sec is also used to poll session status
5319 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5320 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5321 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5322 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5323 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5324 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5325 - ob_start();
5326 - do_action( 'woocommerce_after_order_notes');
5327 - $content .= ob_get_clean();
5328 - }
5329 - $content .= wp_nonce_field('do_express','sec',1,false);
5330 -
5331 - $termsHTML = '';
5332 - if ($askForTerms) {
5333 - // Include shop terms
5334 - ob_start();
5335 - wc_get_template('checkout/terms.php');
5336 - $termsHTML = ob_get_clean();
5337 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5338 - }
5339 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5340 -
5341 - if ($productinfo) {
5342 - foreach($productinfo as $key=>$value) {
5343 - $k = esc_attr($key);
5344 - $v = esc_attr($value);
5345 - $content .= "<input type='hidden' name='$k' value='$v' />";
5346 - }
5347 - }
5348 - ob_start();
5349 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5350 - $content .= ob_get_clean();
5351 - $content .= "</form>";
5352 -
5353 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5354 - $pressTheButtonHTML = "";
5355 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5356 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5357 - }
5358 -
5359 - if ($execute) {
5360 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5361 - $content .= "<div id='vipps-status-message'></div>";
5362 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5363 - return;
5364 - } else {
5365 - $content .= $askForConfirmationHTML;
5366 - $content .= $extraHTML;
5367 - $content .= $termsHTML;
5368 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5369 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5370 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='button vipps-express-checkout' title='$title'><img alt='$title' border=0 src='$buttonimgurl'></a></div>";
5371 - $content .= "<div id='vipps-status-message'></div>";
5372 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5373 - return;
5374 - }
5375 - }
5376 -
5377 -
5378 -
5379 - public function vipps_wait_for_payment() {
5380 - status_header(200,'OK');
5381 - Vipps::nocache();
5382 -
5660 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5661 + private function handle_payment_poll_and_redirect () {
5383 5662 $orderid = WC()->session->get('_vipps_pending_order');
5384 5663
5385 5664 $order = null;
5386 5665 $gw = $this->gateway();
@@ -5394,9 +5673,9 @@
5394 5673 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5395 5674 // simulating the session with that.
5396 5675 // IOK 2019-11-19, changed to using GET 2023-01-23
5397 5676 if ($no_session && $limited_session) {
5398 - $orderid = intval(@$_GET['id']);
5677 + $orderid = intval($_GET['id'] ?? false);
5399 5678 }
5400 5679 if ($orderid) {
5401 5680 clean_post_cache($orderid);
5402 5681 $order = wc_get_order($orderid);
@@ -5411,20 +5690,22 @@
5411 5690 $session = WC()->session;
5412 5691 if (!$session->has_session()) {
5413 5692 $session->set_customer_session_cookie(true);
5414 5693 }
5694 +
5695 + $sessionorders= WC()->session->get('_vipps_session_orders');
5696 + $sessionorders[$orderid] = 1;
5697 + WC()->session->set('_vipps_session_orders',$sessionorders);
5415 5698 $session->set('_vipps_pending_order', $orderid);
5699 + WC()->session->save_data();
5416 5700 }
5417 5701 }
5418 5702
5419 -
5420 - do_action('woo_vipps_wait_for_payment_page',$order);
5421 -
5422 5703 $deleted_order=0;
5423 5704 if ($orderid && !$order) {
5424 5705 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5425 5706 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5426 - $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5707 + $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5427 5708 $deleted_order=1;
5428 5709 }
5429 5710
5430 5711 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5434,12 +5715,13 @@
5434 5715
5435 5716 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5436 5717 $do_poll = true;
5437 5718
5438 - // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5719 + // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5439 5720 if ($do_poll && $status == 'pending') {
5440 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5441 - $newstatus = $gw->callback_check_order_status($order);
5721 + // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5722 + $newstatus = $gw->poll_and_check_order_status($order);
5723 + $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5442 5724 if ($status != $newstatus) {
5443 5725 $status = $newstatus;
5444 5726 clean_post_cache($orderid);
5445 5727 $order = wc_get_order($orderid); // Reload order object
@@ -5444,11 +5726,13 @@
5444 5726 clean_post_cache($orderid);
5445 5727 $order = wc_get_order($orderid); // Reload order object
5446 5728 }
5447 5729 } else {
5448 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5449 5731 }
5450 5732
5733 + // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5734 + // the payment status at Vipps without polls, which will tell us if the payment succeeded in case people use custom order statuses and so on. IOK 2026-09-29
5451 5735 $payment = 'notchecked';
5452 5736 if ($do_poll) {
5453 5737 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5454 5738 }
@@ -5462,9 +5746,8 @@
5462 5746 exit();
5463 5747 }
5464 5748
5465 5749 // We are done, but in failure. Don't poll.
5466 - $content = "";
5467 5750 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5468 5751
5469 5752 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5470 5753 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
@@ -5472,8 +5755,9 @@
5472 5755 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5473 5756 wp_redirect($failure_redirect);
5474 5757 exit();
5475 5758 }
5759 +
5476 5760 if ($status == 'cancelled' || $payment == 'cancelled') {
5477 5761 $this->maybe_restore_cart($orderid,'failed');
5478 5762 if ($failure_redirect){
5479 5763 wp_redirect($failure_redirect);
@@ -5478,27 +5762,46 @@
5478 5762 if ($failure_redirect){
5479 5763 wp_redirect($failure_redirect);
5480 5764 exit();
5481 5765 }
5766 + } else {
5767 + // If not, enqueue the status checker IOK 2026-09-21
5768 + wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5769 + }
5770 +
5771 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5772 +
5773 + // Communicate this to the shortcode IOK 2026-09-21
5774 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5775 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5776 + });
5777 +
5778 + }
5779 +
5780 + public function vipps_wait_for_payment() {
5781 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5782 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5783 +
5784 + $orderid = $data['orderid'] ?? 0;
5785 + $status = $data['status'] ?? "";
5786 + $payment = $data['payment'] ?? "";
5787 +
5788 + $order = wc_get_order($orderid);
5789 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5790 +
5791 + do_action('woo_vipps_wait_for_payment_page',$order);
5792 + $gw = $this->gateway();
5793 +
5794 + $content = "";
5795 + if ($status == 'cancelled' || $payment == 'cancelled') {
5482 5796 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5483 5797 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5484 5798 $content .= "</div>";
5485 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5486 -
5487 - return;
5799 + return $this->special_page_html('', $content);
5488 5800 }
5489 5801
5490 5802 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5491 -
5492 5803 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5493 - wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5494 -
5495 - // Check that order exists and belongs to our session. Can use WC()->session->get() I guess - set the orderid or a hash value in the session
5496 - // and check that the order matches (and is 'pending') (and exists)
5497 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5498 - $vippsstatus = $order->get_meta('_vipps_status');
5499 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5500 -
5501 5804 $signal = $this->callbackSignal($order);
5502 5805 $content = "";
5503 5806 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5504 5807
@@ -5506,18 +5809,18 @@
5506 5809 $signalurl = $this->callbackSignalURL($signal);
5507 5810
5508 5811 $content .= "</p></div>";
5509 5812
5510 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5511 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5512 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5813 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5814 +
5815 + // Carry the order status to the checking script IOK 2026-09-21
5816 + $content .= "<form id='vippsdata'>";
5513 5817 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5514 5818 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5515 5819 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5516 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5820 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5517 5821 $content .= "</form>";
5518 5822
5519 -
5520 5823 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5521 5824 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5522 5825 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5523 5826 $content .= "</div>";
@@ -5531,94 +5834,22 @@
5531 5834 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5532 5835 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5533 5836 $content .= "</div>";
5534 5837
5838 + return $this->special_page_html('', $content);
5839 + }
5535 5840
5536 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5841 + // Returns formatted html for the vipps special page. LP 2026-08-27
5842 + public function special_page_html($header, $content) {
5843 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5844 + $html = <<<EOF
5845 + $header_html
5846 + <div class="vipps-special-page-content">$content</div>
5847 + EOF;
5848 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5537 5849 }
5538 5850
5539 5851
5540 -
5541 - public function fakepage($title,$content) {
5542 - global $wp, $wp_query;
5543 - // We don't want this here.
5544 - remove_filter ('the_content', 'wpautop');
5545 -
5546 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5547 - $wp_post = null;
5548 - if ($specialid) {
5549 - $wp_post = get_post($specialid);
5550 - if ($wp_post) {
5551 - $wp_post->post_title = $title;
5552 - $wp_post->post_content = $content;
5553 - // Normalize a bit
5554 - $wp_post->filter = 'raw'; // important
5555 - $wp_post->post_status = 'publish';
5556 - $wp_post->comment_status= 'closed';
5557 - $wp_post->ping_status= 'closed';
5558 - } else {
5559 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5560 - }
5561 - }
5562 - if (!$wp_post || is_wp_error($wp_post)) {
5563 - $post = new stdClass();
5564 - $post->ID = -99;
5565 - $post->post_author = 1;
5566 - $post->post_date = current_time( 'mysql' );
5567 - $post->post_date_gmt = current_time( 'mysql', 1 );
5568 - $post->post_title = $title;
5569 - $post->post_content = $content;
5570 - $post->post_status = 'publish';
5571 - $post->comment_status = 'closed';
5572 - $post->ping_status = 'closed';
5573 - $post->post_name = 'vippsconfirm-fake-page-name';
5574 - $post->post_type = 'page';
5575 - $post->filter = 'raw'; // important
5576 - $wp_post = new WP_Post($post);
5577 - wp_cache_add( -99, $wp_post, 'posts' );
5578 - }
5579 -
5580 - // Update the main query
5581 - $wp_query->post = $wp_post;
5582 - $wp_query->posts = array( $wp_post );
5583 - $wp_query->queried_object = $wp_post;
5584 - $wp_query->queried_object_id = $wp_post->ID;
5585 - $wp_query->found_posts = 1;
5586 - $wp_query->post_count = 1;
5587 - $wp_query->max_num_pages = 1;
5588 - $wp_query->is_page = true;
5589 - $wp_query->is_singular = true;
5590 - $wp_query->is_single = false;
5591 - $wp_query->is_attachment = false;
5592 - $wp_query->is_archive = false;
5593 - $wp_query->is_category = false;
5594 - $wp_query->is_tag = false;
5595 - $wp_query->is_tax = false;
5596 - $wp_query->is_author = false;
5597 - $wp_query->is_date = false;
5598 - $wp_query->is_year = false;
5599 - $wp_query->is_month = false;
5600 - $wp_query->is_day = false;
5601 - $wp_query->is_time = false;
5602 - $wp_query->is_search = false;
5603 - $wp_query->is_feed = false;
5604 - $wp_query->is_comment_feed = false;
5605 - $wp_query->is_trackback = false;
5606 - $wp_query->is_home = false;
5607 - $wp_query->is_embed = false;
5608 - $wp_query->is_404 = false;
5609 - $wp_query->is_paged = false;
5610 - $wp_query->is_admin = false;
5611 - $wp_query->is_preview = false;
5612 - $wp_query->is_robots = false;
5613 - $wp_query->is_posts_page = false;
5614 - $wp_query->is_post_type_archive = false;
5615 - // Update globals
5616 - $GLOBALS['wp_query'] = $wp_query;
5617 - $wp->register_globals();
5618 - return $wp_post;
5619 - }
5620 -
5621 5852 // Support the interactivity API with data about our cart IOK 2026-02-23
5622 5853 public function woo_vipps_store_api_cart_data() {
5623 5854 // Reverting the condition with the directive data-wp-bind--hidden does not work, so we need the flipped bool here (hide instead of show). LP 2026-02-10
5624 5855
@@ -5624,13 +5855,15 @@
5624 5855
5625 5856 $checkout_page = $this->gateway()->vipps_checkout_available();
5626 5857 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5627 5858 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5859 +
5628 5860 $cart_data = array(
5629 5861 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5630 5862 'cart_supports_checkout' => (bool) $checkout_page,
5631 5863 'checkout_url' => $checkout_url,
5632 5864 );
5865 +
5633 5866 return $cart_data;
5634 5867 }
5635 5868
5636 5869 public function woo_vipps_store_api_cart_schema() {
@@ -5652,8 +5885,117 @@
5652 5885 ),
5653 5886 );
5654 5887 }
5655 5888
5889 + // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5890 + // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5891 + private function init_button_options() {
5892 + /* New structure as of now
5893 + * [
5894 + * 'version' => x.x,
5895 + * 'express' => [
5896 + * 'version' => x.x, used to migrate from previous iterations
5897 + * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5898 + * 'global' => ['compact' => ..., 'verb' => ..., ...],
5899 + * 'cart' => [...],
5900 + * 'product' => [...],
5901 + * 'checkout' => [...],
5902 + * ...
5903 + * ],
5904 + * 'product_configs' => [ overrides for specific products
5905 + * 1532 => ['compact' => ..., 'verb' => ..., ...],
5906 + * ...
5907 + * ],
5908 + * ],
5909 + * ]
5910 + */
5911 + $options = get_option('vipps_button_options2');
5912 + if (!empty($options)) return;
5913 +
5914 + $old_options = get_option('vipps_button_options');
5915 + $default_config = $this->get_html_button_default_attrs();
5916 + unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5917 + $default_compact = array_replace($default_config, ['compact' => 'true']);
5918 +
5919 + $default_options = [
5920 + 'version' => $this->button_options_version,
5921 + 'express' => [
5922 + 'version' => $this->button_options_express_version,
5923 + 'configs' => [
5924 + 'global' => $default_config,
5925 + // Need compact version by default for below pages. LP 2026-07-07
5926 + 'catalog' => $default_compact,
5927 + 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5928 + ],
5929 + 'product_configs' => [],
5930 + ],
5931 + ];
5932 +
5933 + $new_options = $default_options;
5934 +
5935 + //Actually, we have some options from the old structure IOK 2026-07-15
5936 + if (!empty($old_options)) {
5937 + $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5938 + unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5939 + }
5940 +
5941 + // Migrate context/page mini override to new context config. LP 2026-06-26
5942 + if (is_array($old_options['express']['force-mini'] ?? null)) {
5943 + foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5944 + if ("yes" === $use_mini) {
5945 + $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5946 + unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5947 + $config['compact'] = 'true';
5948 + $new_options['express']['configs'][$context] = $config;
5949 + }
5950 + }
5951 + }
5952 +
5953 + if ($this->get_payment_method_name() !== 'MobilePay') {
5954 + // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5955 + foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5956 + if ('fi' === ($config['language'] ?? '')) {
5957 + $config['language'] = 'store';
5958 + $new_options['express']['configs'][$context] = $config;
5959 + }
5960 + }
5961 + }
5962 +
5963 + /* translators: placeholders are arrays */
5964 + $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5965 +
5966 +
5967 +
5968 + update_option('vipps_button_options2', $new_options);
5969 + }
5970 +
5971 + // Old variant string => new config array. LP 2026-06-26
5972 + public function migrate_button_variant_to_config($variant_slug) {
5973 + if (!is_string($variant_slug)) return [];
5974 + $config = $this->get_html_button_default_attrs();
5975 + $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5976 + $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5977 + if (str_contains($variant_slug, 'buy-now')) {
5978 + $config['verb'] = 'buy';
5979 + } else if (str_contains($variant_slug, 'express')) {
5980 + $config['verb'] = 'express';
5981 + }
5982 + return $config;
5983 + }
5984 +
5985 + // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5986 + public function get_express_logo_variants() {
5987 + return [
5988 + 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5989 + 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5990 + 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5991 + 'express-pill' => __('Express pill', 'woo-vipps'),
5992 + 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5993 + 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5994 + ];
5995 + }
5996 +
5997 +
5656 5998 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5657 5999 public static function order_is_vipps_retryable($order_id) {
5658 6000 $order = wc_get_order($order_id);
5659 6001 if (!$order) return false;
@@ -5662,6 +6004,20 @@
5662 6004 $shipping_set = $order->get_meta('_vipps_shipping_set');
5663 6005
5664 6006 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5665 6007 return $nonexpress_epayment || $shipping_set;
6008 + }
6009 +
6010 + /** Returns the plugin's rest api namespace including the version.
6011 + * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
6012 + public static function get_rest_namespace($version = 'latest') {
6013 + $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
6014 + return self::REST_NAMESPACE_BASE . "/$version";
6015 + }
6016 +
6017 + /** Returns the plugin's rest api url.
6018 + * $version accepts 'latest', but you probably don't want to do that.
6019 + * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
6020 + public static function get_rest_url($version, $route) {
6021 + return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5666 6022 }
5667 6023 }