PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.1
Pay with Vipps and MobilePay for WooCommerce v6.3.1
6.3.1 6.2.6 6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 All 190 releases
woo-vipps / payment / Vipps.class.php

Vipps.class.php in Pay with Vipps and MobilePay for WooCommerce 6.3.1, at payment/Vipps.class.php

5,948 lines 309.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 This class is for hooks and plugin managent, and is instantiated as a singleton and set globally as $Vipps. IOK 2018-02-07
4 For WP-specific interactions.
5
6
7 This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce
8 Copyright (c) 2019 WP-Hosting AS
9
10 MIT License
11
12 Copyright (c) 2019 WP-Hosting AS
13
14 Permission is hereby granted, free of charge, to any person obtaining a copy
15 of this software and associated documentation files (the "Software"), to deal
16 in the Software without restriction, including without limitation the rights
17 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 copies of the Software, and to permit persons to whom the Software is
19 furnished to do so, subject to the following conditions:
20
21 The above copyright notice and this permission notice shall be included in all
22 copies or substantial portions of the Software.
23
24 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30 SOFTWARE.
31
32
33 */
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit; // Exit if accessed directly
36 }
37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38
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
44 private static $instance = null;
45
46 /* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */
47 public static $installed_gateways = [];
48
49 /* This directory stores the files used to speed up the callbacks checking the order status. IOK 2018-05-04 */
50 private $callbackDirname = 'wc-vipps-status';
51 private $countrymap = null;
52 // Used to provide the order in a callback to the session handler etc. IOK 2019-10-21
53 public $callbackorder = 0;
54
55 // True if HPOS is being used
56 public $HPOSActive = null;
57
58 public $vippsJSConfig = array();
59
60 public $button_options_version = '2.0';
61 public $button_options_express_version = '2.0';
62
63 // IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction
64 public static function CompanyName() {
65 return __("Vipps MobilePay", 'woo-vipps');
66 }
67 public static function CheckoutName($order=null) {
68 return "Vipps MobilePay Checkout"; // Do not translate
69 }
70 public static function ExpressCheckoutName($order=null) {
71 return __("Vipps MobilePay Express Checkout", 'woo-vipps');
72 }
73 public static function LoginName() {
74 return __("Login with Vipps", 'woo-vipps');
75 }
76
77 public static function instance() {
78 if (!static::$instance) static::$instance = new Vipps();
79 return static::$instance;
80 }
81
82 // recognize our own gateways or gateway ids IOK 2026-05-26
83 // param is either order or order's payment method id string. LP 2026-05-28
84 public static function is_vipps_order($order_or_string) {
85 $id = is_string($order_or_string) ? $order_or_string : $order_or_string->get_payment_method();
86 return in_array($id , ['vipps', 'vipps_card']);
87 }
88
89 // To simplify development, we load translations from the plugins' own .mos on development branches. IOK 2023-11-28
90 public static function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
91 $development = apply_filters('woo_vipps_use_plugin_translations', false);
92 if (!$development) {
93 return load_plugin_textdomain($domain, $deprecated, $plugin_rel_path);
94 }
95 // Available since 6.1.0 only IOK 2023-01-25
96 global $wp_textdomain_registry;
97 if ($wp_textdomain_registry) {
98 $locale = apply_filters( 'plugin_locale', determine_locale(), $domain );
99 $mofile = $domain . '-' . $locale . '.mo';
100 $path = WP_PLUGIN_DIR . '/' . trim( $plugin_rel_path, '/' );
101 $wp_textdomain_registry->set_custom_path( $domain, $path );
102 return load_textdomain( $domain, $path . '/' . $mofile, $locale );
103 }
104 }
105
106 public static function register_hooks() {
107 $Vipps = static::instance();
108 register_activation_hook(WC_VIPPS_MAIN_FILE, array($Vipps,'activate'));
109 register_deactivation_hook(WC_VIPPS_MAIN_FILE,array('Vipps','deactivate'));
110 if (is_admin()) {
111 add_action('admin_init',array($Vipps,'admin_init'));
112 add_action('admin_menu',array($Vipps,'admin_menu'));
113 } else {
114 add_action('wp_footer', array($Vipps,'footer'));
115 }
116 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
117 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
118 add_action( 'init',array($Vipps,'init'));
119 add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
120 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
121 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
122 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
123 // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
124 // not have loaded if the default checkout solution isn't the Checkout block. We'll load it anyway if the user has any local pickup locations
125 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
126 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
127
128 // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
129 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
130 // is important.
131 add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
132 }
133
134 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
135 public function woocommerce_blocks_loaded() {
136 if ( ! function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) {
137 return;
138 }
139 woocommerce_store_api_register_endpoint_data(
140 array(
141 'endpoint' => Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema::IDENTIFIER,
142 'namespace' => 'woo-vipps',
143 'data_callback' => [$this, 'woo_vipps_store_api_cart_data'],
144 'schema_callback' => [$this, 'woo_vipps_store_api_cart_schema'],
145 'schema_type' => ARRAY_A,
146 )
147 );
148 }
149
150 // Some different bits and pieces: If we are on the pay-for-order page, we cannot provide Vipps for an order that has been at Vipps. IOK 2024-05-17
151 // Since we now support Vipps restart sessions, we *may* now provide Vipps a payment option on this page even if the order has been at Vipps. LP 2026-03-10
152 public function payment_gateway_filter ($gateways) {
153 if (is_checkout_pay_page()) {
154 $orderid = absint(get_query_var( 'order-pay'));
155 $order = $orderid ? wc_get_order($orderid) : null;
156 if (is_a($order, 'WC_Order')
157 && $order->get_meta('_vipps_init_timestamp') // allow vipps payment for new orders, like when creating an order from backend. LP 2026-05-28
158 ) {
159 // Existing override that allows repayment. IOK 2024-06-04
160 // i.e a third party plugin that implemented payment retrying for our plugin, we used to enable repayment only if this plugin was found.
161 // $allow_repayment = class_exists('\Site\Plugins\WooVipps\WooVippsPayForOrder');
162 // However, now we implement payment retrying ourselves. LP 2026-03-18
163
164 $vipps_status = $order->get_meta('_vipps_status');
165 $retry_count = $order->get_meta('_vipps_retry_count');
166 $retry_enabled = apply_filters('woo_vipps_enable_payment_retry', true, $order, $vipps_status, $retry_count);
167 $order_is_retryable = static::order_is_vipps_retryable($order->get_id());
168
169 // by default enable repayment if we can retry the order. LP 2026-03-18
170 $allow_repayment = apply_filters('woo_vipps_allow_repayment', $retry_enabled && $order_is_retryable, $order); // legacy filter
171 if (!$allow_repayment) unset($gateways['vipps']);
172 }
173 }
174 return $gateways;
175 }
176
177 // Get the singleton WC_GatewayVipps instance
178 public function gateway() {
179 if (class_exists('WC_Payment_Gateway')) {
180 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
181 return WC_Gateway_Vipps::instance();
182 } else {
183 $this->log(__("Error: Cannot instantiate payment gateway, because WooCommerce is not loaded! This can happen when WooCommerce updates itself; but if it didn't, please activate WooCommerce again", 'woo-vipps'), 'error');
184 return null;
185 }
186 }
187
188
189 // These are strings that should be available for translation possibly at some future point. Partly to be easier to work with translate.wordpress.org
190 // Other usages are to translate any dynamic strings that may come from APIs etc. IOK 2021-03-18
191 private function translatable_strings() {
192 // Nothing here right now
193 return false;
194 }
195
196 // True iff support for HPOS has been activated IOK 2022-12-07
197 public function useHPOS() {
198 if ($this->HPOSActive == null) {
199
200 // Current way of checking IOK 2023-12-19
201 if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil')) {
202 if (Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) {
203 $this->HPOSActive = true;
204 } else {
205 $this->HPOSActive = false;
206 }
207 return $this->HPOSActive;
208 }
209
210 // This works in the backend, so ensures we are good with the meta fields etc.
211 if (function_exists('wc_get_container') && // 4.4.0
212 function_exists('wc_get_page_screen_id') && // Part of HPOS, not yet released
213 class_exists("Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController") &&
214 wc_get_container()->get( Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
215 $this->HPOSActive = true;
216 } else {
217 $this->HPOSActive = false;
218 }
219 }
220 return $this->HPOSActive;
221 }
222
223 public function init () {
224
225 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
226 // needs these to be defined in the backend. IOK 2024-04-16
227 add_action('wp_loaded', array($this, 'wp_register_scripts'));
228 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
229 add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
230
231 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
232 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
233
234 // Used in 'compat mode' only to add products to the cart
235 add_filter('woocommerce_add_to_cart_redirect', array($this, 'woocommerce_add_to_cart_redirect'), 10, 1);
236
237 $this->add_shortcodes();
238 $this->maybe_add_vipps_badge_feature();
239
240 // Handle the asynch call to send Order Management data on payment complete - this will push order data to the users' Vipps app
241 add_action('admin_post_nopriv_woo_vipps_order_management', array($this, 'do_order_management'));
242 add_action('admin_post_woo_vipps_order_management', array($this, 'do_order_management'));
243
244 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
245 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
246
247
248 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
249 // action scheduler would be better, but we can't do that just yet because of backwards
250 // compatibility. At some point, support for older woo-versions should be dropped; then this
251 // should use the action scheduler instead. IOK 2021-06-21
252 add_filter('cron_schedules', function ($schedules) {
253 if(!isset($schedules["5min"])){
254 $schedules["5min"] = array(
255 'interval' => 5*60,
256 'display' => __('Once every 5 minutes'));
257 }
258 return $schedules;
259 });
260 // Offload work to wp-cron so it can be done in the background on sites with heavy load IOK 2020-04-01
261 add_action('vipps_cron_cleanup_hook', array($this, 'cron_cleanup_hook'));
262 // Check periodically for orders that are stuck pending with no callback IOK 2021-06-21
263 add_action('vipps_cron_missing_callback_hook', array($this, 'cron_check_for_missing_callbacks'));
264
265 // For the rest, we need to read the payment gateways setting, and the payment gateway may not actually
266 // exist at this point. This is because for it to exist, WooCommerce must have loaded, and if it hasn't, for instance
267 // because it is self-updating or because it has been deactivated just now or something, we won't have access to it.
268 // Therefore test it first. IOK 2022-12-08
269 $gw = $this->gateway();
270
271 // Set default button options, migrating any older setup IOK 2026-07-15
272 $this->init_button_options();
273
274 /*
275 From version 6.2.x we create a real physical page to handle the "special" vipps pages,
276 where we earlier used just a fake page with no real page id, unless especially configured.
277 We therefore need to add code to maintain this special page.
278
279 woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
280 and we hook unto this with woocommerce_create_pages. LP 2026-09-03
281 */
282
283 // Delete special page id option when its deleted or trashed, so that we dont have to load
284 // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
285 $delete_special_page_id = function($post_id, $post = null) {
286 if (static::get_special_page_id() === $post_id) {
287 delete_option('woocommerce_vipps_special_page_page_id');
288 }
289 };
290 add_action('delete_post', $delete_special_page_id, 10, 2);
291 add_action('wp_trash_post', $delete_special_page_id, 10, 2);
292
293 $this->ensure_special_page_exists();
294
295
296 // We want this special page to have a certain title and maybe special scripts and so on,
297 // this gets run in template redirect for these pages.
298 add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
299
300 // Add an admin interface for this page as well IOK 2026-09-11
301 add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
302 }
303
304
305 public function rest_api_init () {
306
307 // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
308 register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
309 'methods' => 'GET',
310 'callback' => [$this, 'rest_express_checkout_products'],
311 'permission_callback' => '__return_true',
312 ]);
313
314 // Start a single product express checkout process. IOK 2026-08-25
315 register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
316 'methods' => 'POST',
317 'callback' => [$this, 'rest_do_single_product_express_checkout'],
318 'permission_callback' => '__return_true',
319 ]);
320 // And one for the cart. IOK 2026-09-04
321 register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
322 'methods' => 'POST',
323 'callback' => [$this, 'rest_do_express_checkout'],
324 'permission_callback' => '__return_true',
325 ]);
326 }
327
328 public function admin_init () {
329 $gw = $this->gateway();
330 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
331 $adminSettings = VippsAdminSettings::instance();
332 // Stuff for the Order screen
333 add_action('woocommerce_order_item_add_action_buttons', array($this, 'order_item_add_action_buttons'), 10, 1);
334
335 // Don't allow deletion of refunds made through Vipps IOK 2025-11-17
336 add_action('woocommerce_after_order_refund_item_name', function ($refund) {
337 $orderid = $refund->get_parent_id();
338 $order = wc_get_order($orderid);
339 if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
340 $id = $refund->get_id();
341 $gw = $refund->get_refunded_payment();
342 if ($gw) {
343 $msg = sprintf(__('Refunded through %1$s', 'woo-vipps'), $this->get_payment_method_name());
344 echo "<style>#woocommerce-order-items tr.refund[data-order_refund_id=\"" . intval($id) . "\"] .wc-order-edit-line-item .wc-order-edit-line-item-actions a.delete_refund { display: none; }</style>";
345 echo "<i>" . esc_html($msg) . "</i>";
346 }
347 }});
348
349 require_once(dirname(__FILE__) . "/VippsDismissibleAdminBanners.class.php");
350 VippsDismissibleAdminBanners::add();
351
352 // Styling etc
353 add_action('admin_head', array($this, 'admin_head'));
354
355 // Scripts
356 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
357
358 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
359 add_action('current_screen', function ($screen) {
360 if (!is_admin() || !$screen || $screen->id !== 'woocommerce_page_wc-settings') return;
361 $section = ($_GET['section'] ?? "");
362 if (($_GET['tab'] ?? "")!= 'checkout'
363 || !in_array($section, ['vipps', 'vipps_card'])
364 ) return;
365
366 $settings_tab = '';
367 if ('vipps_card' === $section) $settings_tab = '#Card payments';
368
369 wp_safe_redirect(admin_url("admin.php?page=vipps_settings_menu$settings_tab"));
370 exit();
371 });
372
373 // Custom product properties
374 // IOK 2024-01-17 temporary: The special product properties are currenlty only active for Vipps
375 // IOK 2025-09-01 now available for all
376 add_filter('woocommerce_product_data_tabs', array($this,'woocommerce_product_data_tabs'),99);
377 add_action('woocommerce_product_data_panels', array($this,'woocommerce_product_data_panels'),99);
378 add_action('woocommerce_process_product_meta', array($this, 'process_product_meta'), 10, 2);
379
380 add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
381
382 // Keep admin notices during redirects IOK 2018-05-07
383 add_action('admin_notices',array($this,'stored_admin_notices'));
384
385 // Ajax just for the backend
386 add_action('wp_ajax_vipps_create_shareable_link', array($this, 'ajax_vipps_create_shareable_link'));
387 add_action('wp_ajax_vipps_payment_details', array($this, 'ajax_vipps_payment_details'));
388 add_action('wp_ajax_vipps_update_admin_settings', array($adminSettings, 'ajax_vipps_update_admin_settings'));
389
390 // POST actions for the backend
391 add_action('admin_post_update_vipps_badge_settings', array($this, 'update_badge_settings'));
392 add_action('admin_post_update_vipps_button_settings', array($this, 'update_button_settings'));
393 add_action('admin_post_vipps_delete_webhook', array($this, 'vipps_delete_webhook'));
394 add_action('admin_post_vipps_add_webhook', array($this, 'vipps_add_webhook'));
395
396 // Link to the settings page from the plugin list
397 add_filter( 'plugin_action_links_'.plugin_basename(WC_VIPPS_MAIN_FILE ), array($this, 'plugin_action_links'));
398
399 if ($gw->enabled == 'yes' && $gw->is_test_mode()) {
400 $what = sprintf(__('%1$s is currently in test mode - no real transactions will occur', 'woo-vipps'), Vipps::CompanyName());
401 $this->add_vipps_admin_notice($what,'info', '', 'test-mode');
402 }
403
404
405 // This requires merchants using the old shipping callback filter to choose between this or the new shipping method mechanism. IOK 2020-02-17
406 if (has_action('woo_vipps_shipping_methods')) {
407 $option = $gw->get_option('newshippingcallback');
408 if ($option != 'old' && $option != 'new') {
409 $what = __('Your theme or a plugin is currently overriding the <code>\'woo_vipps_shipping_methods\'</code> filter to customize your shipping alternatives. While this works, this disables the newer Express Checkout shipping system, which is neccessary if your shipping is to include metadata. You can do this, or stop this message, from the <a href="%1$s">settings page</a>', 'woo-vipps');
410 $this->add_vipps_admin_notice($what,'info');
411 }
412 }
413
414 // IOK 2020-04-01 If the plugin is updated, the normal 'activate' hook may not run. Add the scheduled events if not present.
415 // Normal updates will not need this, but if updates are 'sideloaded', it is neccessary still. This call will only do work if the
416 // jobs are not scheduled. We'll ensure the action is active first time an admin logs in.
417 if (!defined('DOING_AJAX') || !DOING_AJAX) {
418 static::maybe_add_cron_event();
419 if (!get_option('woo-vipps-configured')) {
420 list($ok, $msg) = $gw->check_connection();
421 if (!$ok){
422 if ($msg) {
423 $this->add_vipps_admin_notice(sprintf(__("<p>%1\$s not yet correctly configured: please go to <a href='%2\$s'>the %1\$s settings</a> to complete your setup:<br> %3\$s</p>", 'woo-vipps'), Vipps::CompanyName(), admin_url('/admin.php?page=vipps_settings_menu'), $msg));
424 } else {
425 $this->add_vipps_admin_notice(sprintf(__("<p>%1\$s not yet configured: please go to <a href='%2\$s'>the %1\$s settings</a> to complete your setup!</p>", 'woo-vipps'), Vipps::CompanyName(), admin_url('/admin.php?page=vipps_settings_menu')));
426 }
427 }
428
429 }
430 // If we are configured, but we don't have any webhooks yet, initialize them for the epayment api. IOK 2023-12-20
431 // if we do have them, check them for consistency
432 if (get_option('woo-vipps-configured')) {
433 if (empty(get_option('_woo_vipps_webhooks'))) {
434 $gw->initialize_webhooks();
435 } else {
436 $ok = $gw->check_webhooks();
437 if (!$ok) {
438 $gw->initialize_webhooks();
439 };
440 }
441 }
442 }
443 }
444
445
446 /** Ensure we have a special page for payment flows
447 *
448 * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
449 * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
450 **/
451 public function ensure_special_page_exists() {
452 if (static::get_special_page_id()) return;
453 $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
454
455 // If user had in previous version overriden the fake page with a real one: migrate this page to be the special page. LP 2026-09-01
456 $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
457 if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
458 $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
459 // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
460 update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
461
462 // Ensure this page has the necessary shortcode. LP 2026-09-01
463 if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
464 $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
465 wp_update_post([
466 'ID' => $old_special_page_id,
467 'post_content' => $new_content,
468 ]);
469 }
470 } else {
471 // Create special page if its missing. LP 2026-09-01
472 $this->maybe_create_vipps_pages();
473 }
474 }
475
476 // Admin interface for the special page on woo/advanced/pages
477 public function woocommerce_settings_pages ($settings) {
478 $i = -1;
479 foreach($settings as $entry) {
480 $i++;
481 if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
482 break;
483 }
484 }
485 if ($i > 0) {
486 $vippspagesettings = array(
487 array(
488 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
489 'desc' => sprintf(__('This page is used for various special pages used by %1$s', 'woo-vipps'), Vipps::CompanyName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_special_page') ,
490 'id' => 'woocommerce_vipps_special_page_page_id',
491 'type' => 'single_select_page_with_search',
492 'default' => '',
493 'class' => 'wc-page-search',
494 'css' => 'min-width:300px;',
495 'args' => array(
496 'exclude' =>
497 array(
498 wc_get_page_id( 'myaccount' ),
499 wc_get_page_id( 'checkout' ),
500 wc_get_page_id( 'cart' ),
501 ),
502 ),
503 'desc_tip' => true,
504 'autoload' => false,
505 ));
506 array_splice($settings, $i, 0, $vippspagesettings);
507 }
508
509 return $settings;
510 }
511
512
513
514 // Runs on init, adds the Vipps badge feature if activated
515 public function maybe_add_vipps_badge_feature () {
516 $badge_options = get_option('vipps_badge_options');
517 if (!$badge_options || !@$badge_options['badgeon']) return false;
518
519 add_action('wp_enqueue_scripts', function () { wp_enqueue_script('vipps-onsite-messageing'); });
520 add_action('woocommerce_before_add_to_cart_form', function () use ($badge_options) {
521 global $product;
522 if (!is_a($product, 'WC_Product')) return;
523
524 $show = intval(@$badge_options['defaultall']);
525 $forthis = $product->get_meta('_vipps_show_badge', true);
526 $dontshow = ($forthis == 'none');
527
528 $doshow = !$dontshow && ($show || ($forthis && $forthis != 'none'));
529
530 if (!apply_filters('woo_vipps_show_vipps_badge_for_product', $doshow, $product)) {
531 return;
532 }
533
534 $attr = "";
535 if ($forthis != 'none' || isset($badge_options['variant'])) {
536 $variant = ($forthis && $forthis != 'none') ? $forthis : $badge_options['variant'];
537 $attr .= " variant='" . sanitize_title($variant) . "' ";
538 }
539
540 $lang = $this->get_customer_language();
541 if ($lang) {
542 $attr .= " language='". $lang . "' ";
543 }
544
545 $brand = $this->get_payment_method_name();
546 if ($brand) $attr .= " brand='". strtolower($brand) . "' ";
547
548
549 $badge = "<vipps-mobilepay-badge $attr></vipps-mobilepay-badge>";
550
551 echo apply_filters('woo_vipps_product_badge_html', $badge);
552 });
553
554 }
555
556 // A small interface for editing and managing the webhooks for the MSNs for this site IOK 2023-12-20
557 public function webhook_menu_page () {
558 if (!current_user_can('manage_woocommerce')) {
559 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
560 }
561 $portalurl = 'https://portal.vippsmobilepay.com';
562 $webhookapi = 'https://developer.vippsmobilepay.com/docs/APIs/webhooks-api/';
563
564 echo "<div class='wrap vipps-badge-settings'>\n";
565 echo "<h1>" . __('Webhooks', 'woo-vipps') . "</h1>\n";
566 echo "<p>"; printf(__('Whenever an event like a payment or a cancellation occurs on a %1$s account, you can be notified of this using a <i>webhook</i>. This is used by this plugin to get noticed of payments by users even when they do not return to your store.', 'woo-vipps'), Vipps::CompanyName()); echo "</p>";
567 echo "<p>"; __('To do this, the plugin will automatically add webhooks for the MSN - Merchant Serial Numbers - configured on this site', 'woo-vipps'); echo "</p>";
568 echo "<p>"; __('If your MSN has registered other callbacks, for instance for another website, you can manage these here - and you can also add your own hooks that will be notified of payment events to any other URL you enter.', 'woo-vipps'); echo "</p>";
569 echo "<p>"; printf(__('Implementing a webhook is not trivial, so you will probably need a developer for this. You can read more about what is required <a href="%1$s">here</a>. ', 'woo-vipps'), $webhookapi);
570 printf(__('Please note that there is normally a limit of <em><strong>5</strong> webhooks per MSN</em> - contact %1$s if you need more', 'woo-vipps'), Vipps::CompanyName());
571 echo "</p>";
572 echo "<p>"; print __('The following is a listing of your webhooks. If you have changed your website name, you may see some hooks that you do not recognize - these should be deleted', 'woo-vipps'); echo "</p>";
573
574 $keyset = $this->gateway()->get_keyset();
575 $recurrings = $this->gateway()->get_keyset();
576 foreach($recurrings as $msn=> $keys) {
577 if (!isset($keyset[$msn])) {
578 $keyset[$msn] = $keys;
579 }
580 }
581 $allhooks = $this->gateway()->initialize_webhooks();
582 $localhooks = get_option('_woo_vipps_webhooks');
583
584 echo "<form method='post' action='" . admin_url("admin-post.php") . "' autocomplete='off' id=webhook_action_form>";
585 echo "<input type='hidden' id='webhook_id' name='webhook_id' value='' autocomplete='false'>";
586 echo "<input type='hidden' id='webhook_msn' name='webhook_msn' value='' autocomplete='false'>";
587 echo "<input type='hidden' id='webhook_url' name='webhook_url' value='' autocomplete='false'>";
588 echo "<input type='hidden' id='webhook_events' name='webhook_events' value='' autocomplete='false'>";
589 echo "<input type='hidden' id='webhook_post_action' name='action' value='' autocomplete='false'>";
590 wp_nonce_field('webhook_nonce', 'webhook_nonce');
591 echo "</form>";
592
593 foreach ($keyset as $msn => $data) {
594 $testmode = $data['testmode'] ?? false;
595 echo "<div style='margin-top: 2rem; margin-bottom: 2rem'>";
596 echo "<h2>";
597 echo sprintf(__('Merchant Serial Number %1$s', 'woo-vipps'), $msn);
598 if ($testmode) echo " (" . __('Test mode', 'woo-vipps') . ")";
599 echo "<a style='float:right; font-size:smaller' class='webhook-adder' href='javascript:void(0)' data-msn='" . esc_attr($msn) . "'>[" . __('Add a webhook to this MSN', 'woo-vipps') . "]</a>";
600 echo "</h2>";
601
602 $all = $allhooks[$msn] ?? [];
603 $thehooks = $all['webhooks'] ?? [];
604 $locals = $localhooks[$msn] ?? [];
605
606 echo "<table class='table webhook-table'><thead><tr><th style='text-align: left'>" . __('Webhook', 'woo-vipps') . "</th><th>" . __('Action', 'woo-vipps') . "</th>" . "</tr></thead>";
607 echo "<tbody>";
608 foreach($thehooks as $hook) {
609 $id = $hook['id'];
610 $url = $hook['url'];
611 $events = $hook['events'];
612 $local = $locals[$id] ?? false;
613
614
615 echo "<tr" . ($local ? " class='local' " : '') . " data-webhook-id='" . esc_attr($id) . "' data-msn='" . esc_attr($msn) . "'";
616 echo " data-hookdata='" . json_encode($hook) . "'>";
617 echo "<td>" . esc_html($url) . "</td>";
618 echo "<td class='actions'>";
619 echo "<a href='javascript:void(0)' class='webhook-viewer'>[" . __('View', 'woo-vipps') . "]</a> ";
620 if (!$local) {
621 echo " <a href='javascript:void(0)' class='webhook-deleter'>[" . __('Delete', 'woo-vipps') . "]</a>";
622 } else {
623 echo " <em>". __('Created for this site', 'woo-vipps') . "</em>";
624 }
625 echo "</td>";
626 echo "</tr>";
627 }
628 echo "</tbody>";
629 echo "</table>";
630 echo "</div>";
631 echo "<hr>";
632 }
633
634 $epayment_events = [__('Created', 'woo-vipps') => 'epayments.payment.created.v1',
635 __('Aborted', 'woo-vipps') => 'epayments.payment.aborted.v1',
636 __('Expired', 'woo-vipps') => 'epayments.payment.expired.v1',
637 __('Cancelled', 'woo-vipps') => 'epayments.payment.cancelled.v1',
638 __('Captured', 'woo-vipps') => 'epayments.payment.captured.v1',
639 __('Refunded', 'woo-vipps') => 'epayments.payment.refunded.v1',
640 __('Authorized', 'woo-vipps') => 'epayments.payment.authorized.v1',
641 __('Terminated', 'woo-vipps') => 'epayments.payment.terminated.v1'];
642
643 $recurring_events = [ __('Agreement accepted', 'woo-vipps') =>'recurring.agreement-activated.v1',
644 __('Agreement rejected', 'woo-vipps') =>'recurring.agreement-rejected.v1',
645 __('Agreement stopped', 'woo-vipps') =>'recurring.agreement-stopped.v1',
646 __('Agreement expired', 'woo-vipps') =>'recurring.agreement-expired.v1',
647 __('Charge reserved', 'woo-vipps') =>'recurring.charge-reserved.v1',
648 __('Charge captured', 'woo-vipps') =>'recurring.charge-captured.v1',
649 __('Charge cancelled', 'woo-vipps') =>'recurring.charge-canceled.v1',
650 __('Charge failed', 'woo-vipps') =>'recurring.charge-failed.v1'];
651
652 $qr_events = [__('User Checked in', 'woo-vipps')=> 'user.checked-in.v1'];
653
654
655 $defaultevents = ['epayments.payment.authorized.v1', 'epayments.payment.aborted.v1', 'epayments.payment.expired.v1', 'epayments.payment.terminated.v1'];
656
657
658 ?>
659
660 <dialog id='webhook_view_dialog' style='width:70%'>
661 <form method="dialog">
662 <div class='viewdata' style='margin-bottom: 3rem'>
663 <label>ID</label><span class='webhook_id'></span>
664 <label>URL</label><span class='webhook_url'></span>
665 <label>Events</label><div style='width:80%' class='webhook_events'></div>
666 </div>
667 <button class="button btn button-primary" type="submit" value="OK"><?php _e('OK'); ?></button>
668 </form>
669 </dialog>
670
671
672 <dialog id='webhook_add_dialog' style='width: 70%'>
673 <form method="dialog">
674 <h3><?php _e('Add a webhook', 'woo-vipps'); ?></h3>
675 <label for='dialog_webhook_msn'>MSN</label><input style='width: 50%' id='dialog_webhook_msn' required readonly type="text" name="webhook_msn" placeholder="">
676 <label for='dialog_webhook_url'>URL</label><input style='width: 50%' id='dialog_webhook_url' autofocus required type="url" name="webhook_url" placeholder="https://...">
677 <div class="events" style="margin-bottom: 2rem">
678 <h3>Epayment</h3>
679 <?php foreach($epayment_events as $label=>$event): ?>
680 <label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?>
681 <input <?php if (in_array($event, $defaultevents)) echo " checked " ?>
682 type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'>
683 </label>
684 <?php endforeach; ?>
685 <h3>Recurring</h3>
686 <?php foreach($recurring_events as $label=>$event): ?>
687 <label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?>
688 <input <?php if (in_array($event, $defaultevents)) echo " checked " ?>
689 type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'>
690 </label>
691 <?php endforeach; ?>
692 <h3>QR</h3>
693 <?php foreach($qr_events as $label=>$event): ?>
694 <label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?>
695 <input <?php if (in_array($event, $defaultevents)) echo " checked " ?>
696 type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'>
697 </label>
698 <?php endforeach; ?>
699
700 </div>
701 <div class='buttonholder'>
702 <button class="button btn button-primary" type="submit" value="OK"><?php _e('Add this URL as a webhook', 'woo-vipps'); ?></button>
703 <button class="button btn" type="submit" formnovalidate value="NO"><?php _e('No, forget it', 'woo-vipps'); ?></button>
704 </div>
705 </form>
706 </dialog>
707
708 <style>
709 dialog#webhook_add_dialog::backdrop {
710 background-color: rgba(0.9,0.9,0.9,0.7);
711 }
712 </style>
713
714 <script>
715 let dialog = document.getElementById('webhook_add_dialog');
716 let viewdialog = document.getElementById('webhook_view_dialog');
717 dialog.addEventListener('close', function () {
718 if (dialog.returnValue =='OK') {
719 let msn = dialog.querySelector('input[name="webhook_msn"]').value;
720 let url = dialog.querySelector('input[name="webhook_url"]').value;
721 dialog.querySelector('input[name="webhook_url"]').value = "";
722 dialog.querySelector('input[name="webhook_msn"]').value = "";
723
724 let events = dialog.querySelectorAll('input[name="webhook_event"]:checked');
725 let eventlist = [];
726 let eventstring = '';
727 for (const ev of events.values()) {
728 eventlist.push(ev.value);
729 }
730 eventstring = eventlist.join(',');
731
732
733 if (msn && url && eventstring) {
734 jQuery('#webhook_msn').val(msn);
735 jQuery('#webhook_post_action').val('vipps_add_webhook');
736 jQuery('#webhook_url').val(url);
737 jQuery('#webhook_events').val(eventstring);
738 let f = jQuery('#webhook_action_form');
739 f.submit();
740 }
741 }
742 dialog.querySelector('input[name="webhook_url"]').value = "";
743 dialog.querySelector('input[name="webhook_msn"]').value = "";
744 });
745
746 let data = "";
747 jQuery('a.webhook-viewer').click(function (e) {
748 e.preventDefault();
749 let row= jQuery(this).closest('tr');
750 data = row.data('hookdata');
751 viewdialog.querySelector('.viewdata').querySelector('.webhook_id').innerHTML= data['id'];
752 viewdialog.querySelector('.viewdata').querySelector('.webhook_url').innerHTML= data['url'];
753 viewdialog.querySelector('.viewdata').querySelector('.webhook_events').innerHTML= data['events'].join(" ");
754 viewdialog.showModal();
755 });
756
757
758 jQuery('a.webhook-deleter').click(function (e) {
759 e.preventDefault();
760 let row = jQuery(this).closest('tr');
761 let wh = row.data('webhook-id');
762 let msn = row.data('msn');
763 let f = jQuery('#webhook_action_form');
764 jQuery('#webhook_id').val(wh);
765 jQuery('#webhook_msn').val(msn);
766 jQuery('#webhook_post_action').val('vipps_delete_webhook');
767 f.submit();
768 });
769
770 jQuery('a.webhook-adder').click(function (e) {
771 e.preventDefault();
772 let msn = jQuery(this).data('msn');
773 dialog.querySelector('input[name="webhook_url"]').value = "";
774 dialog.querySelector('input[name="webhook_msn"]').value = msn;
775 dialog.showModal();
776 });
777
778 </script>
779
780 <?php
781
782
783 echo "</div>";
784 }
785
786 // To be called in admin-post.php
787 public function vipps_delete_webhook() {
788 static::set_locale_if_in_header();
789 $ok = wp_verify_nonce($_REQUEST['webhook_nonce'],'webhook_nonce');
790 if (!$ok) {
791 wp_die("Wrong nonce");
792 }
793 if (!current_user_can('manage_woocommerce')) {
794 wp_die(__('You don\'t have sufficient rights', 'woo-vipps'));
795 }
796
797 $msn = sanitize_title($_REQUEST['webhook_msn']);
798 $id = sanitize_title($_REQUEST['webhook_id']);
799
800 if ($msn && $id) {
801 $this->gateway()->api->delete_webhook($msn, $id);
802 }
803
804 wp_safe_redirect(admin_url("admin.php?page=vipps_webhook_menu"));
805 exit();
806 }
807
808 // To be called in admin-post.php
809 public function vipps_add_webhook() {
810 static::set_locale_if_in_header();
811 $ok = wp_verify_nonce($_REQUEST['webhook_nonce'],'webhook_nonce');
812 if (!$ok) {
813 wp_die("Wrong nonce");
814 }
815 if (!current_user_can('manage_woocommerce')) {
816 wp_die(__('You don\'t have sufficient rights', 'woo-vipps'));
817 }
818
819 $msn = sanitize_title($_REQUEST['webhook_msn']);
820 $url = sanitize_url($_REQUEST['webhook_url']);
821 $events = [];
822 foreach(explode(",", $_REQUEST['webhook_events']) as $event) {
823 $events[] = $event;
824 }
825 if (!empty($events) && $msn && $url) {
826 $this->gateway()->api->register_webhook($msn, $url, $events);
827 }
828
829 wp_safe_redirect(admin_url("admin.php?page=vipps_webhook_menu"));
830 exit();
831 }
832
833 public function badge_menu_page () {
834 if (!current_user_can('manage_woocommerce')) {
835 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
836 }
837 wp_enqueue_script('vipps-onsite-messageing');
838
839 $badge_options = get_option('vipps_badge_options');
840
841 // Get current brand and language
842 $current_brand = strtolower($this->get_payment_method_name());
843 $current_language = $this->get_customer_language();
844 if ('se' === $current_language) $current_language = 'sv';
845 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
846 if ('dk' === $current_language) $current_language = 'da';
847
848 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
849 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
850 'purple'=> __('Purple', 'woo-vipps')];
851
852 ?>
853 <div class='wrap vipps-badge-settings'>
854
855 <h1><?php echo sprintf(__('%1$s On-Site Messaging', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
856
857 <h3><?php echo sprintf(__('%1$s On-Site Messaging contains <em>badges</em> in different variants that can be used to let your customers know that %1$s payment is accepted.', 'woo-vipps'), Vipps::CompanyName()); ?></h3>
858
859 <p>
860 <?php _e('You can configure these badges on this page, turning them on in all or some products and configure their default setup. You can also add a badge using a shortcode or a Block', 'woo-vipps'); ?>
861 </p>
862
863 <h2> <?php _e('Settings', 'woo-vipps'); ?></h2>
864 <form class="vipps-badge-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
865 <input type="hidden" name="action" value="update_vipps_badge_settings" />
866 <?php wp_nonce_field( 'badgeaction', 'badgenonce'); ?>
867 <div>
868 <label for="badgeon"><?php echo sprintf(__('Turn on support for %1$s On-site Messaging badges', 'woo-vipps'), Vipps::CompanyName()); ?></label>
869 <input type="hidden" name="badgeon" value="0" />
870 <input <?php if (@$badge_options['badgeon']) echo " checked "; ?> value="1" type="checkbox" id="badgeon" name="badgeon" />
871 </div>
872
873 <div>
874 <label for="defaultall"><?php _e('Add badge to all products by default', 'woo-vipps'); ?></label>
875 <input type="hidden" name="defaultall" value="0" />
876 <input <?php if (@$badge_options['defaultall']) echo " checked "; ?> value="1" type="checkbox" id="defaultall" name="defaultall" />
877 <p><?php echo sprintf(__("If selected, all products will get a badge, but you can override this on the %1\$s tab on the product data page. If not, it's the other way around. You can also choose a particular variant on that page", 'woo-vipps'), Vipps::CompanyName()); ?></p>
878 </div>
879 <p id="badgeholder" style="font-size:1.5rem">
880 <vipps-mobilepay-badge id="vipps-badge-demo"
881 brand="<?php echo esc_attr($current_brand); ?>"
882 language="<?php echo esc_attr($current_language); ?>"
883 <?php if (@$badge_options['variant']) echo ' variant="' . esc_attr($badge_options['variant']) . '" ' ?>
884 ></vipps-mobilepay-badge>
885 </p>
886
887 <div>
888 <label for="vippsBadgeVariant"><?php _e('Variant', 'woo-vipps'); ?></label>
889
890 <select id=vippsBadgeVariant name="variant" onChange='changeVariant()'>
891 <option value=""><?php _e('Choose color variant:', 'woo-vipps'); ?></option>
892 <?php foreach($variants as $key=>$name): ?>
893 <option value="<?php echo $key; ?>" <?php if (@$badge_options['variant'] == $key) echo " selected "; ?> >
894 <?php echo $name ; ?>
895 </option>
896 <?php endforeach; ?>
897 </select>
898
899 <div>
900 <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
901 </div>
902
903 </form>
904
905 <h2><?php _e('The Gutenberg Block', 'woo-vipps'); ?></h2>
906 <p><?php echo sprintf(__('If you use Gutenberg, you should be able to add a %1$s Badge block wherever you need it. It is called %1$s On-Site Messaging Badge Block.', 'woo-vipps'), Vipps::CompanyName()); ?>
907
908 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
909 <p><?php echo sprintf(__('If you need to add a %1$s badge on a specific page, footer, header and so on, and you cannot use the Gutenberg Block provided for this, you can either add the %1$s Badge manually (as <a href="%2$s" nofollow rel=nofollow target=_blank>documented here</a>) or you can use the shortcode.', 'woo-vipps'), Vipps::CompanyName(), "https://developer.vippsmobilepay.com/docs/knowledge-base/design-guidelines/on-site-messaging/"); ?></p>
910 <br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br>
911 <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
912 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
913 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
914 </p>
915
916 </div>
917 <script>
918 function changeVariant() {
919 const badge = document.getElementById('vipps-badge-demo');
920 const variantSelector = document.getElementById('vippsBadgeVariant');
921 const variant = variantSelector.options[variantSelector.selectedIndex].value;
922
923 // Just update the variant attribute, preserving brand and language
924 badge.setAttribute('variant', variant);
925 }
926 </script>
927 <?php
928 }
929
930 public function update_button_settings () {
931 $ok = wp_verify_nonce($_REQUEST['buttonnonce'],'buttonaction');
932 if (!$ok) {
933 wp_die("Wrong nonce");
934 }
935 if (!current_user_can('manage_woocommerce')) {
936 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
937 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
938 }
939
940 $old = get_option('vipps_button_options2', []);
941 $new = $old;
942 if (isset($_POST['express']['configs'])) {
943 foreach ($_POST['express']['configs'] as $ctx => $config) {
944 $sanitized_ctx = sanitize_title($ctx);
945 $sanitized_config = map_deep($config, 'sanitize_title');
946
947 // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
948 if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
949 $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
950 } else {
951 $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
952 }
953 }
954 }
955 update_option('vipps_button_options2', $new);
956 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
957 exit();
958 }
959
960 public function update_badge_settings () {
961 static::set_locale_if_in_header();
962 $ok = wp_verify_nonce($_REQUEST['badgenonce'],'badgeaction');
963 if (!$ok) {
964 wp_die("Wrong nonce");
965 }
966 if (!current_user_can('manage_woocommerce')) {
967 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
968 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
969 }
970
971 $current = get_option('vipps_badge_options');
972 if (isset($_POST['badgeon'])) {
973 $current['badgeon'] = intval($_POST['badgeon']);
974 }
975 if (isset($_POST['defaultall'])) {
976 $current['defaultall'] = intval($_POST['defaultall']);
977 }
978 if (isset($_POST['variant'])) {
979 $current['variant'] = sanitize_title($_POST['variant']);
980 }
981
982 update_option('vipps_badge_options', $current);
983 wp_safe_redirect(admin_url("admin.php?page=vipps_badge_menu"));
984 exit();
985 }
986
987 public function vipps_mobilepay_badge_shortcode($atts) {
988 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
989
990 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : "";
991 $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
992 if ('se' === $language) $language = 'sv';
993 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
994 if ('dk' === $language) $language = 'da';
995 $id = sanitize_title($args['id']);
996 $class = sanitize_text_field($args['class']);
997
998 $attributes = [];
999 $attributes['brand'] = strtolower($this->get_payment_method_name());
1000 if ($variant) $attributes['variant'] = $variant;
1001 if ($language) $attributes['language'] = $language;
1002 if ($id) $attributes['id'] = $id;
1003 if ($class) $attributes['class'] = $class;
1004
1005 $badgeatts = "";
1006 foreach($attributes as $key=>$value) $badgeatts .= " $key=\"" . esc_attr($value) . '"';
1007
1008 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
1009 }
1010
1011 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1012 // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
1013 public function vipps_badge_shortcode($atts) {
1014 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
1015
1016 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : "";
1017 $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1018 if ('se' === $language) $language = 'sv';
1019 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1020 if ('dk' === $language) $language = 'da';
1021
1022 $id = sanitize_title($args['id']);
1023 $class = sanitize_text_field($args['class']);
1024
1025 $attributes = [];
1026 if ($variant) $attributes['variant'] = $variant;
1027 if ($language) $attributes['language'] = $language;
1028 if ($id) $attributes['id'] = $id;
1029 if ($class) $attributes['class'] = $class;
1030
1031 $badgeatts = "";
1032 foreach($attributes as $key=>$value) $badgeatts .= " $key=\"" . esc_attr($value) . '"';
1033
1034 return "<vipps-badge $badgeatts></vipps-badge>";
1035 }
1036
1037 public function get_html_button_default_attrs() {
1038 return [
1039 'language' => 'store',
1040 'variant' => 'primary',
1041 'rounded' => 'false',
1042 'verb' => 'buy',
1043 'stretched' => 'false',
1044 'compact' => 'false',
1045 'brand' => strtolower($this->get_payment_method_name()), // NB: if setting wp option, you need to remember to unset this value so it's dynamic. LP 2026-07-01
1046 ];
1047 }
1048
1049 public function get_html_button_attrs_for_context($context = 'global') {
1050 $options = get_option('vipps_button_options2', []);
1051 if (!is_string($context)) $context = 'global';
1052
1053 // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1054 $gutenberg = false;
1055 if ($context == 'checkout_gutenberg') {
1056 $context = 'checkout';
1057 $gutenberg = true;
1058 }
1059 if ($context == 'cart_gutenberg') {
1060 $context = 'cart';
1061 $gutenberg = true;
1062 }
1063
1064 $config = $options['express']['configs'][$context] ?? [];
1065 $use_global = !$config || ($config['use-global-config'] ?? false);
1066 if ($use_global) {
1067 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
1068 }
1069
1070 // see above.
1071 if ($gutenberg) {
1072 $config['stretched']='true';
1073 }
1074 return $config;
1075 }
1076
1077 public function get_html_button_for_context($context = 'global') {
1078 return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1079 }
1080
1081 // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1082 // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1083 public function get_html_button($attrs = []) {
1084 $payment_method = $this->get_payment_method_name();
1085 $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1086 $attrs['brand'] = strtolower($payment_method);
1087 $attrs['type'] = 'button'; // static
1088
1089 // Support using store language
1090 if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1091 // Don't support these login verbs. LP 2026-06-04
1092 if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1093 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1094 if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1095 // Fix swedish too. LP 2026-10-06
1096 if ('se' === $attrs['language']) $attrs['language'] = 'sv';
1097
1098 $escaped_attrs = [];
1099 foreach($attrs as $k => $v) {
1100 $escaped_attrs[$k] = esc_attr($v);
1101 }
1102
1103 // id attribute
1104 $id = $escaped_attrs['id'] ?? '';
1105 $id_str = $id ? "id='$id'" : '';
1106
1107 // class attribute
1108 $class_str = '';
1109 if (isset($attrs['class'])) {
1110 if (is_array($attrs['class'])) {
1111 $class_str = implode(' ', $attrs['class']);
1112 } else if (is_string($attrs['class'])) {
1113 $class_str = $attrs['class'];
1114 }
1115 }
1116
1117 // The html
1118 $html = <<<EOF
1119 <vipps-mobilepay-button
1120 $id_str
1121 $class_str
1122 type="{$escaped_attrs['type']}"
1123 brand="{$escaped_attrs['brand']}"
1124 language="{$escaped_attrs['language']}"
1125 variant="{$escaped_attrs['variant']}"
1126 rounded="{$escaped_attrs['rounded']}"
1127 verb="{$escaped_attrs['verb']}"
1128 stretched="{$escaped_attrs['stretched']}"
1129 compact="{$escaped_attrs['compact']}"
1130 ></vipps-mobilepay-button>
1131 EOF;
1132 return apply_filters('woo_vipps_html_button', $html, $attrs);
1133 }
1134
1135 public function button_menu_page() {
1136 if (!current_user_can('manage_woocommerce')) {
1137 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1138 }
1139 wp_enqueue_script('vipps-button-webcomponent');
1140 ?>
1141 <div class='wrap vipps-button-settings'>
1142 <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1143 <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1144 <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1145 <input type="hidden" name="action" value="update_vipps_button_settings" />
1146 <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
1147
1148 <!-- Express section -->
1149 <?php $this->button_menu_express_section(); ?>
1150
1151 <!-- submit button -->
1152 <div id="vipps-button-settings-save">
1153 <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1154 </div>
1155 </form>
1156 </div>
1157 <?php
1158 }
1159
1160 private function button_menu_express_section() {
1161 $options = get_option('vipps_button_options2', []);
1162 $express = $options['express'] ?? [];
1163 $configs = $express['configs'] ?? [];
1164
1165
1166 $contexts = [
1167 'global' => __('Global', 'woo-vipps'),
1168 'product' => __('Product', 'woo-vipps'),
1169 'catalog' => __('Catalog', 'woo-vipps'),
1170 'cart' => __('Cart', 'woo-vipps'),
1171 'minicart' => __('Mini cart', 'woo-vipps'),
1172 'checkout' => __('Checkout', 'woo-vipps'),
1173 ];
1174 $init_context = 'global';
1175 $init_config = $configs[$init_context] ?? [];
1176
1177 // html button args
1178 $init_args = $init_config;
1179 $init_args['id'] = 'vipps-button-express-preview';
1180
1181 ?>
1182 <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1183 <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
1184
1185 <!-- Context dropdown -->
1186 <div id="vipps-button-settings-express-context">
1187 <label>
1188 <?php _e('Config context', 'woo-vipps'); ?>
1189 </label>
1190 <select id="context" onChange='updateContext()'>
1191 <?php foreach($contexts as $key => $label): ?>
1192 <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1193 <?php echo $label ; ?>
1194 </option>
1195 <?php endforeach; ?>
1196 </select>
1197 <label class="hidden" id="use-global-config-container"><input onchange="updateContext()" type="checkbox" name="express[tmpConfig][use-global-config]" checked><?php _e('Use global config', 'woo-vipps'); ?></label>
1198 </div>
1199
1200
1201 <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1202 On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1203 <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1204 <fieldset>
1205 <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1206 <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1207 <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1208 </fieldset>
1209 <fieldset>
1210 <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1211 <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1212 <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1213 <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1214 <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1215 <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1216 <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1217 <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1218 <?php endif; ?>
1219 </fieldset>
1220
1221 <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1222 <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1223 <?php endif; ?>
1224
1225 <fieldset>
1226 <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1227 <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1228 <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1229 <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1230 <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1231 <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1232 <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1233 </fieldset>
1234 <fieldset>
1235 <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1236 <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1237 <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1238 <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1239 </fieldset>
1240 </div>
1241
1242 <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1243 <?php echo $this->get_html_button($init_args); ?>
1244 </div>
1245
1246 <script>
1247 // When inputs change, update the preview args. LP 2026-06-24
1248 jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
1249
1250 let currentContext = '<?php echo $init_context; ?>';
1251 let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1252
1253 // Updates the actual html inputs from given config. LP 2026-07-01
1254 function setInputsFromConfig(context, config) {
1255 const useGlobalConfig = Boolean(config?.["use-global-config"]);
1256 const isGlobal = "global" === context;
1257
1258 // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1259 jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1260
1261 // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1262 if (!config || (!isGlobal && useGlobalConfig)) {
1263 config = contextConfigs["global"];
1264
1265 jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1266
1267 // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1268 jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1269 } else {
1270 jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1271 jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1272 }
1273
1274 Object.entries(config).forEach(([key, val]) => {
1275 if ("use-global-config" === key) return;
1276 const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1277 const type = inputs.prop('type');
1278 switch (type) {
1279 case "checkbox":
1280 inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1281 break;
1282 case "radio":
1283 inputs.filter(`[value="${val}"]`).prop('checked', true);
1284 break;
1285 default:
1286 console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1287 }
1288 });
1289
1290 updatePreview();
1291 }
1292 // init the starting config from option. LP 2026-06-25
1293 setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1294
1295 // Update the preview web component's attributes. LP 2026-06-24
1296 function updatePreview(event) {
1297 const args = getPreviewArgs();
1298 // FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1299 // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1300 if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1301 const button = jQuery('#vipps-button-express-preview');
1302 button.attr(args);
1303 }
1304
1305 function getPreviewArgs() {
1306 const args = {};
1307 jQuery('#vipps-button-settings-express-args input').each(function () {
1308 // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1309 const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1310 const attr = matches.length ? matches[matches.length - 1][1] : null;
1311 if (!attr) {
1312 console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1313 return;
1314 }
1315 if (this.type === 'checkbox') {
1316 args[attr] = this.checked;
1317 } else if (this.checked) {
1318 args[attr] = this.value;
1319 }
1320 });
1321
1322 return args;
1323 }
1324
1325 // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1326 function updateContext() {
1327 const wasGlobal = "global" === currentContext;
1328 const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1329
1330 // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1331 if (wasGlobal || !useGlobalConfig) {
1332 contextConfigs[currentContext] = getPreviewArgs();
1333 } else {
1334 contextConfigs[currentContext] = {'use-global-config': true};
1335 }
1336
1337 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1338 const newContext = jQuery("#context").val();
1339 const newConfig = contextConfigs[newContext];
1340
1341 setInputsFromConfig(newContext, newConfig);
1342 currentContext = newContext;
1343 }
1344
1345 // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1346 jQuery('#vipps-button-settings-form').on('formdata', e => {
1347 const formData = e?.originalEvent?.formData;
1348 if (!formData) return;
1349
1350 // run this to store current context config before posting. LP 2026-06-24
1351 updateContext();
1352
1353 // now we can delete the current temporary config from post data. LP 2026-06-24
1354 const keysToDelete = [];
1355 for (const [key] of formData.entries()) {
1356 if (key.startsWith("express[tmpConfig][")) {
1357 keysToDelete.push(key);
1358 }
1359 }
1360 keysToDelete.forEach(key => formData.delete(key));
1361
1362 // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1363 Object.entries(contextConfigs).forEach(([context, config]) => {
1364 Object.entries(config).forEach(([key, val]) => {
1365 formData.append(`express[configs][${context}][${key}]`, val);
1366 });
1367 });
1368 });
1369 </script>
1370 <?php
1371 }
1372
1373
1374 public function admin_menu_page () {
1375 $flavour = sanitize_title($this->get_payment_method_name());
1376
1377 // The function which is hooked in to handle the output of the page must check that the user has the required capability as well. (manage_woocommerce)
1378 if (!current_user_can('manage_woocommerce')) {
1379 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1380 }
1381
1382 $recurringsettings = admin_url('/admin.php?page=wc-settings&tab=checkout&section=vipps_recurring');
1383 $checkoutsettings = admin_url('/admin.php?page=vipps_settings_menu');
1384 $loginsettings = admin_url('options-general.php?page=vipps_login_settings');
1385
1386 $logininstall = admin_url('/plugin-install.php?s=login-with-vipps&tab=search&type=term');
1387 $subscriptioninstall = 'https://woocommerce.com/products/woocommerce-subscriptions/';
1388
1389 $logspage = admin_url('/admin.php?page=wc-status&tab=logs');
1390 $forumpage = 'https://wordpress.org/support/plugin/woo-vipps/';
1391
1392 $portalurl = 'https://portal.vippsmobilepay.com';
1393
1394 $installed = get_plugins();
1395
1396 $recurringinstalled = array_key_exists('vipps-recurring-payments-gateway-for-woocommerce/woo-vipps-recurring.php',$installed);
1397 $recurringactive = class_exists('WC_Vipps_Recurring');
1398 $recurringstandalone = $recurringactive && !(defined('WC_VIPPS_RECURRING_INTEGRATED') && WC_VIPPS_RECURRING_INTEGRATED);
1399 $deactivatelink = admin_url("plugins.php?s=vipps-recurring-payments-gateway-for-woocommerce");
1400
1401 $logininstalled = array_key_exists('login-with-vipps/login-with-vipps.php', $installed);
1402 $loginactive = class_exists('ContinueWithVipps');
1403 $slogan = __('- very, very simple', 'woo-vipps');
1404
1405
1406 $gw = $this->gateway();
1407 $configured = get_option('woo-vipps-configured', false);
1408 $isactive = ($gw->enabled == 'yes');
1409 $istestmode = $gw->is_test_mode();
1410 $ischeckout = false;
1411 if ($isactive) {
1412 $ischeckout = ($gw->get_option('vipps_checkout_enabled') == 'yes');
1413 }
1414
1415 if (WC_Gateway_Vipps::instance()->get_payment_method_name() != "Vipps"):
1416 ?>
1417 <style>.notice.notice-vipps.test-mode { display: none; }body.wp-admin.toplevel_page_vipps_admin_menu #wpcontent {background-color: white; }</style>
1418 <header class="vipps-admin-page-header <?php echo esc_attr($flavour); ?>" style="padding-top: 3.5rem ; line-height: 30px;">
1419 <h1><?php echo esc_html(Vipps::CompanyName()); ?> <?php echo esc_html($slogan); ?></h1>
1420 </header>
1421 <div class='wrap vipps-admin-page'>
1422 <div id="vipps_page_vipps_banners"><?php echo apply_filters('woo_vipps_vipps_page_banners', ""); ?></div>
1423 <h1><?php echo sprintf(__("%1\$s for WordPress and WooCommerce", 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1424 <div class="pluginsection woo-vipps">
1425
1426 <p><?php echo sprintf(__("This plugin gives you %1\$s in WooCommerce, either as a fully fledged Checkout, or as a flexible payment method.",'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p>
1427 <p><?php echo sprintf(__("With Checkout, you’ll also get access to shipping addresses, shipping selection and other payment options. Currently Checkout supports %1\$s and bank transfer; VISA and MasterCard payments will be added later.",'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p>
1428
1429 <p><strong><?php echo sprintf(__("NB! Checkout for MobilePay is currently in beta mode; Bank Transfer has limited availability", 'woo-vipps')); ?></strong></p>
1430
1431 <p><?php echo sprintf(__("Configure the plugin on its <a href='%1\$s'>settings page</a> and get your keys from the <a target='_blank' href='%2\$s'>%3\$s portal</a>.",'woo-vipps'), $checkoutsettings, $portalurl, Vipps::CompanyName());?></p>
1432 <p><?php echo sprintf(__("If you experience problems or unexpected results, please check the 'fatal-errors' and 'woo-vipps' logs at <a href='%1\$s'>WooCommerce logs page</a>.", 'woo-vipps'), $logspage); ?></p>
1433 <p><?php echo sprintf(__("If you need support, please use the <a href='%1\$s'>forum page</a> for the plugin. If you cannot post your question publicly, contact WP-Hosting directly at [email protected].", 'woo-vipps'), $forumpage); ?></p>
1434 <div class="pluginstatus vipps_admin_highlighted_section <?php echo esc_attr($flavour); ?>">
1435 <?php if ($istestmode): ?>
1436 <p><b>
1437 <?php echo sprintf(__('%1$s is currently in test mode - no real transactions will occur.', 'woo-vipps'), Vipps::CompanyName()); ?>
1438 </b></p>
1439 <?php endif; ?>
1440 <p>
1441 <?php if ($configured): ?>
1442 <?php echo sprintf(__("<a href='%1\$s'>%2\$s configuration</a> is complete.", 'woo-vipps'), $checkoutsettings, Vipps::CompanyName()); ?>
1443 <?php else: ?>
1444 <?php echo sprintf(__("%1\$s configuration is not yet complete - you must get your keys from the %1\$s portal and enter them on the <a href='%2\$s'>settings page</a>", 'woo-vipps'), Vipps::CompanyName(), $checkoutsettings); ?>
1445 <?php endif; ?>
1446 </p>
1447 <?php if ($isactive): ?>
1448 <p>
1449 <?php echo sprintf(__("The plugin is <b>active</b> - %1\$s is available as a payment method.", 'woo-vipps'), Vipps::CompanyName()); ?>
1450 <?php if ($ischeckout): ?>
1451 </p>
1452 <p>
1453 <?php echo sprintf(__("You are now using <b>%1\$s Checkout</b> instead of the standard WooCommerce Checkout page.", 'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?>
1454 <?php endif; ?>
1455 </p>
1456 <?php else:; ?>
1457 <?php endif; ?>
1458 </div>
1459
1460 </div>
1461 </div>
1462
1463 <?php else: ?>
1464
1465 <style>.notice.notice-vipps.test-mode { display: none; }body.wp-admin.toplevel_page_vipps_admin_menu #wpcontent {background-color: white; }</style>
1466 <header class="vipps-admin-page-header <?php echo esc_attr($flavour); ?>" style="padding-top: 3.5rem ; line-height: 30px;">
1467 <h1><?php echo esc_html(Vipps::CompanyName()); ?> <?php echo esc_html($slogan); ?></h1>
1468 </header>
1469 <div class='wrap vipps-admin-page'>
1470 <div id="vipps_page_vipps_banners"><?php echo apply_filters('woo_vipps_vipps_page_banners', ""); ?></div>
1471 <h1><?php echo sprintf(__("%1\$s for WordPress and WooCommerce", 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1472 <p><?php echo sprintf(__("%1\$s officially supports WordPress and WooCommerce with a family of plugins implementing a payment gateway for WooCommerce, a system for managing QR-codes that link to your products or landing pages, a plugin for recurring payments, and a system for passwordless logins.", 'woo-vipps'), Vipps::CompanyName());?></p>
1473 <p><?php echo sprintf(__("To order or configure your %1\$s account that powers these plugins, log onto <a target='_blank' href='%2\$s'>the %1\$s portal</a> and use the keys and data from that to set up your plugins as needed.", 'woo-vipps'), Vipps::CompanyName(), $portalurl); ?></p>
1474
1475 <h1><?php echo sprintf(__("The %1\$s plugins", 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1476 <div class="pluginsection woo-vipps">
1477 <h2><?php echo sprintf(__('Pay with %1$s for WooCommerce', 'woo-vipps' ), Vipps::CompanyName());?></h2>
1478 <p><?php echo sprintf(__("This plugin implements a %1\$s checkout solution for WooCommerce and an alternate %1\$s hosted checkout that supports both %2\$s and credit cards. It also supports %1\$s's QR-api for creating QR-codes to your landing pages or products.", 'woo-vipps'), Vipps::CompanyName(), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p>
1479 <p><?php echo sprintf(__("Configure the plugin on its <a href='%1\$s'>settings page</a> and get your keys from the <a target='_blank' href='%2\$s'>%3\$s portal</a>.",'woo-vipps'), $checkoutsettings, $portalurl, Vipps::CompanyName());?></p>
1480 <p><?php echo sprintf(__("If you experience problems or unexpected results, please check the 'fatal-errors' and 'woo-vipps' logs at <a href='%1\$s'>WooCommerce logs page</a>.", 'woo-vipps'), $logspage); ?></p>
1481 <p><?php echo sprintf(__("If you need support, please use the <a href='%1\$s'>forum page</a> for the plugin. If you cannot post your question publicly, contact WP-Hosting directly at [email protected].", 'woo-vipps'), $forumpage); ?></p>
1482 <div class="pluginstatus vipps_admin_highlighted_section">
1483 <?php if ($istestmode): ?>
1484 <p><b>
1485 <?php echo sprintf(__('%1$s is currently in test mode - no real transactions will occur.', 'woo-vipps'), Vipps::CompanyName()); ?>
1486 </b></p>
1487 <?php endif; ?>
1488 <p>
1489 <?php if ($configured): ?>
1490 <?php echo sprintf(__("<a href='%1\$s'>%2\$s configuration</a> is complete.", 'woo-vipps'), $checkoutsettings, Vipps::CompanyName()); ?>
1491 <?php else: ?>
1492 <?php echo sprintf(__("%1\$s configuration is not yet complete - you must get your keys from the %1\$s portal and enter them on the <a href='%2\$s'>settings page</a>", 'woo-vipps'), Vipps::CompanyName(), $checkoutsettings); ?>
1493 <?php endif; ?>
1494 </p>
1495 <?php if ($isactive): ?>
1496 <p>
1497 <?php echo sprintf(__("The plugin is <b>active</b> - %1\$s is available as a payment method.", 'woo-vipps'), Vipps::CompanyName()); ?>
1498 <?php if ($ischeckout): ?>
1499 </p>
1500 <p>
1501 <?php echo sprintf(__("You are now using <b>%1\$s Checkout</b> instead of the standard WooCommerce Checkout page.", 'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?>
1502 <?php endif; ?>
1503 </p>
1504 <?php else:; ?>
1505 <?php endif; ?>
1506 </div>
1507
1508 </div>
1509
1510 <div class="pluginsection vipps-recurring">
1511 <h2><?php echo sprintf(__( 'Recurring Payments with %1$s', 'woo-vipps' ), Vipps::CompanyName());?></h2>
1512 <p>
1513 <?php echo sprintf(__("%1\$s supports recurring payments through the plugin <a href='%2\$s' target='_blank'>WooCommerce Subscriptions</a>. This support is written and supported by <a href='%3\$s' target='_blank'>Everyday</a>, and is perfect for you if you run a web shop with subscription based services or other products that would benefit from subscriptions.", 'woo-vipps'), Vipps::CompanyName(), 'https://woocommerce.com/products/woocommerce-subscriptions/', Vipps::CompanyName(), 'https://everyday.no/'); ?>
1514 <?php do_action('vipps_page_vipps_recurring_payments_section'); ?>
1515 <div class="pluginstatus vipps_admin_highlighted_section">
1516 <?php if ($recurringactive): ?>
1517 <p>
1518 <?php echo sprintf(__("Support for recurring payments with %1\$s is <b>active</b>. You can configure the plugin at its <a href='%2\$s'>settings page</a>.", 'woo-vipps'), Vipps::CompanyName(), $recurringsettings); ?>
1519 </p>
1520 <?php endif; ?>
1521 <?php if (!$subscriptioninstall): ?>
1522 <p>
1523 <?php echo sprintf(__("This plugins support for recurring payments requires the plugin <a href='%1\$s' target='_blank'>WooCommerce Subscriptions</a>. You need to install and activate this first.", 'woo-vipps'), 'https://woocommerce.com/products/woocommerce-subscriptions/'); ?>
1524 </p>
1525 <?php endif; ?>
1526 <?php if ($recurringactive && $recurringstandalone): ?>
1527 <p>
1528 <?php echo sprintf(__("Your support for recurring payments with %1\$s uses the legacy stand-alone plugin. This is no longer required, and you should <b><a href='%2\$s'>deactivate</a></b> this plugin, since development on this will soon cease.", 'woo-vipps'), Vipps::CompanyName(), esc_attr($deactivatelink));?>
1529 </p>
1530
1531 <?php endif; ?>
1532
1533 </div>
1534
1535 </div>
1536
1537 <div class="pluginsection login-with-vipps">
1538 <h2><?php echo sprintf(__( '%1$s', 'woo-vipps' ), Vipps::LoginName());?></h2>
1539 <p><?php echo sprintf(__("<a href='%1\$s' target='_blank'>%3\$s</a> is a password-less solution that lets you or your customers to securely log into your site without having to remember passwords - you only need the %2\$s app. The plugin does not require WooCommerce, and it can be customized for many different usecases.", 'woo-vipps'), 'https://www.wordpress.org/plugins/login-with-vipps/',Vipps::CompanyName(), Vipps::LoginName()); ?></p>
1540 <p>
1541 <?php echo sprintf(__("Remember, you need to set up %3\$s at the <a target='_blank' href='%2\$s'>%1\$s Portal</a>, where you will find the keys you need and where you will have to register the <em>return url</em> you will find on the settings page.", 'woo-vipps'),Vipps::CompanyName(),$portalurl, Vipps::LoginName()); ?>
1542 </p>
1543
1544 <div class="pluginstatus vipps_admin_highlighted_section">
1545 <?php if ($loginactive): ?>
1546 <p>
1547 <?php echo sprintf(__("%1\$s is installed and active. You can configure the plugin at its <a href='%2\$s'>settings page</a>", 'woo-vipps'),Vipps::LoginName(), $loginsettings); ?>
1548 </p>
1549 <?php elseif ($logininstalled): ?>
1550 <p>
1551 <?php echo sprintf(__("%1\$s is installed, but not active. Activate it on the <a href='%2\$s'>plugins page</a>", 'woo-vipps'), Vipps::LoginName(), admin_url("/plugins.php")); ?>
1552 </p>
1553 <?php else: ?>
1554 <p>
1555 <?php echo sprintf(__("%1\$s is not installed. You can install it <a href='%2\$s'>here!</a>", 'woo-vipps'), Vipps::LoginName(), $logininstall); ?>
1556 </p>
1557 <?php endif; ?>
1558 </div>
1559
1560 </div>
1561
1562 </div>
1563
1564 <?php endif;
1565 }
1566
1567 // Add a link to the settings page from the plugin list
1568 public function plugin_action_links ($links) {
1569 $link = '<a href="'.esc_attr(admin_url('/admin.php?page=vipps_settings_menu')). '">'.__('Settings', 'woo-vipps').'</a>';
1570 array_unshift( $links, $link);
1571 return $links;
1572 }
1573
1574
1575 // Requested by Vipps: It is a feature of this plugin that a prefix is added to the order number, in order to make it possible to use several different stores
1576 // that may use the same ordre number ranges. The prefix used to be just "Woo" by default, but Vipps felt it would be easier to respond to support request by
1577 // (trying to) identify the store/site directly in the order prefix. So this does that: It creates a prefix "woo-" + 8 chars derived from the domain of the siteurl.
1578 // The result should be "woo-abcdefgh-" which should leave 18 digits for the actual order number. IOK 2020-05-19
1579 public function generate_order_prefix() {
1580 $parts = parse_url(site_url());
1581 if (!$parts) return 'Woo';
1582 $domain = explode(".", $parts['host'] ?? '');
1583 if (empty($domain)) return 'Woo';
1584 $first = strtolower($domain[0]);
1585 $second = isset($domain[1]) ? $domain[1] : '';
1586 $key = 'Woo';
1587 // Select first part of domain unless that has no content, otherwise second. Default to Woo again.
1588 if (in_array($first, array('www','test','dev','vdev')) && !empty($second)) {
1589 $key = $second;
1590 } else {
1591 $key = $first;
1592 }
1593 // Use only 8 chars for the site. Try to make it so by dropping vowels, if that doesn't succeed, just chop it.
1594 $key = $key;
1595 $key = sanitize_title($key);
1596 $len = strlen($key);
1597 if ($len <= 8) return "woo-$key-";
1598 $kzk = preg_replace("/[aeiouæøåüö]/i","",$key);
1599 if (strlen($kzk) <= 8) return "woo-$kzk-";
1600 return "woo-" . substr($key,0,8) . "-";
1601 }
1602
1603 // Add a backend notice to stand out a bit, using a Vipps logo and the Vipps color for info-level messages. IOK 2020-02-16
1604 public function add_vipps_admin_notice ($text, $type='info',$key='', $extraclasses='') {
1605 if ($key) {
1606 $dismissed = get_option('_vipps_dismissed_notices');
1607 if (isset($dismissed[$key])) return;
1608 }
1609 add_action('admin_notices', function() use ($text,$type, $key, $extraclasses) {
1610 $logo = plugins_url('img/vmp-logo.png',__FILE__);
1611 $message = "<img style='height:40px;float:left;' src='$logo' alt='Vipps-logo'> $text";
1612 echo "<div class='notice notice-vipps notice-$type $extraclasses is-dismissible' data-key='" . esc_attr($key) . "'><p>$message</p></div>";
1613 });
1614 }
1615
1616
1617 // This function will delete old orders that were cancelled before the Vipps action was completed. We keep them for
1618 // 10 minutes so we can work with them in hooks and callbacks after they are cancelled. IOK 2019-10-22
1619 # protected function delete_old_cancelled_orders() {
1620 public function delete_old_cancelled_orders() {
1621 $limit = 30;
1622 $cutoff = time() - 600; // Ten minutes old orders: Delete them
1623 $oldorders = time() - (60*60*24*7); // Very old orders: Ignore them to make this work on sites with enormous order databases
1624 $delenda = [];
1625
1626 if ($this->useHPOS()) {
1627 $args = array(
1628 'status' => 'cancelled',
1629 'limit' => $limit,
1630 'date_modified' => "$oldorders...$cutoff",
1631 'meta_query' => [[ 'key' => '_vipps_delendum', 'value' => 1 ]]
1632 );
1633 $delenda = wc_get_orders($args);
1634 } else {
1635 // Old-style orders, we'll just use SQL
1636 global $wpdb;
1637 $sql = $wpdb->prepare("SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm on (pm.post_id = p.ID AND pm.meta_key = '_vipps_delendum') WHERE p.post_type = 'shop_order' AND p.post_status = 'wc-cancelled' AND p.post_modified_gmt >= %s AND p.post_modified_gmt <= %s AND pm.meta_value = 1 LIMIT %d",
1638 gmdate( 'Y-m-d H:i:s', $oldorders ),
1639 gmdate( 'Y-m-d H:i:s', $cutoff ),
1640 $limit
1641 );
1642 $order_ids = $wpdb->get_col($sql);
1643 foreach($order_ids as $did) {
1644 $d = wc_get_order($did);
1645 if ($d && !is_wp_error($d)) {
1646 $delenda[] = $d;
1647 }
1648 }
1649 }
1650
1651 foreach ($delenda as $del) {
1652 // Delete only if there is no customer info for the order IOK 2022-10-12
1653 if (!$del->get_billing_email()) {
1654 $del->delete(true);
1655 } else {
1656 // If we've gotten a billing email, don't delete this. IOK 2022-10-12
1657 $del->delete_meta_data('_vipps_delendum');
1658 }
1659 }
1660 }
1661
1662 // This is called asynch/nonblocking on payment_complete
1663 public function do_order_management() {
1664 $orderid = isset($_POST['orderid']) ? $_POST['orderid'] : false;
1665 $orderkey = isset($_POST['orderkey']) ? $_POST['orderkey'] : false;
1666 if ($orderid && $orderkey) {
1667 // This will keep running even if the request ends, and this method is called asynchrounously.
1668 add_action('shutdown', function () use ($orderid, $orderkey) { WC_Gateway_Vipps::instance()->payment_complete_at_shutdown ($orderid, $orderkey); });
1669 http_response_code(200);
1670 header('Content-Type: application/json; charset=utf-8');
1671 header("Content-length: 1");
1672 print "1";
1673 flush();
1674 } else {
1675 http_response_code(403);
1676 }
1677 }
1678
1679
1680 public function admin_head() {
1681 // Add some styling to the Vipps product-meta box
1682 $smile= plugins_url('img/vmp-logo.png',__FILE__);
1683 ?>
1684 <style>
1685 @media only screen and (max-width: 900px) {
1686 #woocommerce-product-data ul.wc-tabs li.vipps_tab a:before {
1687 background: url(<?php echo $smile ?>) center center no-repeat;
1688 content: " " !important;
1689 background-size: 20px 20px;
1690 }
1691 }
1692 @media only screen and (min-width: 900px) {
1693 #woocommerce-product-data ul.wc-tabs li.vipps_tab a:before {
1694 background: url(<?php echo $smile ?>) center center no-repeat;
1695 content: " " !important;
1696 background-size:100%;
1697 width:13px;height:13px;display:inline-block;line-height:1;
1698 }
1699 }
1700 </style>
1701 <?php
1702 }
1703 // Scripts used in the backend
1704 public function admin_enqueue_scripts($hook) {
1705
1706 wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1707 $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1708 wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1709 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1710 $this->script_add_vippslocale('vipps-admin');
1711 wp_enqueue_script('vipps-admin');
1712
1713 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1714 wp_enqueue_style('vipps-fonts');
1715 wp_enqueue_style('vipps-fonts',plugins_url('css/fonts.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/fonts.css"), 'all');
1716 }
1717
1718
1719 public function admin_menu () {
1720 // IOK 2023-12-01 replace old Vipps smile in larger contexts
1721 // $logo= plugins_url('img/vipps-smile-orange.png',__FILE__);
1722 $logo = plugins_url('img/vmp-logo.png', __FILE__);
1723 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
1724 $adminSettings = VippsAdminSettings::instance();
1725
1726 add_menu_page(sprintf(__("%1\$s", 'woo-vipps'), Vipps::CompanyName()), sprintf(__("%1\$s", 'woo-vipps'), Vipps::CompanyName()), 'manage_woocommerce', 'vipps_admin_menu', array($this, 'admin_menu_page'), $logo, 58);
1727
1728 add_submenu_page( 'vipps_admin_menu', __('Settings', 'woo-vipps'), __('Settings', 'woo-vipps'), 'manage_woocommerce', 'vipps_settings_menu', array($adminSettings, 'init_admin_settings_page_react_ui'), 90);
1729
1730 if (class_exists('WC_Vipps_Recurring') && class_exists('WC_Subscriptions_Plugin')) {
1731 add_submenu_page( 'vipps_admin_menu', __('Recurring Payments', 'woo-vipps'), __('Recurring Payments', 'woo-vipps'), 'manage_woocommerce', 'vipps_recurring__settings_menu', array($this, 'recurring_settings_page'), 95);
1732 }
1733
1734 add_submenu_page( 'vipps_admin_menu', __('Badges', 'woo-vipps'), __('Badges', 'woo-vipps'), 'manage_woocommerce', 'vipps_badge_menu', array($this, 'badge_menu_page'), 90);
1735 add_submenu_page( 'vipps_admin_menu', __('Buttons', 'woo-vipps'), __('Buttons', 'woo-vipps'), 'manage_woocommerce', 'vipps_button_menu', array($this, 'button_menu_page'), 80);
1736 add_submenu_page( 'vipps_admin_menu', __('Webhooks', 'woo-vipps'), __('Webhooks', 'woo-vipps'), 'manage_woocommerce', 'vipps_webhook_menu', array($this, 'webhook_menu_page'), 10);
1737 }
1738
1739 // Just a redirect to the recurring payment settings for the time being. IOK 2025-01-08
1740 public function recurring_settings_page () {
1741 if (class_exists('WC_Vipps_Recurring') && class_exists('WC_Subscriptions_Plugin')) {
1742 wp_safe_redirect(admin_url('/admin.php?page=wc-settings&tab=checkout&section=vipps_recurring'), 302);
1743 } else {
1744 wp_safe_redirect(admin_url('/admin.php?page=vipps_admin_menu'), 302);
1745 }
1746 exit();
1747 }
1748
1749 public function add_meta_boxes () {
1750 $screen = 'shop_order';
1751 $useHPOS = $this->useHPOS();
1752
1753 if ($useHPOS && function_exists('wc_get_page_screen_id')) {
1754 $screen = wc_get_page_screen_id('shop-order');
1755 }
1756
1757 $vippsorder = false;
1758 $order = null;
1759 global $post;
1760 if ($post && $post->post_type == 'shop_order') {
1761 $order = wc_get_order($post);
1762 } else {
1763 // New style HPOS order table doesn't let us inspect the order, so we must fetch it from query args
1764 $screen = get_current_screen();
1765 if ($screen && $screen->id == 'woocommerce_page_wc-orders') {
1766 $orderid = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
1767 $order = wc_get_order($orderid);
1768 }
1769 }
1770 if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
1771 $vippsorder = true;
1772 }
1773
1774 if ($vippsorder) {
1775 add_meta_box( 'vippsdata', sprintf(__('%1$s','woo-vipps'), $this->get_payment_method_name()), array($this,'add_vipps_metabox'), $screen, 'side', 'core' );
1776 }
1777 }
1778
1779 public function wp_register_scripts () {
1780 // We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues
1781 // (which it only is if Gutenberg is active) or not provided at all, add it now.
1782 wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks', 'wp-api-fetch','vipps-widget-sdk'),filemtime(dirname(__FILE__) . "/js/vipps.js"), true);
1783
1784 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1785 wp_register_script('vipps-onsite-messageing',
1786 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1787 array(),
1788 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1789 [
1790 'in_footer' => true,
1791 'strategy' => 'async',
1792 ],
1793 );
1794
1795 add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1796 if ($handle == 'vipps-widget-sdk') {
1797 $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1798 return $tag;
1799 }
1800 return $tag;
1801 },10,3);
1802
1803 wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1804 array('vipps-button-webcomponent'),
1805 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1806 ['in_footer' => true]
1807 );
1808
1809 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1810 wp_register_script('vipps-button-webcomponent',
1811 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1812 array(),
1813 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1814 [
1815 'in_footer' => false
1816 ]
1817 );
1818 }
1819
1820 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1821 public function script_add_vippslocale ($handle) {
1822 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1823 $name = $this->get_payment_method_name();
1824 $strings = array(
1825 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1826 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1827 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1828 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1829 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1830 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1831 'cancel'=> __("Cancel", 'woo-vipps'),
1832 'close'=> __("Close", 'woo-vipps'),
1833 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1834 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1835 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1836 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1837 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1838 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1839 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1840 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1841 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1842 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1843 );
1844 wp_localize_script($handle, 'VippsLocale', $strings);
1845 }
1846
1847 public function wp_enqueue_scripts() {
1848 // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1849 $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1850 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1851 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1852 $this->script_add_vippslocale('vipps-gw');
1853
1854 wp_enqueue_script('vipps-gw');
1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1856 wp_enqueue_script('vipps-button-webcomponent');
1857 }
1858
1859 // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1860 // the pay-for-order screen. IOK 2026-09-15
1861 public function enqueue_classic_checkout_scripts () {
1862 if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1863 return;
1864 }
1865 // Order-pay is rendered by the classic form even with a Blocks checkout page.
1866 // It must bypass the check for the parent checkout page's block content.
1867 if ( ! is_checkout_pay_page() ) {
1868 $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1869 $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1870 ? $utils::is_checkout_block_default()
1871 : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1872
1873 if ( $uses_checkout_block ) {
1874 return;
1875 }
1876 }
1877
1878 // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1879 $relative_path = 'js/vipps-classic-checkout.js';
1880 wp_enqueue_script(
1881 'vipps-classic-checkout',
1882 plugins_url( $relative_path, __FILE__ ),
1883 array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1884 filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1885 true
1886 );
1887
1888 if ( is_checkout_pay_page() ) {
1889 $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1890 wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1891 'orderId' => $order ? $order->get_id() : 0,
1892 'orderKey' => $order ? $order->get_order_key() : '',
1893 'billingEmail' => $order ? $order->get_billing_email() : '',
1894 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1895 'nonce' => wp_create_nonce( 'wc_store_api' ),
1896 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1897 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1898 ) ) . ';', 'before' );
1899 }
1900 }
1901
1902
1903 public function add_shortcodes() {
1904 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1905 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1906
1907 // Badges, if using shortcodes
1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1910 // Legacy vipps-badge shortcode. LP 19.11.2024
1911 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1912
1913 // special page handling, previously a fake page. LP 2026-08-25
1914 add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1915 }
1916
1917
1918 public function log ($what,$type='info') {
1919 $logger = function_exists('wc_get_logger') ? wc_get_logger() : false;
1920 if ($logger) {
1921 $context = array('source'=>'woo-vipps');
1922 $logger->log($type,$what,$context);
1923 } else {
1924 error_log("woo-vipps ($type): $what");
1925 }
1926 }
1927
1928
1929 // If we have admin-notices that we haven't gotten a chance to show because of
1930 // a redirect, this method will fetch and show them IOK 2018-05-07
1931 public function stored_admin_notices() {
1932 $stored = get_transient('_vipps_save_admin_notices');
1933 if ($stored) {
1934 delete_transient('_vipps_save_admin_notices');
1935 print $stored;
1936 }
1937 do_action('vipps_admin_notices');
1938 }
1939
1940 // Show express button option on checkout form. LP 2026-03-23
1941 public function checkout_before_customer_details_express () {
1942 if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1943 do_action('woo_vipps_checkout_before_customer_details_express');
1944 $gw = $this->gateway();
1945 if (!$gw->show_express_checkout()) return;
1946 $this->express_checkout_section_html();
1947 }
1948
1949 public function express_checkout_section_html() {
1950 $payment_method = $this->get_payment_method_name();
1951 $header_text = __('Express Checkout', 'woo-vipps');
1952 $header = "<legend class='express-header'>$header_text</legend>";
1953 $div_classes = "legacy-checkout express $payment_method";
1954 echo "<fieldset class='$div_classes'>$header";
1955 $this->checkout_express_checkout_button_html();
1956 echo '</fieldset>';
1957 }
1958
1959 // Show the express button if reasonable to do so
1960 public function cart_express_checkout_button() {
1961 $gw = $this->gateway();
1962
1963 if ($gw->show_express_checkout()){
1964 return $this->cart_express_checkout_button_html();
1965 }
1966 }
1967
1968 public function minicart_express_checkout_button() {
1969 $gw = $this->gateway();
1970
1971 if ($gw->show_express_checkout()){
1972 return $this->cart_express_checkout_button_html('minicart');
1973 }
1974 }
1975
1976 // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1977 // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1978 public function add_checkout_button_for_classic () {
1979 $button = $this->get_html_button_for_context('checkout');
1980 $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1981 echo $submit;
1982 }
1983
1984 public function cart_express_checkout_button_html($context= 'cart') {
1985 $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1986 $method = $this->get_payment_method_name();
1987 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1988 $url = "#";
1989 $sec = wp_create_nonce('express');
1990 $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1991 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1992 echo $html;
1993 }
1994
1995 public function checkout_express_checkout_button_html() {
1996 $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1997 $method = $this->get_payment_method_name();
1998 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1999 $url = "#";
2000 $sec = wp_create_nonce('express');
2001 $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
2002 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
2003 echo $html;
2004 }
2005
2006 // A shortcode for a single buy now button. Express checkout must be active; but I don't check for this here, as this button may be
2007 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
2008 public function buy_now_button_shortcode ($atts) {
2009 // The new web component button args. LP 2026-07-02
2010 $button_args = $this->get_html_button_default_attrs();
2011 unset($button_args['brand']);
2012
2013 // Variant exists for the product variant. LP 2026-07-02
2014 if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
2015 unset($button_args['variant']);
2016
2017 $args = shortcode_atts(
2018 array(...$button_args,
2019 'id' => '','variant'=> '','sku' => '',
2020 ),
2021 $atts,
2022 );
2023
2024 // Variant exists for the product variant. LP 2026-07-02
2025 $button_args = $args;
2026 if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
2027 unset($button_args['button_variant']);
2028 unset($button_args['sku']);
2029 unset($button_args['id']);
2030 // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
2031 // "Note also that there is a bug in the library, and it currently only renders one language per page."
2032 // it seems like get_html_button() runs once before this shortcode code (whic gets default attrs including language), so I think this is why language bug appears. LP 2026-07-02
2033
2034 return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
2035 }
2036
2037 // The express checkout shortcode implementation. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
2038 public function express_checkout_button_shortcode() {
2039 $gw = $this->gateway();
2040 if (!$gw->cart_supports_express_checkout()) return;
2041 ob_start();
2042 $this->cart_express_checkout_button_html('cart');
2043 return ob_get_clean();
2044 }
2045
2046 // Manage the various product meta fields
2047 public function process_product_meta ($id, $post) {
2048 // This is for the 'buy now' button
2049 if (isset($_POST['woo_vipps_add_buy_now_button'])) {
2050 update_post_meta($id, '_vipps_buy_now_button', sanitize_text_field($_POST['woo_vipps_add_buy_now_button']));
2051 }
2052 // This is for overriding Vipps Badge settings
2053 if (isset($_POST['woo_vipps_show_badge'])) {
2054 update_post_meta($id, '_vipps_show_badge', sanitize_text_field($_POST['woo_vipps_show_badge']));
2055 }
2056
2057 // This is for the shareable links.
2058 if (isset($_POST['woo_vipps_shareable_delenda'])) {
2059 $delenda = array_map('sanitize_text_field',$_POST['woo_vipps_shareable_delenda']);
2060 foreach($delenda as $delendum) {
2061 // This will delete the actual link
2062 delete_post_meta($post->ID, '_vipps_shareable_link_'.$delendum);
2063 }
2064 // Delete all legacy "shareable links" collections. IOK 2024-06-19
2065 delete_post_meta($post->ID, '_vipps_shareable_links');
2066 }
2067 }
2068
2069 // An extra product meta tab for Vipps
2070 public function woocommerce_product_data_tabs ($tabs) {
2071 $img = plugins_url('img/vipps_logo.png',__FILE__);
2072 $tabs['vipps'] = array( 'label' => sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()), 'priority'=>100, 'target'=>'woo-vipps', 'class'=>array());
2073 return $tabs;
2074 }
2075 public function woocommerce_product_data_panels() {
2076 global $post;
2077 echo "<div id='woo-vipps' class='panel woocommerce_options_panel'>";
2078 // IOK 2024-01-17 Temporary: Only Vipps supports express checkout, shareable links (express checkout) and badges
2079 // IOK 2025-09-01 Now available for all
2080 $this->product_options_vipps();
2081 $this->product_options_vipps_badges();
2082 $this->product_options_vipps_shareable_link();
2083 echo "</div>";
2084 }
2085 // Product data specific to Vipps - mostly the use of the 'Buy now!' button
2086 public function product_options_vipps() {
2087 $gw = $this->gateway();
2088 $choice = $gw->get_option('singleproductexpress');
2089 echo '<div class="options_group">';
2090 echo "<div class='blurb' style='margin-left:13px'><h4>";
2091 echo __("Buy-now button", 'woo-vipps') ;
2092 echo "<h4></div>";
2093 if ($choice == 'some') {
2094 $button = sanitize_text_field(get_post_meta( get_the_ID(), '_vipps_buy_now_button', true));
2095 echo "<input type='hidden' name='woo_vipps_add_buy_now_button' value='no' />";
2096 woocommerce_wp_checkbox( array(
2097 'id' => 'woo_vipps_add_buy_now_button',
2098 'value' => $button,
2099 'label' => sprintf(__('Add \'Buy now with %1$s\' button', 'woo-vipps'), $this->get_payment_method_name()),
2100 'desc_tip' => true,
2101 'description' => sprintf(__('Add a \'Buy now with %1$s\'-button to this product','woo-vipps'), $this->get_payment_method_name())
2102 ) );
2103 } else if ($choice == "all") {
2104 $prod = wc_get_product(get_the_ID());
2105 $canbebought = false;
2106 if (is_a($prod, 'WC_Product')) {
2107 $canbebought = $gw->product_supports_express_checkout(wc_get_product(get_the_ID()));
2108 }
2109
2110 echo "<p>";
2111 echo sprintf(__("The %1\$s settings are currently set up so all products that can be bought with Express Checkout will have a Buy Now button.", 'woo-vipps'), Vipps::CompanyName());
2112 echo " ";
2113 if ($canbebought) {
2114 echo __("This product supports express checkout, and so will have a Buy Now button." , 'woo-vipps');
2115 } else {
2116 echo __("This product does <b>not</b> support express checkout, and so will <b>not</b> have a Buy Now button." , 'woo-vipps');
2117 }
2118 echo "</p>";
2119 } else {
2120 $settings = esc_attr(admin_url('/admin.php?page=vipps_settings_menu'));
2121 echo "<p>";
2122 echo sprintf(__("The %1\$s settings</a> are configured so that no products will have a Buy Now button - including this.", 'woo-vipps'), Vipps::CompanyName());
2123 echo "</p>";
2124 }
2125 echo '</div>';
2126 }
2127
2128 public function product_options_vipps_badges() {
2129 $current = get_option('vipps_badge_options');
2130 if (!$current || !($current['badgeon'] ?? false)) return;
2131 echo '<div class="options_group">';
2132 echo "<div class='blurb' style='margin-left:13px'><h4>";
2133 echo __("On-site messaging badge", 'woo-vipps') ;
2134 echo "<h4></div>";
2135 $showbadge = sanitize_text_field(get_post_meta( get_the_ID(), '_vipps_show_badge', true));
2136
2137 woocommerce_wp_select(
2138 array(
2139 'id' => 'woo_vipps_show_badge',
2140 'label' => __( 'Override default settings', 'woo-vipps' ),
2141 'options' => array(
2142 '' => __('Default setting', 'woo-vipps'),
2143 'none' => __('No badge', 'woo-vipps'),
2144 'white' => __('White', 'woo-vipps'),
2145 'grey' => __('Grey', 'woo-vipps'),
2146 'filled' => __('Filled', 'woo-vipps'),
2147 'light' => __('Light', 'woo-vipps'),
2148 'purple' => __('Purple', 'woo-vipps'),
2149 ),
2150 'value' => $showbadge
2151 )
2152 );
2153 echo "</div>";
2154
2155 }
2156
2157 public function product_options_vipps_shareable_link() {
2158 global $post;
2159 global $wpdb;
2160 $product = wc_get_product($post->ID);
2161 $variable = ($product->get_type() == 'variable');
2162
2163 $buy_url = $this->buy_product_url();
2164 $q = $wpdb->prepare("SELECT meta_key, meta_value FROM `{$wpdb->postmeta}` WHERE post_id = %d AND meta_key LIKE '_vipps_shareable_link@_%' escape '@'", $product->get_id());
2165 $res = $wpdb->get_results($q, ARRAY_A);
2166 $shareables = [];
2167 if ($res) {
2168 foreach($res as $entry) {
2169 $shareable = maybe_unserialize($entry['meta_value']);
2170 if (!$shareable || empty($shareable['key'])) continue;
2171 $url = add_query_arg('pr',$shareable['key'],$this->buy_product_url());
2172 $shareable['url'] = $url;
2173 $shareables[] = $shareable;
2174 }
2175 }
2176
2177 $qradmin = admin_url("/edit.php?post_type=vipps_qr_code");
2178 ?>
2179 <div class="options_group">
2180 <div class='blurb' style='margin-left:13px'>
2181 <h4><?php echo __("Shareable links", 'woo-vipps') ?></h4>
2182 <p><?php echo sprintf(__('Shareable links are links you can share externally on banners or other places that when followed will start %1$s of this product immediately. Maintain these links here for this product.', 'woo-vipps'), Vipps::ExpressCheckoutName()); ?> </p>
2183 <p><?php echo sprintf(__("To create a QR code for your shareable link, we recommend copying the URL and then using the <a href='%2\$s'>%1\$s QR Api</a>", 'woo-vipps'), "Vipps", $qradmin); ?> </p>
2184 <input type=hidden id=vipps_sharelink_id value='<?php echo $product->get_id(); ?>'>
2185 <?php
2186 echo wp_nonce_field('share_link_nonce','vipps_share_sec',1,false);
2187 if ($variable):
2188 $variations = $product->get_available_variations();
2189 echo "<button id='vipps-share-link' disabled class='button' onclick='return false;'>"; echo __("Create shareable link",'woo-vipps'); echo "</button>";
2190 echo "<select id='vipps_sharelink_variant'><option value=''>"; echo __("Select variant", 'woo-vipps'); echo "</option>";
2191 foreach($variations as $var) {
2192 $varid = esc_attr($var['variation_id']);
2193 echo "<option value='$varid'>$varid";
2194 echo esc_html($var['sku']);
2195 echo "</option>";
2196 }
2197 echo "</select>";
2198 else:
2199 echo "<button id='vipps-share-link' class='button' onclick='return false;'>"; echo __("Create shareable link", 'woo-vipps'); "</button>";
2200 endif;
2201 ?>
2202 </div> <!-- end blurb -->
2203 <div style="display:none;" id='woo_vipps_shareable_link_template'>
2204 <a class='shareable' title="<?php echo __('Click to copy', 'woo-vipps'); ?>" href="javascrip:void(0)"></a><input class=deletemarker type=hidden value=''>
2205 </div>
2206 <div style="display:none;" id='woo_vipps_shareable_command_template'>
2207 <a class="copyaction" href='javascript:void(0)'>[<?php echo __("Copy", 'woo-vipps'); ?>]</a>
2208 <a class="deleteaction" style="margin-left:13px;" class="deleteaction" href="javascript:void(0)">[<?php echo __('Delete', 'woo-vipps'); ?>]</a>
2209 </div>
2210 <style>
2211 #woo_vipps_shareables a.deleted {
2212 text-decoration: line-through;
2213 }
2214 </style>
2215 <div class='blurb' style='margin-left:13px;margin-right:13px'>
2216 <div id="message-area" style="min-height:2em">
2217 <div class="vipps-shareable-link-error" style="display:none"><?php echo __('An error occured while creating a shareable link', 'woo-vipps');?>
2218 <span id="vipps-shareable-link-error"></span>
2219 </div>
2220 <div id="vipps-shareable-link-delete-message" style="display:none"><em><?php echo __('Link(s) will be deleted when you save the product', 'woo-vipps');?> </em></div>
2221 </div>
2222 <table id='woo_vipps_shareables' class='woo-vipps-link-table' style="width:100% <?php if (empty($shareables)) echo ';display:none;'?>">
2223 <thead>
2224 <tr>
2225 <?php if ($variable): ?><th align=left><?php echo __('Variant','woo-vipps'); ?></th><?php endif; ?>
2226 <th align=left><?php echo __('Link','woo-vipps'); ?></th>
2227 <th><?php echo __('Action','woo-vipps'); ?></th></tr>
2228 </thead>
2229 <tbody>
2230 <tr>
2231 <?php foreach ($shareables as $shareable): ?>
2232 <?php if ($variable): ?><td><?php echo esc_html($shareable['variant']); ?></td><?php endif; ?>
2233 <td><a class='shareable' title="<?php echo __('Click to copy','woo-vipps'); ?>" href="javascrip:void(0)"><?php echo esc_html($shareable['url']); ?></a><input class="deletemarker" type=hidden value='<?php echo esc_attr($shareable['key']); ?>'></td>
2234 <td align=center>
2235 <a class="copyaction" title="<?php echo __('Click to copy','woo-vipps'); ?>" href='javascript:void(0)'>[<?php echo __("Copy", 'woo-vipps'); ?>]</a>
2236 <a class="deleteaction" title="<?php echo __('Mark this link for deletion', 'woo-vipps'); ?>" style="margin-left:13px;" class="deleteaction" href="javascript:void(0)">[<?php echo __('Delete', 'woo-vipps'); ?>]</a>
2237 </td>
2238 </tr>
2239 <?php endforeach; ?>
2240 </tbody>
2241 </table>
2242 </div> <!-- end blurb -->
2243 </div> <!-- end options-group -->
2244 <?php
2245 }
2246
2247
2248 // This creates and stores a shareable link that when followed will allow external buyers to buy the specified product direclty.
2249 // Only products with these links can be bought like this; both to avoid having to create spurious orders from griefers and to ensure
2250 // that a link can be retracted if it has been printed or shared in emails with a specific price. IOK 2018-10-03
2251 public function ajax_vipps_create_shareable_link() {
2252 check_ajax_referer('share_link_nonce','vipps_share_sec');
2253 if (!current_user_can('manage_woocommerce')) {
2254 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
2255 wp_die();
2256 }
2257 static::set_locale_if_in_header();
2258 $prodid = intval($_POST['prodid']);
2259 $varid = intval($_POST['varid']);
2260
2261 $product = '';
2262 $variant = '';
2263 $varname = '';
2264 try {
2265 $product = wc_get_product($prodid);
2266 $variant = $varid ? wc_get_product($varid) : null;
2267 $varname = $variant ? $variant->get_id() : '';
2268 if ($variant && $variant->get_sku()) {
2269 $varname .= ":" . sanitize_text_field($variant->get_sku());
2270 }
2271 } catch (Exception $e) {
2272 echo json_encode(array('ok'=>0,'msg'=>$e->getMessage()));
2273 wp_die();
2274 }
2275 if (!$product) {
2276 echo json_encode(array('ok'=>0,'msg'=>__('The product doesn\'t exist', 'woo-vipps')));
2277 wp_die();
2278 }
2279
2280 // Find a free shareable link by generating a hash and testing it. Normally there won't be any collisions at all.
2281 $key = '';
2282 while (!$key) {
2283 global $wpdb;
2284 $key = substr(sha1(mt_rand() . ":" . $prodid . ":" . $varid),0,8);
2285 $existing = $wpdb->get_row("SELECT post_id from {$wpdb->prefix}postmeta where meta_key='_vipps_shareable_link_$key' limit 1",'ARRAY_A');
2286 if (!empty($existing)) $key = '';
2287 }
2288
2289 $url = add_query_arg('pr',$key,$this->buy_product_url());
2290 $payload = array('product_id'=>$prodid,'variation_id'=>$varid,'key'=>$key, 'url'=>$url, 'variant'=>$varname);
2291
2292 // This is used to find the link itself
2293 update_post_meta($prodid,'_vipps_shareable_link_'.$key, array('product_id'=>$prodid,'variation_id'=>$varid,'key'=>$key));
2294
2295 echo json_encode(array('ok'=>1,'msg'=>'ok', 'url'=>$url, 'variant'=> $varname, 'key'=>$key));
2296 wp_die();
2297 }
2298
2299 // A metabox for showing Vipps information about the order. IOK 2018-05-07
2300 public function add_vipps_metabox ($post_or_order_object) {
2301 $order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
2302 $order = wc_get_order($post_or_order_object);
2303 $pm = $order->get_payment_method();
2304 if (!self::is_vipps_order($pm)) return;
2305 $orderid=$order->get_id();
2306
2307 $init = intval($order->get_meta('_vipps_init_timestamp'));
2308 $callback = intval($order->get_meta('_vipps_callback_timestamp'));
2309 $capture = intval($order->get_meta('_vipps_capture_timestamp'));
2310 $refund = intval($order->get_meta('_vipps_refund_timestamp'));
2311 $cancel = intval($order->get_meta('_vipps_cancel_timestamp'));
2312
2313 $status = $order->get_meta('_vipps_status');
2314 $total = intval($order->get_meta('_vipps_amount'));
2315 $captured = intval($order->get_meta('_vipps_captured'));
2316 $refunded = intval($order->get_meta('_vipps_refunded'));
2317 $cancelled = intval($order->get_meta('_vipps_cancelled'));
2318
2319 $capremain = intval($order->get_meta('_vipps_capture_remaining'));
2320 $refundremain = intval($order->get_meta('_vipps_refund_remaining'));
2321
2322 $paymentdetailsnonce=wp_create_nonce('paymentdetails');
2323
2324 $failures = intval($order->get_meta('_vipps_capture_failures'));
2325
2326 $currency = $order->get_currency();
2327
2328 print "<table border=0><thead></thead><tbody>";
2329 print "<tr><td colspan=2>"; print $order->get_payment_method_title();print "</td></tr>";
2330 print "<tr><td>Status</td>";
2331 print "<td align=right>" . htmlspecialchars($status);print "</td></tr>";
2332 print "<tr><td>Amount</td><td align=right>" . sprintf("%0.2f ",$total/100); print $currency; print "</td></tr>";
2333 print "<tr><td>Captured</td><td align=right>" . sprintf("%0.2f ",$captured/100); print $currency; print "</td></tr>";
2334 print "<tr><td>Refunded</td><td align=right>" . sprintf("%0.2f ",$refunded/100); print $currency; print "</td></tr>";
2335 print "<tr><td>Cancelled</td><td align=right>" . sprintf("%0.2f ",$cancelled/100); print $currency; print "</td></tr>";
2336
2337 if ($failures) {
2338 print("<tr><td>Capture attempts</td><td align=right>$failures</td></tr>");
2339 }
2340
2341 print "<tr><td>Vipps initiated</td><td align=right>";if ($init) print date('Y-m-d H:i:s',$init); print "</td></tr>";
2342 print "<tr><td>Vipps response </td><td align=right>";if ($callback) print date('Y-m-d H:i:s',$callback); print "</td></tr>";
2343 print "<tr><td>Vipps capture </td><td align=right>";if ($capture) print date('Y-m-d H:i:s',$capture); print "</td></tr>";
2344 print "<tr><td>Vipps refund</td><td align=right>";if ($refund) print date('Y-m-d H:i:s',$refund); print "</td></tr>";
2345 print "<tr><td>Vipps cancelled</td><td align=right>";if ($cancel) print date('Y-m-d H:i:s',$cancel); print "</td></tr>";
2346 print "</tbody></table>";
2347 print "<a href='javascript:VippsGetPaymentDetails($orderid,\"$paymentdetailsnonce\");' class='button'>" . __('Show complete transaction details','woo-vipps') . "</a>";
2348 }
2349
2350
2351 // Vipps' requirement for phone numbers is very strict, and payments initiated with
2352 // numbers in any other format will fail. Therefore we must try to convert to MSISDN before that.
2353 public static function normalizePhoneNumber($phone, $country='') {
2354 $phonenr = preg_replace("![^0-9]!", "", strval($phone));
2355 $phonenr = preg_replace("!^0+!", "", $phonenr);
2356
2357 // Try to reconstruct phone numbers from information provided
2358 switch ($country) {
2359 case 'DK':
2360 if (8 === strlen($phonenr)) {
2361 $phonenr = "45$phonenr";
2362 }
2363 break;
2364 case 'SE': // 10 digits, but we stripped the leading zero above, https://www.sent.dm/resources/se. LP 2026-02-09
2365 if (9 === strlen($phonenr)) {
2366 $phonenr = "46$phonenr";
2367 }
2368 break;
2369 case 'NO':
2370 if (8 === strlen($phonenr)) {
2371 $phonenr = "47$phonenr";
2372 }
2373 break;
2374 case 'FI': // https://en.wikipedia.org/wiki/Telephone_numbers_in_Finland and https://kielitoimistonohjepankki.fi/ohje/puhelinnumerot/
2375 if (9 === strlen($phonenr) // 04x 123 45 67 and 050 123 45 67 (but we removed leading zero already)
2376 || 10 === strlen($phonenr) // 0457 123 45 67 (but we removed leading zero already)
2377 ) {
2378 $phonenr = "358$phonenr";
2379 }
2380 break;
2381 }
2382
2383 if (!preg_match("/^\d{10,15}$/", $phonenr)) {
2384 $phonenr = false;
2385 }
2386 return $phonenr;
2387 }
2388
2389
2390 // This is for debugging and ensuring we have excact details correct for a transaction.
2391 public function ajax_vipps_payment_details() {
2392 check_ajax_referer('paymentdetails','vipps_paymentdetails_sec');
2393 static::set_locale_if_in_header();
2394 $orderid = intval($_REQUEST['orderid']);
2395 $gw = $this->gateway();
2396 $order = wc_get_order($orderid);
2397 if (!$order) {
2398 print "<p>" . __("Unknown order", 'woo-vipps') . "</p>";
2399 exit();
2400 }
2401 $pm = $order->get_payment_method();
2402 if (!self::is_vipps_order($pm)) {
2403 print "<p>" . sprintf(__("The order is not a %1\$s order", 'woo-vipps'), $this->get_payment_method_name()) . "</p>";
2404 exit();
2405 }
2406
2407 $gw = $this->gateway();
2408 try {
2409 $details = $gw->get_payment_details($order);
2410
2411 if ($details) {
2412 try {
2413 $details['epaymentLog'] = $gw->api->epayment_get_payment_log ($order);
2414 } catch (Exception $e) {
2415 $this->log("Could not get transaction log for " . $order->get_id() . " : " . $e->getMessage(), 'error');
2416 }
2417 }
2418 $order->update_meta_data('_vipps_capture_failures', 0); // Reset this if getting full data
2419 $order = $gw->update_vipps_payment_details($order, $details);
2420 } catch (Exception $e) {
2421 print "<p>";
2422 print __('Transaction details not retrievable: ','woo-vipps') . $e->getMessage();
2423 print "</p>";
2424 exit();
2425 }
2426
2427 print "<h2>" . __('Transaction details','woo-vipps') . "</h2>";
2428 print "<p>";
2429 print __('Order id', 'woo-vipps') . ": " . @$details['orderId'] . "<br>";
2430 print __('Order status', 'woo-vipps') . ": " .@$details['status'] . "<br>";
2431 if (isset($details['paymentMethod'])) {
2432 $method = (is_array($details['paymentMethod'])) ? $details['paymentMethod']['type'] : "";
2433 print __("Payment method", 'woo-vipps') . ":" . $method . "<br>";
2434 } else {
2435 print __("Payment method", 'woo-vipps') . ": Vipps <br>";
2436 }
2437 print __("API", 'woo-vipps') .": " . esc_html($order->get_meta('_vipps_api')) . "</br>";
2438
2439 if (!empty(@$details['transactionSummary'])) {
2440 $ts = $details['transactionSummary'];
2441 print "<h3>" . __('Transaction summary', 'woo-vipps') . "</h3>";
2442 print __('Capured amount', 'woo-vipps') . ":" . @$ts['capturedAmount'] . "<br>";
2443 print __('Remaining amount to capture', 'woo-vipps') . ":" . @$ts['remainingAmountToCapture'] . "<br>";
2444 print __('Refunded amount', 'woo-vipps') . ":" . @$ts['refundedAmount'] . "<br>";
2445 print __('Remaining amount to refund', 'woo-vipps') . ":" . @$ts['remainingAmountToRefund'] . "<br>";
2446 if (isset($ts['cancelledAmount'])) {
2447 print __('Cancelled amount', 'woo-vipps') . ":" . @$ts['cancelledAmount'] . "<br>";
2448 print __('Remaining amount to cancel', 'woo-vipps') . ":" . @$ts['remainingAmountToCancel'] . "<br>";
2449 }
2450 }
2451 if (!empty(@$details['shippingDetails'])) {
2452 $ss = $details['shippingDetails'];
2453 $addr = isset($ss['address']) ? $ss['address'] : array();
2454 print "<h3>" . __('Shipping details', 'woo-vipps') . "</h3>";
2455 print __('Address', 'woo-vipps') . ": " . htmlspecialchars(join(', ', array_filter(array_values($addr), 'is_scalar'))) . "<br>";
2456 if (@$ss['shippingMethod']) print __('Shipping method', 'woo-vipps') . ": " . htmlspecialchars(@$ss['shippingMethod']) . "<br>";
2457 if (@$ss['shippingCost']) print __('Shipping cost', 'woo-vipps') . ": " . @$ss['shippingCost'] . "<br>";
2458 print __('Shipping method ID', 'woo-vipps') . ": " . htmlspecialchars(@$ss['shippingMethodId']) . "<br>";
2459 if (isset($ss['pickupPoint'])) {
2460 $pp = $ss['pickupPoint'];
2461 print "<h3>" . __('Pickup Point', 'woo-vipps') . "</h3>";
2462 print $pp['name'] . "<br>";
2463 print $pp['address'] . "<br>";
2464 print $pp['postalCode'] . " ";
2465 print $pp['city'] . "<br>";
2466 print $pp['country'] . "<br>";
2467 }
2468 }
2469 if (!empty(@$details['billingDetails'])) {
2470 $us = $details['billingDetails'];
2471 print "<h3>" . __('Billing details', 'woo-vipps') . "</h3>";
2472 print __('First Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['firstName']) . "<br>";
2473 print __('Last Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['lastName']) . "<br>";
2474 print __('Mobile Number', 'woo-vipps') . ": " . htmlspecialchars(@$us['phoneNumber']) . "<br>";
2475 print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>";
2476 }
2477 // Checkout v3: No userDetails, but Vipps email may be present
2478 if (!empty(@$details['userInfo'])) {
2479 $us = $details['userInfo'];
2480 print "<h3>" . __('User details', 'woo-vipps') . "</h3>";
2481 print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>";
2482 } else if (!empty(@$details['userDetails'])) {
2483 // Older versions of the api, as well as express checkout has "userDetails"
2484 $us = $details['userDetails'];
2485 print "<h3>" . __('User details', 'woo-vipps') . "</h3>";
2486 print __('User ID', 'woo-vipps') . ": " . htmlspecialchars(@$us['userId']) . "<br>";
2487 print __('First Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['firstName']) . "<br>";
2488 print __('Last Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['lastName']) . "<br>";
2489 print __('Mobile Number', 'woo-vipps') . ": " . htmlspecialchars(@$us['mobileNumber']) . "<br>";
2490 print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>";
2491 }
2492 if (!empty(@$details['epaymentLog']) && is_array($details['epaymentLog'])) {
2493 print "<h3>" . __('Transaction Log', 'woo-vipps') . "</h3>";
2494 $i = count($details['epaymentLog'])+1;
2495 $reversed = array_reverse($details['epaymentLog']);
2496 foreach ($reversed as $td) {
2497 print "<br>";
2498 print __('Operation','woo-vipps') . ": " . htmlspecialchars(@$td['name']) . "<br>";
2499 $value = intval(@$td['amount']['value'])/100;
2500 $curr = $td['amount']['currency'];
2501
2502 print __('Amount','woo-vipps') . ": " . esc_html($value) . " " . esc_html($curr) . "<br>";
2503 print __('Success','woo-vipps') . ": " . @$td['success'] . "<br>";
2504 print __('Timestamp','woo-vipps') . ": " . htmlspecialchars(@$td['timestamp']) . "<br>";
2505 print __('Transaction ID','woo-vipps') . ": " . htmlspecialchars(@$td['pspReference']) . "<br>";
2506 }
2507 }
2508 exit();
2509 }
2510
2511 // This function will create a file with an obscure filename in the $callbackDirname directory.
2512 // When initiating payment, this file will be created with a zero value. When the response is reday,
2513 // it will be rewritten with the value 1.
2514 // This function can fail if we can't write to the directory in question, in which case, return null and
2515 // to the check with admin-ajax instead. IOK 2018-05-04
2516 public function createCallbackSignal($order,$ok=0) {
2517 $fname = $this->callbackSignal($order);
2518 if (!$fname) return null;
2519 if ($ok) {
2520 @file_put_contents($fname,"1");
2521 }else {
2522 @file_put_contents($fname,"0");
2523 }
2524 if (is_file($fname)) return $fname;
2525 return null;
2526 }
2527
2528 //Helper function that produces the signal file name for an order IOK 2018-05-04
2529 public function callbackSignal($order) {
2530 $dir = $this->callbackDir();
2531 if (!$dir) return null;
2532 $fname = 'vipps-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction')) . ".txt";
2533 return $dir . DIRECTORY_SEPARATOR . $fname;
2534 }
2535 // URL of the above product thing
2536 public function callbackSignalURL($signal) {
2537 if (!$signal) return "";
2538 $uploaddir = wp_upload_dir();
2539 return $uploaddir['baseurl'] . '/' . $this->callbackDirname . '/' . basename($signal);
2540 }
2541
2542 // Clean up old signal files. If there gets to be a lot of them, this may take some time. IOK 2018-05-04.
2543 public function cleanupCallbackSignals() {
2544 $dir = $this->callbackDir();
2545 if (!is_dir($dir)) return;
2546 $signals = scandir($dir);
2547 $now = time();
2548 foreach($signals as $signal) {
2549 $path = $dir . DIRECTORY_SEPARATOR . $signal;
2550 if (is_dir($path)) continue;
2551 if (is_file($path)) {
2552 $age = @filemtime($path);
2553 $halfhour = 30*60;
2554 if (($age+$halfhour) < $now) {
2555 @unlink($path);
2556 }
2557 }
2558 }
2559 }
2560
2561 // Returns the name of the callback-directory, or null if it doesn't exist. IOK 2018-05-04
2562 private function callbackDir() {
2563 $uploaddir = wp_upload_dir();
2564 $base = $uploaddir['basedir'];
2565 $callbackdir = $base . DIRECTORY_SEPARATOR . $this->callbackDirname;
2566 if (is_dir($callbackdir)) return $callbackdir;
2567 $ok = mkdir($callbackdir, 0755);
2568 if ($ok) return $callbackdir;
2569 return null;
2570 }
2571
2572
2573 // Because the prefix used to create the Vipps order id is editable
2574 // by the user, we will store that as a meta and use this for callbacks etc.
2575 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2576 // debug filters and similar.
2577 // IOK 2026-05-27 rewritten to avoid wc_get_orders for pre-HPOS. Still not used.
2578 public function getOrderIdByVippsOrderId($vippsorderid) {
2579 $result = false;
2580 if ($this->useHPOS()) {
2581 $result = wc_get_orders( array(
2582 'limit' => 1,
2583 'return' => 'ids',
2584 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2585 ));
2586 if ($result && is_array($result)) return $result[0];
2587 } else {
2588 // Pre-HPOS did not support meta_query, so we're doing it with direct access to the database. IOK 2026-05-27
2589 global $wpdb;
2590 $q = $wpdb->prepare("SELECT p.ID from `{$wpdb->posts}` p JOIN `{$wpdb->postmeta}` m ON (m.post_id = p.ID and m.meta_key = '_vipps_orderid') WHERE p.post_type = 'shop_order' AND m.meta_value = %s LIMIT 1", $vippsorderid);
2591 $res = $wpdb->get_results($q, ARRAY_A);
2592 if (empty($res)) return 0;
2593 return $res[0]['ID'];
2594 }
2595 return 0;
2596 }
2597
2598 // This is like getOrderByVipsOrderId, but only fetches pending orders.
2599 // This is used for the webhooks, where there is no way to add our own order info. IOK 2023-12-19
2600 private function get_pending_vipps_order($vippsorderid) {
2601 if ($this->useHPOS()) {
2602 $sevendaysago = time() - (60*60*24*7);
2603 $result = wc_get_orders( array(
2604 'limit' => 1,
2605 'status' => 'wc-pending',
2606 'type' => 'shop_order',
2607 'date_created' => '>' . $sevendaysago,
2608 'return' => 'objects',
2609 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2610 ));
2611 if (!empty($result) && is_a($result[0], 'WC_Order')) return $result[0];
2612 return null;
2613 } else {
2614 global $wpdb;
2615 $q = $wpdb->prepare("SELECT p.ID from `{$wpdb->posts}` p JOIN `{$wpdb->postmeta}` m ON (m.post_id = p.ID and m.meta_key = '_vipps_orderid') WHERE p.post_type = 'shop_order' && p.post_status = 'wc-pending' AND m.meta_value = %s LIMIT 1", $vippsorderid);
2616 $res = $wpdb->get_results($q, ARRAY_A);
2617 if (empty($res)) return null;
2618 $o = wc_get_order($res[0]['ID']);
2619 if (is_a($o, 'WC_Order')) return $o;
2620 return null;
2621 }
2622 }
2623
2624 // Special pages, and some callbacks. IOK 2018-05-18
2625 public function template_redirect() {
2626
2627 // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2628 // We redirect these to the new location.
2629 $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2630 if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2631 $url = static::get_special_page_url();
2632 $_GET['action'] = 'buy_product';
2633 $q = build_query($_GET);
2634 wp_redirect($url . "?" . $q, 302);
2635 exit();
2636 }
2637
2638 if (static::is_special_page()) {
2639 // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2640 remove_filter('template_redirect', 'redirect_canonical', 10);
2641 // dont cache special page. LP 2026-08-25
2642 $this->nocache();
2643 // Do the custom pre-load actions for these pages IOK 2026-09-11
2644 do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2645 }
2646 }
2647
2648 // Ran in template redirect for the special page. IOK 2026-09-2
2649 public function pre_special_page_actions ($action) {
2650 // Change title dynamically depending on action. LP 2026-09-02
2651 add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2652
2653 // If we are handling the 'wait for payment' action, we need to poll the order status before
2654 // we start producing content IOK 2026-09-21
2655 if ($action == 'wait_for_payment') {
2656 $this->handle_payment_poll_and_redirect();
2657 }
2658
2659 // Some validation is required for this action
2660 if ($action == 'do_express_checkout') {
2661 $this->vipps_express_checkout_consistency_check();
2662 }
2663 // These two actions require an extra script
2664 if (in_array($action, ['buy_product','do_express_checkout'])) {
2665 wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2666 filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2667 ['in_footer'=>true]
2668 );
2669 }
2670 }
2671
2672 // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2673 public function vipps_special_page_endpoint_title($title, $postid = 0) {
2674 global $wp_query;
2675 // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2676
2677 // In block themes the whole template (header, footer, content) renders inside the main
2678 // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2679 // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2680 // the queried page so an earlier title doesn't consume this one-shot filter.
2681 if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2682 switch ($_GET['action'] ?? '') {
2683 case 'wait_for_payment':
2684 $title = __('Processing order', 'woo-vipps');
2685 break;
2686 case 'do_express_checkout':
2687 case 'buy_product':
2688 $title = __('Express Checkout', 'woo-vipps');
2689 break;
2690 }
2691 }
2692 return $title;
2693 }
2694
2695 // Template handling for special pages. IOK 2018-11-21
2696 // This is legacy - the special page is now a real page, so it can have a special template using standard WP methods. IOK 2026-09-11
2697 public function template_include($template) {
2698 if (static::is_special_page()) {
2699 // Get any special template override from the options IOK 2020-02-18
2700 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2701 $found = locate_template($specific,false,false);
2702 if ($found) $template=$found;
2703
2704 return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2705 }
2706 return $template;
2707 }
2708
2709 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2710 // the user if using Express Checkout IOK 2020-10-09
2711 public function woocommerce_before_thankyou ($orderid) {
2712 $order = wc_get_order($orderid);
2713 if ($order) {
2714 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2715 // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2716 $this->maybe_log_in_user($order);
2717 $order->delete_meta_data('_vipps_limited_session');
2718 $order->save();
2719
2720 // Now if this was express checkout and we are a guest, ensure we have the correct email in the session->customer array IOK 2023-07-17
2721 if (! is_user_logged_in() ) {
2722 $this->maybe_set_session_customer_email($order);
2723 }
2724 }
2725 $this->maybe_restore_cart($orderid);
2726
2727 WC()->session->set('current_vipps_session', false);
2728 WC()->session->set('vipps_checkout_current_pending',false);
2729 WC()->session->set('vipps_address_hash', false);
2730 do_action('woo_vipps_before_thankyou', $orderid, $order);
2731 }
2732 public function woocommerce_loaded() {
2733 // Ended buy-now product block support for allproducts block. LP 29.11.2024
2734
2735 /* This is for the other product blocks - here we only have a single HTML filter unfortunately */
2736 add_filter('woocommerce_blocks_product_grid_item_html', function ($html, $data, $product) {
2737 if (!$this->loop_single_product_is_express_checkout_purchasable($product)) return $html;
2738 $stripped = preg_replace("!</li>$!", "", $html);
2739 $pid = $product->get_id();
2740 $button = '<div class="wp-block-button wc-block-components-product-button wc-block-button-vipps">';
2741 $button .= $this->get_buy_now_button($pid,false, null, false, '', 'catalog');
2742 $button .= '</div>';
2743 return $stripped . $button . "</li>";
2744 }, 10, 3);
2745
2746 // If local pickup has been added to express/checkout by filters, add this to emails/confirmation pages. IOK 2025-08-15
2747 add_filter('woocommerce_order_shipping_to_display', function($shipping, $order, $tax_display) {
2748 if (!is_a($order, 'WC_Order')) return $shipping;
2749 if (! self::is_vipps_order($order)) return $shipping;
2750 $shipping_method = current( $order->get_shipping_methods() );
2751
2752 if (empty($shipping_method)) return $shipping;
2753
2754 // Handled by Woo Central IOK 2025-08-15
2755 if ('pickup_location' == $shipping_method->get_method_id()) {
2756 return $shipping;
2757 }
2758
2759
2760 $details = trim($shipping_method->get_meta( 'pickup_details' ));
2761 $location = trim($shipping_method->get_meta( 'pickup_location' ));
2762 $address = trim($shipping_method->get_meta( 'pickup_address' ));
2763
2764 if (!empty($location) || !empty($address)) {
2765 $shipping .= "<br><strong>" . __( 'Pickup location', 'woocommerce' ) . ":</strong>";
2766 }
2767 if (!empty($location)) $shipping .= esc_html($location);
2768 if (!empty($address)) $shipping .= "<br>" . esc_html($address);
2769 if (!empty($details)) $shipping .= "<br><small>" . esc_html($details) . "</small>";
2770
2771 return $shipping;
2772 }, 10, 3);
2773
2774
2775 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2776 // IOK 2025-11-19
2777 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2778 }
2779
2780 public function get_payment_method_name() {
2781 return $this->gateway()->get_option('payment_method_name');
2782 }
2783
2784 public function plugins_loaded() {
2785 /* The gateway is added at 'plugins_loaded' and instantiated by Woo itself. IOK 2018-02-07 */
2786 add_filter( 'woocommerce_payment_gateways', array($this,'woocommerce_payment_gateways' ));
2787 /* Try to get a list of all installed gateways *before* we instantiate our own IOK 2024-05-27 */
2788 add_filter( 'woocommerce_payment_gateways', function ($gws) {
2789 if (!empty(Vipps::$installed_gateways)) return Vipps::$installed_gateways;
2790 Vipps::$installed_gateways = $gws;
2791 return $gws;
2792 }, 99999);
2793 }
2794
2795 public function after_setup_theme() {
2796 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2797 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2798
2799 // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2800 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2801 // is important.
2802 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2803
2804 // Callbacks use the Woo API IOK 2018-05-18
2805 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2806 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2807
2808 // Currently this sets Vipps as default payment method if hooked. IOK 2018-06-06
2809 add_action( 'woocommerce_cart_updated', array($this,'woocommerce_cart_updated'));
2810
2811 // Template integrations
2812 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2813 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2814
2815 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2816 // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2817 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2818
2819 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2820 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2821
2822 // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2823 // is Vipps
2824 add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2825 add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2826
2827 // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2828 add_action('template_redirect', array($this,'template_redirect'),1);
2829
2830 // Allow overriding their templates
2831 add_filter('template_include', array($this,'template_include'), 10, 1);
2832
2833 // Ajax endpoints for checking the order status while waiting for confirmation
2834 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2835 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2836
2837 // Handle the cancel unpaid order action when the "hold stock" times out.
2838 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2839 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2840 // For Checkout the rules are different though.
2841 add_filter('woocommerce_cancel_unpaid_order', function ($cancel, $order) {
2842
2843 // If we can't cancel for some other reason, don't.
2844 if (!$cancel) return $cancel;
2845
2846 // Only check Vipps orders
2847 if (! self::is_vipps_order($order)) return $cancel;
2848
2849 // For Vipps, all unpaid orders must be pending.
2850 if ($order->get_status() != 'pending' && $order->get_status() != 'failed') return $cancel;
2851
2852 // Handle this separately, in the Checkout class. IOK 2025-10-08
2853 $checkout_session = $order->get_meta('_vipps_checkout_session');
2854 if ($checkout_session) {
2855 $exception = null;
2856 try {
2857 $polldata = $this->gateway()->api->checkout_get_session_info($order);
2858 $sessionState = (!empty($polldata) && is_array($polldata) && isset($polldata['sessionState'])) ? $polldata['sessionState'] : "";
2859 // We can cancel the order iff we haven't started payment yet.
2860 if ($sessionState == 'PaymentSuccessful' || $sessionState == 'PaymentInitiated') return false;
2861 return true;
2862 } catch (VippsAPIException $e) {
2863 $resp = intval($e->responsecode);
2864 if ($resp == 402 || $resp == 404) {
2865 // We don't know about this transaction, so allow cancel IOK 2026-04-29
2866 return true;
2867 }
2868 $exception = $e; // Unknown exception, handle below
2869 } catch (Exception $e) {
2870 $exception = $e; // Unknown exception, handle below
2871 }
2872 if ($exception) {
2873 // If Vipps is unreachable, be safe and don't delete
2874 $this->log("Checkout: " . sprintf(__("Cannot get status of %1\$d at %2\$s in woocommerce_cancel_unpaid_order, not allowing deletion: %3\$s", 'woo-vipps'), $order->get_id(), Vipps::CompanyName(), $exception->getMessage()));
2875 return false;
2876 }
2877 return false;
2878 }
2879
2880 // Epayment/non-checkout IOK 2026-04-29
2881 // Keep in mind, checkout will fall through to here if the checkout session initialization failed. LP 2026-04-29
2882 try {
2883 $exception = null;
2884 $result = $this->gateway()->api->epayment_get_payment($order);
2885 } catch (VippsAPIException $e) {
2886 $resp = intval($e->responsecode);
2887 if ($resp == 402 || $resp == 404) {
2888 // We don't know about this transaction, so allow cancel IOK 2026-04-29
2889 return true;
2890 }
2891 $exception = $e; // Unknown exception, handle below
2892 } catch (Exception $e) {
2893 $exception = $e; // Unknown exception, handle below
2894 }
2895
2896 if ($exception) {
2897 // If Vipps is unreachable, be safe and don't delete
2898 $this->log(sprintf(__("Cannot get status of %1\$d at %2\$s in woocommerce_cancel_unpaid_order, not allowing deletion: %3\$s", 'woo-vipps'), $order->get_id(), Vipps::CompanyName(), $exception->getMessage()));
2899 return false;
2900 }
2901
2902 // We should now have an object with the 'state' in one of the Vipps states. We'll translate all of them to
2903 // cancelled or nah, and if cancelled, we allow deletion. IOK 2025-10-07
2904 if (empty($result)) return true;
2905 $state = $this->gateway()->interpret_vipps_order_status($result['state'] ?? 'CANCEL');
2906 if (empty($state) || $state == 'cancelled') return true;
2907
2908 return false;
2909
2910 }, 20, 2);
2911
2912 // Used both in admin and non-admin-scripts, load as quick as possible IOK 2020-09-03
2913 $this->vippsJSConfig = array();
2914 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2915 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2916 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2917 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2918 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2919 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2921 $this->vippsJSConfig['vippslocale'] = get_locale();
2922 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2923 $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
2924 $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2925 $wc_lang = $this->get_html_button_attrs_for_context()['language'];
2926 if ('store' === $wc_lang) $wc_lang = $this->get_customer_language();
2927 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
2928 if ('dk' === $wc_lang) $wc_lang = 'da';
2929 $this->vippsJSConfig['webcomponentLanguage'] = $wc_lang;
2930
2931
2932 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2933 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2934 // Ensure gateways are loaded at this point IOK 2026-05-27
2935 require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
2936 require_once(dirname(__FILE__) . '/WC_Gateway_Vipps.class.php');
2937
2938 // Then the payment blocks
2939 require_once(dirname(__FILE__) . "/Blocks/Payment/Vipps.class.php");
2940 require_once(dirname(__FILE__) . "/Blocks/Payment/VippsCard.class.php");
2941 Automattic\WooCommerce\Blocks\Payments\Integrations\Vipps::register();
2942 Automattic\WooCommerce\Blocks\Payments\Integrations\VippsCard::register();
2943 }
2944
2945 // Used for e.g. labels of product/shipping metadata. IOK 2025-05-07
2946 add_filter('woocommerce_attribute_label', function ($label, $name, $product) {
2947 if ( $product ) {
2948 return $label;
2949 }
2950 switch ( $name ) {
2951 case 'brand': // This is for shipping IOK 2025-05-07
2952 return __('Company', 'woo-vipps');
2953 case 'type':
2954 return __('Type', 'woo-vipps');
2955 case 'vipps_delivery_timeslot':
2956 return __('Timeslot', 'woo-vipps');
2957 case 'vipps_delivery_timeslot_id':
2958 return __('Timeslot ID', 'woo-vipps');
2959 }
2960 return $label;
2961 }, 9, 3);
2962
2963
2964 }
2965
2966 // IOK 2021-12-09 try to get the current language in the format Vipps wants, one of 'en' and 'no'
2967 // IOK 2025-09-03 stop trying to get the logged-in users language - it does not seem to work especially well in newer woos.
2968 public function get_customer_language() {
2969 global $TRP_LANGUAGE; // TranslatePress IOK 2025-11-06
2970
2971 $language = substr(get_bloginfo('language'),0,2);
2972 if (function_exists('pll_current_language')) {
2973 $pll_language = pll_current_language('slug');
2974 if ($pll_language) $language = $pll_language;
2975 } elseif (has_filter('wpml_current_language')){
2976 $language=apply_filters('wpml_current_language',null);
2977 } elseif (!empty($TRP_LANGUAGE)) {
2978 $language = sanitize_title($TRP_LANGUAGE);
2979 }
2980 // Just to be sure.
2981 $language = strtolower($language);
2982
2983 // Allow others to override in case they have some unorthodox setups IOK 2025-11-12
2984 $language = apply_filters('woo_vipps_customer_language', $language);
2985
2986 if ($language == 'nb' || $language == 'nn') $language = 'no';
2987 if ($language == 'da') $language = 'dk';
2988 if ($language == 'sv') $language = 'se';
2989 if (! in_array($language, ['en', 'no', 'dk', 'fi', 'se'])) $language = 'en';
2990 return $language;
2991 }
2992
2993 // Called by ajax on the order page; redirects back to same page. IOK 2022-11-02
2994 public function order_handle_vipps_action () {
2995 check_ajax_referer('vippssecnonce','vipps_sec');
2996 static::set_locale_if_in_header();
2997 $order = wc_get_order(intval($_REQUEST['orderid']));
2998 if (!is_a($order, 'WC_Order')) return;
2999 $pm = $order->get_payment_method();
3000 if (!self::is_vipps_order($pm)) return;
3001
3002 $action = isset($_REQUEST['do']) ? sanitize_title($_REQUEST['do']) : 'none';
3003
3004 if ($action == 'do_capture') {
3005 $gw = $this->gateway();
3006 $ok = $gw->maybe_capture_payment($order->get_id());
3007 }
3008 print "1";
3009 }
3010
3011 // Rest route: returns wc products, but only those purchasable by VMP express checkout. LP 2026-01-22
3012 // Called by the buy-now express block. LP 2026-01-22
3013 public function rest_express_checkout_products($request) {
3014 static::set_locale_if_in_header();
3015
3016 // Redirect product fetch to WC rest api. LP 2026-01-23
3017 $wc_request = new WP_REST_Request('GET', '/wc/store/v1/products');
3018 $wc_request->set_query_params($request->get_query_params());
3019 $response = rest_do_request($wc_request);
3020 if ($response->is_error()) {
3021 return $response;
3022 }
3023 $products = $response->get_data();
3024
3025 // Extract variant products out from the parent product, so we can support these. LP 2026-01-22
3026 foreach($products as &$product) {
3027 if (!(isset($product['variations']) && is_array($product['variations']) && $product['variations'])) continue;
3028
3029 foreach($product['variations'] as $variation) {
3030 $v = wc_get_product($variation->id);
3031 if (!is_a($v, 'WC_Product')) continue;
3032 $products[] = [
3033 'is_variation' => true,
3034 'parent' => $product['id'],
3035 'id' => $v->get_id(),
3036 'sku' => $v->get_sku(),
3037 'type' => $v->get_type(),
3038 'slug' => $v->get_slug(),
3039 'name' => $v->get_name()
3040 ];
3041 }
3042 }
3043
3044 // Filter only Express-purchaseable products, variant parents should also be removed here. LP 2026-01-22
3045 $filtered_products = array_filter($products, fn($p) => $this->loop_single_product_is_express_checkout_purchasable(wc_get_product($p['id'])));
3046 // Reindex array to fix output. LP 2026-01-22
3047 $filtered_products = array_values($filtered_products);
3048 $response->set_data($filtered_products);
3049 return $response;
3050
3051 }
3052
3053 // Make admin-notices persistent so we can provide error messages whenever possible. IOK 2018-05-11
3054 public function store_admin_notices() {
3055 // WooCommerce will (now) call this function in the inject_before_notices method. If it does not exist,
3056 // we get a crash. If there is no "current screen", then we cannot provide these.
3057 if (!function_exists('get_current_screen')) return false;
3058 ob_start();
3059 do_action('vipps_admin_notices');
3060 $notices = ob_get_clean();
3061 set_transient('_vipps_save_admin_notices',$notices, 5*60);
3062 }
3063
3064
3065 public function order_item_add_action_buttons ($order) {
3066 $this->order_item_add_capture_button($order);
3067 }
3068
3069 public function order_item_add_capture_button ($order) {
3070 $pm = $order->get_payment_method();
3071 if (!self::is_vipps_order($pm)) return;
3072 $status = $order->get_status();
3073
3074 $show_capture_button = ($status == 'on-hold' || $status == 'processing');
3075 if (!apply_filters('woo_vipps_show_capture_button', $show_capture_button, $order)) {
3076 return;
3077 }
3078
3079 $captured = intval($order->get_meta('_vipps_captured'));
3080 // noncapturable should never be greater than capture remaining, so this *should* not be negative. LP 2026-06-12
3081 $capremain = intval($order->get_meta('_vipps_capture_remaining')) - intval($order->get_meta('_vipps_noncapturable'));
3082 if ($captured && (!$capremain || $capremain < 2)) {
3083 print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>";
3084 return;
3085 }
3086
3087 $logo = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
3088
3089 print '<button type="button" class="button vippsbutton generate-items vipps-action"
3090 data-orderid="' . $order->get_id() . '" data-action="do_capture"
3091 style="background-color:#ff5b24;border-color:#ff5b24;color:#ffffff" >
3092 <img border=0 style="display:inline;height:2ex;vertical-align:text-bottom" class="inline" alt=0 src="'.$logo.'"/> ' . __('Capture payment','woo-vipps') . '</button>';
3093
3094 }
3095
3096
3097 // This is the main callback from Vipps when payments are returned. IOK 2018-04-20
3098 public function vipps_callback() {
3099 $this->log("Callback received");
3100
3101 Vipps::nocache();
3102 // Required for Checkout, we send this early as error recovery here will be tricky anyhow.
3103 status_header(202, "Accepted");
3104
3105
3106 $raw_post = @file_get_contents( 'php://input' );
3107 $result = @json_decode($raw_post,true);
3108
3109 // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
3110 // .. and the epayment webhooks 2023-12-19
3111 $ischeckout = false;
3112 $iswebhook = false;
3113 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
3114 // For Checkout v3 and onwards, we control the callback so the type is just this field
3115 if ($callback == 'checkout') {
3116 $ischeckout = true;
3117 }
3118 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
3119 if ($callback == 'webhook' || (!$ischeckout && ($result['pspReference'] ?? false))) {
3120 $iswebhook = true;
3121 }
3122
3123 $vippsorderid = ($result && isset($result['orderId'])) ? $result['orderId'] : "";
3124 // For checkout, the orderId has been renamed to "reference" IOK 2022-02-11
3125 // We set the orderId here very early so old filters and hooks will continue working - mostly used for debugging.
3126 if (!$vippsorderid && $result && isset($result['reference'])) {
3127 $vippsorderid = $result['reference'];
3128 $result['orderId'] = $result['reference'];
3129 }
3130
3131 do_action('woo_vipps_vipps_callback', $result,$raw_post);
3132
3133 if (!$result) {
3134 $error = json_last_error_msg();
3135 $this->log(sprintf(__("Did not understand callback from %1\$s:",'woo-vipps'), $this->get_payment_method_name()) . " " . $raw_post, 'error');
3136 $this->log(sprintf(__("Error was: %1\$s",'woo-vipps'), $error));
3137 return false;
3138 }
3139
3140 // For testing sites that appear not to receive callbacks
3141 if (isset($result['testing_callback'])) {
3142 $this->log(__("Received a test callback, exiting" , 'woo-vipps'), 'debug');
3143 print '{"status": 1, "msg": "Test ok"}';
3144 exit();
3145 }
3146
3147 // If this is a webhook call, we need to verify it, check that it is one of the 'callback' webhooks, check that we still have a pending
3148 // order for it, normalize the callback data and then handle the callback. IOK 2023-12-21
3149 if ($iswebhook) {
3150 // The webhook payloads spell the msn differently. IOK 2023-12-21
3151 $msn = ($result['msn'] ?? '') ? $result['msn'] : ($result['merchantSerialNumber'] ?? '');
3152 if ($msn) {
3153 $result['msn'] = $msn;
3154 $result['merchantSerialNumber'] = $msn;
3155 }
3156 $hookdata = $this->gateway()->get_local_webhook($msn);
3157 $secret = $hookdata ? ($hookdata['secret'] ?? false) : false;
3158 if (!$secret) {
3159 $this->log(sprintf(__('Cannot verify webhook callback for order %1$s - this shop does not know the secret. You should delete all unwanted webhooks. If you are using the same MSN on several shops, this callback is probably for one of the others.', 'woo-vipps'), $vippsorderid), 'debug');
3160 return false;
3161 }
3162 $verified = $this->verify_webhook($raw_post, $secret);
3163 if (!$verified) {
3164 $this->log(sprintf(__('Cannot verify webhook callback for order %1$s - signature does not match. This may be an attempt to forge callbacks', 'woo-vipps'), $vippsorderid), 'debug');
3165 return;
3166 }
3167
3168 // We need to check if this is a payment event, or if not, and if it is, if it is one of the ones we are prepared to handle. IOK 2023-12-21
3169 $event = $result['name'] ?? '';
3170 $payment_events = ["CREATED", "ABORTED", "EXPIRED", "CANCELLED", "CAPTURED", "REFUNDED", "AUTHORIZED", "TERMINATED"];
3171 $callback_events = ["ABORTED","EXPIRED", "AUTHORIZED", "TERMINATED"];
3172
3173 // If this is a payment event, we should have an order too so try to retrieve it. IOK 2023-12-21
3174 $order = null;
3175 $pending = false;
3176 if ($vippsorderid && $msn && in_array($event, $payment_events)) {
3177 // Then check if the reference/vippsorderid is a pending order
3178 $order = $this->get_pending_vipps_order($vippsorderid);
3179 if ($order) {
3180 $pending = true;
3181 } else {
3182 // If it isn't, but it is a payment event, get the order id from the epayment metadata. IOK 2023-12-21
3183 try {
3184 $polldata = $this->gateway()->api->epayment_get_payment($vippsorderid, $msn);
3185 if ($polldata && isset($polldata['metadata'])) {
3186 $orderid = $polldata['metadata']['orderid'];
3187 if ($orderid) {
3188 $order = wc_get_order($orderid);
3189 if (!$order || $vippsorderid != $order->get_meta('_vipps_orderid')) {
3190 $this->log(
3191 sprintf(__('The reference %1$s and order id %2$s does not match in webhook event %3$s - callback is invalid for the order.', 'woo-vipps'),
3192 $vippsorderid, $orderid, $event), 'debug');
3193 $order = null;
3194 return;
3195 $order = null;
3196 }
3197 }
3198 }
3199 } catch (Exception $e) {
3200 $this->log(sprintf(__("Could not get orderid of reference %2\$s from %1\$s: ", 'woo-vipps'), Vipps::CompanyName(), $vippsorderid) . $e->getMessage(), 'debug');
3201 }
3202 }
3203 }
3204
3205 // This will run for all events, not just the one this handler handles IOK 2023-12-21
3206 do_action('woo_vipps_webhook_event', $result, $order);
3207
3208 // We are not interested in Checkout orders - they have their own callback systems
3209 if ($order && $order->get_meta('_vipps_checkout')) {
3210 $this->log(sprintf(__('Received webhook callback for Checkout order %1$d - ignoring since full callback should come', 'woo-vipps'), $order->get_id()), 'debug');
3211 return;
3212 }
3213 // Now we will handle everything that is a callback event. IOK 2023-12-21
3214 if (!in_array($event, $callback_events)) {
3215 return;
3216 }
3217
3218 if (!$pending) {
3219 // If the order is no longer pending, then we can safely ignore it. IOK 2023-12-21
3220 $this->log(sprintf(__('Received webhook callback for order %1$s but this is no longer pending.', 'woo-vipps'), $vippsorderid), 'debug');
3221 return;
3222 }
3223 do_action('woo_vipps_callback_webhook', $result);
3224
3225 $ok = $this->gateway()->handle_callback($result, $order, false, $iswebhook);
3226 if ($ok) {
3227 // This runs only if the callback actually handled the order, if not, then the order was handled by poll.
3228 do_action('woo_vipps_callback_handled_order', $order);
3229 }
3230
3231 exit();
3232 }
3233
3234 // This branch is only for non-webhook callbacks; which currently means Checkout only. IOK 2025-08-13
3235 $orderid = intval(@$_REQUEST['id']);
3236
3237 if (!$orderid) {
3238 $this->log(sprintf(__("There is no order with this %1\$s orderid, callback fails:",'woo-vipps'), $this->get_payment_method_name()) . " " . $vippsorderid, 'error');
3239 return false;
3240 }
3241
3242 $order = wc_get_order($orderid);
3243 if (!is_a($order, 'WC_Order')) {
3244 $this->log(__("There is no order with this order id, callback fails:",'woo-vipps') . " " . $orderid, 'error');
3245 return false;
3246 }
3247
3248 // a small bit of security
3249 if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) {
3250 $this->log("Wrong authtoken on Vipps payment details callback", 'error');
3251 exit();
3252 }
3253
3254 do_action('woo_vipps_callback_checkout', $result);
3255
3256 $gw = $this->gateway();
3257
3258 // If neccessary, the order session will be restored in this method, and if so it will be reset before the exit happens
3259 // to reduce issues with users simultaneously returning to the store. IOK 2023-07-18
3260 $ok = $gw->handle_callback($result, $order, $ischeckout);
3261 if ($ok) {
3262 // This runs only if the callback actually handled the order, if not, then the order was handled by poll.
3263 do_action('woo_vipps_callback_handled_order', $order);
3264 }
3265
3266 exit();
3267 }
3268
3269 // Returns true iff we can verify that the webhook we just received is valid and that we know its secret IOK 2023-12-21
3270 public function verify_webhook($serialized, $secret) {
3271 // Extract the necessary headers.
3272 $expected_auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['HTTP_X_VIPPS_AUTHORIZATION'] ?? "");
3273 $expected_date = $_SERVER['HTTP_X_MS_DATE'] ?? '';
3274
3275 // Check if the date header is present and within an acceptable range (e.g., +/- 5 minutes) NT 2023-12-22
3276 if (!$this->isDateValid($expected_date)) {
3277 return false; // Date is not valid or not within the acceptable range
3278 }
3279
3280 // Prepare the data for signing.
3281 $hashed_payload = base64_encode(hash('sha256', $serialized, true));
3282 $path_and_query = $_SERVER['REQUEST_URI'];
3283 $host = $_SERVER['HTTP_HOST'];
3284
3285 // Construct the string to sign.
3286 $toSign = "POST\n{$path_and_query}\n{$expected_date};{$host};{$hashed_payload}";
3287
3288 // Generate the HMAC signature.
3289 $signature = base64_encode(hash_hmac('sha256', $toSign, $secret, true));
3290
3291 // Construct the authorization string.
3292 $auth = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={$signature}";
3293
3294 // Compare the generated auth string with the expected one.
3295 // Hash_equals is used to mitigate timing attacks NT 2023-12-22
3296 return hash_equals($auth, $expected_auth);
3297 }
3298
3299 // Helper function to validate the date NT 2023-12-22
3300 private function isDateValid($dateHeader) {
3301 // Define the acceptable time leeway (e.g., 5 minutes)
3302 $leewayInSeconds = 300;
3303
3304 // Convert the header date to a Unix timestamp
3305 $headerTime = strtotime($dateHeader);
3306
3307 // Check if the date is valid
3308 if ($headerTime === false) {
3309 return false; // Invalid date
3310 }
3311
3312 // Get the current time
3313 $currentTime = time();
3314
3315 // Check if the date is within the acceptable range
3316 return abs($currentTime - $headerTime) <= $leewayInSeconds;
3317 }
3318
3319
3320 // Helper function to get ISO-3166 two-letter country codes from country names as supplied by Vipps
3321 // IOK 2021-11-22 Seems as if Vipps is now sending two-letter country codes at least some times
3322 public function country_to_code($countryname) {
3323 if (!$this->countrymap) $this->countrymap = unserialize(file_get_contents(dirname(__FILE__) . "/lib/countrycodes.php"));
3324 $mapped = @$this->countrymap[strtoupper($countryname)];
3325 $code = WC()->countries->get_base_country();
3326 if ($mapped) {
3327 $code = $mapped;
3328 } else if (strlen($countryname)==2) {
3329 $code = strtoupper($countryname);
3330 }
3331 $code = apply_filters('woo_vipps_country_to_code', $code, $countryname);
3332 return $code;
3333 }
3334
3335 // To be added to the 'woocommerce_session_handler' filter IOK 2021-06-21
3336 public static function getCallbackSessionClass ($handler) {
3337 return "VippsCallbackSessionHandler";
3338 }
3339
3340 // Go back to the basic woocommerce session handler if we have temporarily restored session from an Vipps order 2021-06-21
3341 // Only to be called by wp-cron, callbacks etc. Will not actually destroy the stored session, just the current session.
3342 public function callback_destroy_session () {
3343 $this->callbackorder = null;
3344 remove_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass'));
3345 if (version_compare(WC_VERSION, '3.6.4', '>=')) {
3346 // This will replace the old session with this one. IOK 2019-10-22
3347 WC()->initialize_session();
3348 } else {
3349 // Do this manually for 3.6.3 and below
3350 WC()->session = new WC_Session_Handler();
3351 WC()->session->init();
3352 }
3353 }
3354
3355 // When we get callbacks from Vipps, we want to restore the Woo session in place for the order.
3356 // For many plugins this is strictly neccessary because they don't check to see if there is a session
3357 // or not - and for many others, wrong results are produced without the (correct) session. IOK 2019-10-22
3358 public function callback_restore_session ($orderid) {
3359 $this->callbackorder = $orderid;
3360 require_once(dirname(__FILE__) . "/VippsCallbackSessionHandler.class.php");
3361 add_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass'));
3362 // Support older versions of Woo by inlining initialize session IOK 2019-12-12
3363 if (version_compare(WC_VERSION, '3.6.4', '>=')) {
3364 // This will replace the old session with this one. IOK 2019-10-22
3365 WC()->initialize_session();
3366 } else {
3367 // Do this manually for 3.6.3 and below
3368 $session_class = "VippsCallbackSessionHandler";
3369 WC()->session = new $session_class();
3370 WC()->session->init();
3371 }
3372
3373 $customerid= 0;
3374 if (WC()->session && is_a(WC()->session, 'WC_Session_Handler')) {
3375 $customerid = WC()->session->get('express_customer_id');
3376 }
3377 if ($customerid) {
3378 WC()->customer = new WC_Customer($customerid); // Reset from session, logged in user
3379 } else {
3380 WC()->customer = new WC_Customer(); // Reset from session
3381 }
3382 // This is to provide defaults; real address will come from Vipps in this sitation. IOK 2019-10-25
3383 WC()->customer->set_billing_address_to_base();
3384 WC()->customer->set_shipping_address_to_base();
3385
3386 // The normal "restore cart from session" thing runs on wp_loaded, and only there, and cannot
3387 // be called from outside the WC_Cart object. We cannot easily run this on wp_loaded, and it does
3388 // do much more than it should for this particular use:
3389 // We have already created the order, so we only want this cart for the shipping calculations.
3390 // Therefore, we will just recreate the 'data' bit of the contents and set the cart contents directly
3391 // from the now restored session. IOK 2020-04-08
3392 // IOK 2022-06-28 Updated to also call the woocommerce_get_cart_item_from_session filters and to correctly handle
3393 // coupons.
3394 $newcart = array();
3395 if (WC()->session->get('cart', false)) {
3396 foreach(WC()->session->get('cart',[]) as $key => $values) {
3397 $product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );
3398 $session_data = array_merge($values, array( 'data' => $product));
3399 $newcart[$key] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );
3400 }
3401 } else {
3402 $this->log(sprintf(__("Could not restore cart from session of order %1\$d", 'woo-vipps'), $orderid));
3403 }
3404 if (WC()->cart) {
3405
3406 // When doing "calculate_totals" on a cart, Woo will now compare "previous shipping methods" with
3407 // "current shipping methods" and reset the chosen shipping methods even if it is still available.
3408 // This becomes a problem because Woo only loads the pickup location methods in a few places - mostly checkout -
3409 // so if we chose a shipping method while these were available, we'd get ourselves reset just by calculating
3410 // cart totals. Fix this by saving and restoring this value. IOK 2025-11-05
3411 $all_chosen = WC()->session->get( 'chosen_shipping_methods' );
3412
3413 WC()->cart->set_totals( WC()->session->get( 'cart_totals', null ) );
3414 WC()->cart->set_applied_coupons( WC()->session->get( 'applied_coupons', array() ) );
3415 WC()->cart->set_coupon_discount_totals( WC()->session->get( 'coupon_discount_totals', array() ) );
3416 WC()->cart->set_coupon_discount_tax_totals( WC()->session->get( 'coupon_discount_tax_totals', array() ) );
3417 WC()->cart->set_removed_cart_contents( WC()->session->get( 'removed_cart_contents', array() ) );
3418 WC()->cart->set_cart_contents($newcart);
3419 // IOK 2020-07-01 plugins expect this to be called: hopefully they'll not get confused by it happening twice
3420 do_action( 'woocommerce_cart_loaded_from_session', WC()->cart);
3421 WC()->cart->calculate_totals(); // And if any of them changed anything, recalculate the totals again!
3422 // See above: Reset chosen shipping methods to avoid having it be reset by Woo for no good reason.
3423 if ($all_chosen) {
3424 WC()->session->set('chosen_shipping_methods', $all_chosen);
3425 }
3426 } else {
3427 // Apparently this happens quite a lot, so don't log it or anything. IOK 2021-06-21
3428 }
3429 return WC()->session;
3430 }
3431
3432
3433
3434 // Based on either a logged-in user, or the stores' default address, get the address to use when using
3435 // the Express Checkout static shipping feature
3436 // This is neccessary because WC()->customer->set_shipping_address_to_base() only sets country and state.
3437 // IOK 2020-03-18
3438 public function get_static_shipping_address_data () {
3439 // This is the format used by the Vipps callback, we are going to mimic this.
3440 // IOK 2025-05-08 now also using the format used by Checkout in addition to Express. -- streetAddress, postalCode, region
3441 $defaultdata = array('addressId'=>0, "addressLine1"=>"", "addressLine2"=>"", "streetAddress"=>"", "country"=>"NO", "city"=>"", "postalCode"=>"", "postCode"=>"", "addressType"=>"Home");
3442 // IOK 2025-08-14 previously this used the customers' address if logged in or available, but that I think was a mistake - since this is intended to be static, ensure we use the base address only.
3443 $countries=new WC_Countries();
3444 $defaultdata['country'] = $countries->get_base_country();
3445 $defaultdata['city'] = $countries->get_base_city();
3446 $defaultdata['region'] = $countries->get_base_city();
3447 $defaultdata['postalCode'] = $countries->get_base_postcode();
3448 $defaultdata['postCode'] = $countries->get_base_postcode();
3449 $defaultdata['streetAddress'] = $countries->get_base_address();
3450 $defaultdata['addressLine1'] = $countries->get_base_address();
3451 return $defaultdata;
3452 }
3453
3454 // Getting shipping methods/costs for a given order to Vipps for express checkout
3455 public function vipps_shipping_details_callback() {
3456 Vipps::nocache();
3457
3458 $raw_post = @file_get_contents( 'php://input' );
3459 $result = @json_decode($raw_post,true);
3460
3461 if (!$result) {
3462 if (empty(trim($raw_post))) {
3463 status_header(400, "Empty address info");
3464 print "No address";
3465 } else {
3466 status_header(400, "Invalid JSON");
3467 print "Invalid JSON";
3468 }
3469 $error = json_last_error_msg();
3470 $this->log(sprintf(__("Error getting customer data in the %1\$s shipping details callback: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $error));
3471 $this->log(__("Raw input was ", 'woo-vipps'));
3472 $this->log($raw_post);
3473 exit();
3474 }
3475
3476 // IOK 2025-08-15 Express Checkout (now) passes the reference/order-id in the data, but Checkout passes it in the URL, which we
3477 // capture in a callback= parameter added at the end. Format is
3478 // '/v3/checkout/woodigitalt4780/shippingDetails'
3479 $vippsorderid = "";
3480 $callback = sanitize_text_field($_REQUEST['callback'] ?? "");
3481 do_action('woo_vipps_shipping_details_callback', $result,$raw_post,$callback); // This is for debugging. IOK 2025-08-15
3482
3483 if ($callback) {
3484 $data = array_reverse(explode("/",$callback));
3485 $vippsorderid = !empty($data) ? ($data[1] ?? "") : ""; // Second element - callback is /v3/checkout/woodigitalt4780/shippingDetails
3486 } elseif (isset($result['reference'])) {
3487 $vippsorderid = $result['reference'];
3488 }
3489
3490 $orderid = intval($_REQUEST['id'] ?? 0);
3491 if (!$orderid) {
3492 status_header(404, "Unknown order");
3493 print "Unknown order";
3494 $this->log(sprintf(__('Could not find %1$s order with id:', 'woo-vipps'), $this->get_payment_method_name()) . " " . $vippsorderid . "\n" . __('Callback was:', 'woo-vipps') . " " . $callback, 'error');
3495 exit();
3496 }
3497
3498 // This is for debugging sites where shipping handling fails because of blocks etc IOK 2026-01-15
3499 $this->log(sprintf(__("Received shipping callback for order %d", 'woo-vipps'), $orderid));
3500
3501 do_action('woo_vipps_shipping_details_callback_order', $orderid, $vippsorderid);
3502
3503 $order = wc_get_order($orderid);
3504 if (!$order) {
3505 status_header(404, "Unknown order");
3506 print "Unknown order";
3507 $this->log(__('Could not find Woo order with id:', 'woo-vipps') . " " . $orderid, 'error');
3508 exit();
3509 }
3510 if (!self::is_vipps_order($order)) {
3511 status_header(400, "Invalid order");
3512 print "Invalid order";
3513 $this->log(__('Invalid order for shipping callback:', 'woo-vipps') . " " . $orderid, 'error');
3514 exit();
3515 }
3516 // a small bit of security
3517 if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) {
3518 status_header(403, "Wrong auth");
3519 print "Wrong auth";
3520 $this->log("Wrong authtoken on shipping details callback", 'error');
3521 exit();
3522 }
3523 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3524 status_header(400, "Invalid order id");
3525 print "Invalid order id";
3526 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3527 exit();
3528 }
3529
3530 // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3531 // special support for Checkout that this is in fact happening. IOK 2023-01-19
3532 // This needs to be done before "calculate totals".
3533 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3534 $ischeckout = $order->get_meta('_vipps_checkout');
3535
3536 $this->callback_restore_session($orderid);
3537
3538 // 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
3539 // here we will add support for PickupLocations. Also called for static shipping.
3540 // IOK 2025-08-14 now also supported for Express Checkout
3541 $this->load_extra_shipping_methods($order, $result, $ischeckout);
3542
3543 $return = $this->vipps_shipping_details_callback_handler($order, $result,$vippsorderid, $ischeckout);
3544
3545 // Express checkout wants the data wrapped in a object with a 'groups' attribute, Checkout wants thing unwrapped.
3546 // Dispatch on the known type. IOK 2025-08-15
3547 if ($ischeckout) {
3548 $return = $return['shippingDetails'];
3549 } else {
3550 // Note that this is of course different from both Checkout and static shipping.
3551 $return = [ "groups" => $return ];
3552 }
3553
3554 $json = json_encode($return);
3555
3556 header("Content-type: application/json; charset=UTF-8");
3557 print $json;
3558 // Just to be sure, save any changes made to the session by plugins/hooks IOK 2019-10-22
3559 if (is_a(WC()->session, 'WC_Session_Handler')) WC()->session->save_data();
3560 exit();
3561 }
3562
3563 // This function calculates and returns one of two possible JSON representations to Vipps MobilePay, one for Express and one for Checkout.
3564 // First, an intermediate representation is created, based on the original Express API. This is kept because users may still have filters
3565 // that expects this representation. Later, these are transformed and augmented for the newer APIs. IOK 2025-08-14
3566 // Also used for Static Shipping for both representations. IOK 2025-08-14
3567 public function vipps_shipping_details_callback_handler($order, $vippsdata,$vippsorderid, $ischeckout) {
3568 // This filter is used in sub-functions to keep track of what we are calculating for, without having to set globals or pass arguments. IOK 2025-08-14
3569 if ($ischeckout) add_filter('woo_vipps_is_vipps_checkout', '__return_true');
3570
3571 // We may have an address already in the Order, and no *new* address, when recalculating shipping options after modifying the order.
3572 // We'll still create a $vippsdata struct so that old filters can do whatever is neccessary. IOK 2025-09-16
3573 $new_address = !empty($vippsdata);
3574 if (!$new_address) {
3575 $vippsdata['addressLine1'] = $order->get_shipping_address_1();
3576 $vippsdata['addressLine2'] = $order->get_shipping_address_2();
3577 $vippsdata['postCode'] = $order->get_shipping_postcode();
3578 $vippsdata['city'] = $order->get_shipping_city();
3579 $vippsdata['country'] = $order->get_shipping_country();
3580 }
3581
3582 // Since we have legacy users that may have filters defined on these values, we will translate newer apis to the older ones.
3583 // so filters will continue to work for newer apis/checkout
3584 if (isset($vippsdata['streetAddress'])){
3585 $vippsdata['addressLine1'] = $vippsdata['streetAddress'];
3586 $vippsdata['addressLine2'] = "";
3587 }
3588 if (isset($vippsdata['region'])) {
3589 $vippsdata['city'] = $vippsdata['region'];
3590 }
3591 if (isset($vippsdata['postalCode'])) {
3592 $vippsdata['postCode'] = $vippsdata['postalCode'];
3593 }
3594 // Translations for different versions of the API end
3595
3596 $addressid = isset($vippsdata['addressId']) ? $vippsdata['addressId'] : "";
3597 $addressline1 = $vippsdata['addressLine1'];
3598 $addressline2 = $vippsdata['addressLine2'];
3599
3600 // IOK 2019-08-26 apparently the apps contain a lot of addresses with duplicate lines
3601 if ($addressline1 == $addressline2) $addressline2 = '';
3602 if (!$addressline2) $addressline2 = '';
3603
3604 $country = $vippsdata['country'];
3605 $city = $vippsdata['city'];
3606 $postcode= $vippsdata['postCode'];
3607
3608 // Old code here treated "Sofienberggata 12" as a special Vipps pro-forma address; this is no longer necessary.
3609 // If we have gotten a new address from Express or Checkout, update the order. IOK 2025-09-16.
3610 if ($new_address) {
3611 $order->set_billing_address_1($addressline1);
3612 $order->set_billing_address_2($addressline2);
3613 $order->set_billing_city($city);
3614 $order->set_billing_postcode($postcode);
3615 $order->set_billing_country($country);
3616 $order->set_shipping_address_1($addressline1);
3617 $order->set_shipping_address_2($addressline2);
3618 $order->set_shipping_city($city);
3619 $order->set_shipping_postcode($postcode);
3620 $order->set_shipping_country($country);
3621 $order->save();
3622 }
3623
3624 // This is *essential* to get VAT calculated correctly. That calculation uses the customer, which uses the session.IOK 2019-10-25
3625 // We don't *save* this to the customer, because this may happen in a callback from Checkout where the customers' session is live and
3626 // the address info is from Checkout (and not necessarily the customers real address). IOK 2025-09-12
3627 if (WC()->customer) {
3628 WC()->customer->set_billing_location($country,'',$postcode,$city);
3629 WC()->customer->set_shipping_location($country,'',$postcode,$city);
3630 } else {
3631 $this->log("No customer! when trying to calculate shipping");
3632 }
3633
3634 // If you need to do something before the cart is manipulated, this is where it must be done.
3635 // It is possible for a plugin to require a session when manipulating the cart, which could
3636 // currently crash the system. This could be used to avoid that. IOK 2019-10-09
3637 do_action('woo_vipps_shipping_details_before_cart_creation', $order, $vippsorderid, $vippsdata);
3638
3639 // calculate_totals() overwrites the session chosen_shipping_methods to default if it think it changed,
3640 // which will be true if the pickup points are missing from previously. Pickup points only get loaded in woos checkout.
3641 // So reset this to what it was before calling calculate_totals(). LP 2025-11-05
3642 // To be more specific if the *list of available methods* change, it will reset the chosen shipping method,
3643 // even if the chosen shipping method is actually still available. We need to call calculate_totals on the cart,
3644 // so we need to save + restore this.
3645 $chosen = null;
3646 $all_chosen = null;
3647 if (is_a(WC()->session, 'WC_Session_Handler')) {
3648 $all_chosen = WC()->session->get( 'chosen_shipping_methods' );
3649 if (!empty($all_chosen)) $chosen= $all_chosen[0];
3650 }
3651
3652 // Previously, we would create a shoppingcart at this point, because we would not have access to the 'live' one,
3653 // but it turns out this isn't actually possible. Any cart so created will become "the" cart for the Woo front end,
3654 // and anyway, some plugins override the class of the cart, so just using WC_Cart will sometimes break.
3655 // Now however, the session is stored in the order, and the cart will not have been deleted, so we should
3656 // now be able to calculate shipping for the actual cart with no further manipulation. IOK 2020-04-08
3657
3658 // Turns out it is possible for the session - and the cart - to have been deleted at this point, for whatever reason.
3659 // Login will do it, probably some other plugins as well. So if we have no cart at this point, we will ressurect the
3660 // probable cart based on the order. This is only neccessary because Woo will not let us calculate shipping for an *order*.
3661 // IOK 2024-04-09
3662 $cart_is_reconstructed = $this->maybe_reconstruct_cart($order->get_id());
3663
3664 WC()->cart->calculate_totals();
3665
3666 // See above. Restore chosen shipping methods if neccessary. IOK 2025-11-05
3667 if ($all_chosen) {
3668 WC()->session->set('chosen_shipping_methods', $all_chosen);
3669 }
3670
3671 $acart = WC()->cart;
3672
3673 $shipping_methods = array();
3674 $shipping_tax_rates = WC_Tax::get_shipping_tax_rates();
3675
3676
3677 // If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20
3678 if (!$acart->needs_shipping()) {
3679 $no_shipping_taxes = WC_Tax::calc_shipping_tax('0', $shipping_tax_rates);
3680 $shipping_methods['none_required:0'] = new WC_Shipping_Rate('none_required:0',__('No shipping required','woo-vipps'),0,$no_shipping_taxes, 'none_required', 0);
3681 } else {
3682 // Ensure the shipping packages we use has the current order address IOK 2025-09-12
3683 $destination = [ 'country' => $country, 'state' => '', 'postcode' => $postcode, 'city'=> $city, 'address' => $addressline1, 'address_1' => $addressline1, 'address_2' => $addressline2 ];
3684 add_filter('woocommerce_cart_shipping_packages', function ($packages) use($destination) {
3685 $new = [];
3686 foreach($packages as $package) {
3687 $package['destination'] = $destination;
3688 $new[] = $package;
3689 }
3690 return $new;
3691 });
3692
3693 $packages = apply_filters('woo_vipps_shipping_callback_packages', WC()->cart->get_shipping_packages());
3694 $shipping = WC()->shipping->calculate_shipping($packages);
3695
3696 $shipping_methods = WC()->shipping->packages[0]['rates']; // the 'rates' of the first package is what we want.
3697 }
3698
3699 // No exit here, because developers can add more methods using the filter below. IOK 2018-09-20
3700 if (empty($shipping_methods)) {
3701 $name = $ischeckout ? Vipps::CheckoutName() : Vipps::ExpressCheckoutName();
3702 $this->log(sprintf(__('Could not find any applicable shipping methods for %1$s - order %2$d will fail', 'woo-vipps', 'warning'), $name, $order->get_id()), 'debug');
3703 $this->log(sprintf(__('Address given for %1$s was %2$s', 'woo-vipps'), $order->get_id(),
3704 ($addressline1 . " " . $addressline2 . " " . $city . " " . $postcode . " " . $country)
3705 ), 'debug');
3706
3707 }
3708
3709 // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3710 // 'dynamic pricing' 2023-01-26
3711 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3712 $taxrate = 0;
3713 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
3714 $taxrate = current($shipping_tax_rates)['rate'];
3715 }
3716 $order->update_meta_data('_vipps_shipping_tax_rates', $taxrate);
3717
3718 // Merchant is using the old 'woo_vipps_shipping_methods' filter, and hasn't chosen to disable it. Use legacy methd.
3719 // IOK 2025-08-14 I think we should add a deprecation notice to this now. It really should not be used anymore. FIXME
3720 if (has_action('woo_vipps_shipping_methods') && $this->gateway()->get_option('newshippingcallback') != 'new') {
3721 return $this->legacy_shipping_callback_handler($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart);
3722 }
3723
3724 // Earlier we sorted shipping methods based on price; currently we just use WooCommerce's order, but we
3725 // provide this filter for people who would prefer the old logic.
3726 $shipping_methods = apply_filters('woo_vipps_sort_shipping_methods', $shipping_methods, $order);
3727
3728 // IOK 2020-02-13 Ok, new method! We are going to provide a list full of metadata for the users to process this time, which we will massage into the final Vipps method list
3729 $methods = array();
3730 $i=-1;
3731
3732 foreach ($shipping_methods as $key=>$rate) {
3733 $i++;
3734 $method = array();
3735 $method['priority'] = $i;
3736 $method['default'] = false;
3737 $method['rate'] = $rate;
3738 $methods[$key]= $method;
3739 }
3740 $chosen = apply_filters('woo_vipps_default_shipping_method', $chosen, $shipping_methods, $order);
3741
3742 if ($chosen && !isset($methods[$chosen])) {
3743 $chosen = null; // Actually that isn't available
3744 $this->log(sprintf(__("Unavailable shipping method set as default in the %1\$s Express Checkout shipping callback - check the 'woo_vipps_default_shipping_method' filter",'debug'), $this->get_payment_method_name()));
3745 }
3746
3747 if (!$chosen) {
3748 // Find first method that isn't 'local_pickup'
3749 // or pickup_location. IOK 2025-05-07
3750 foreach($methods as $key=>&$data) {
3751 $mid = $data['rate']->get_method_id();
3752 if ($mid != 'local_pickup' && $mid != 'pickup_location') {
3753 $chosen = $key;
3754 break;
3755 }
3756 }
3757 // Ok, just pick the first
3758 if (!$chosen) {
3759 foreach($methods as $key=>&$data) {
3760 $chosen = $key;
3761 break;
3762 }
3763
3764 }
3765 }
3766 if (isset($methods[$chosen])) {
3767 $methods[$chosen]['default'] = true;
3768 }
3769 $methods = apply_filters('woo_vipps_express_checkout_shipping_rates', $methods, $order, $acart);
3770
3771 // Just to be sure, if the current cart was reconstructed from an order, we will delete it now after
3772 // last use of $acart
3773 if ($cart_is_reconstructed) {
3774 WC()->cart->empty_cart();
3775 }
3776
3777 $vippsmethods = array();
3778
3779 // Just a utility from shippingMethodIds to the non-serialized rates, and from the same to the non-serialized
3780 // shipping methods - the last stores settings, the first store metadata
3781 // The ratemap will be used to store a table in the order from an arbitrary ID key to the calculated shipping rate IOK 2025-08-15
3782 $ratemap = array();
3783 $methodmap = array();
3784
3785 // We need access to the extended settings of the shipping methods.
3786 // This is for the 'new' local pickup feature for Woo. IOK 2025-08-14
3787 $methods_classes = WC()->shipping->get_shipping_method_class_names();
3788 $methods_classes['pickup_location'] = 'Automattic\WooCommerce\Blocks\Shipping\PickupLocation'; // Loaded using the "load" hook, after the registered methods, so we need to add it specially.
3789
3790 // Store a table of ratemap key => WC_Shipping_Rate id in the session, so we don't have to load and deserialize the rates from the ratemap,
3791 // e.g used in the Checkout ajax poll shipping-change event. LP 2026-03-20
3792 $rate_id_map = [];
3793
3794 $has_free_shipping = false;
3795 foreach($methods as $method) {
3796 $rate = $method['rate'];
3797 $methodid = $rate->get_method_id();
3798
3799 // Extended settings are stored in these objects
3800 $methodclass = $methods_classes[$methodid] ?? null;
3801 $shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null;
3802
3803 $tax = $rate->get_shipping_tax() ?: 0;
3804 $cost = $rate->get_cost() ?: 0;
3805 $label = html_entity_decode($rate->get_label());
3806
3807 if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) {
3808 $has_free_shipping = true;
3809 }
3810
3811 // We can't just use the method id, because the customer may have different addresses. Just to be sure, hash the entire method and use as a key.
3812 // Actually, we probably *can* use the method id, because other addresses are irellevant. But still, add a random factor
3813 $rand = md5($methodid . bin2hex(random_bytes(32))); // Random enough, 32 chars
3814 // Ensure this never is over 100 chars. Use a dollar sign to indicate 'new method' IOK 2020-02-14
3815 // Reserve 8 chars to contain a : and an option index for Express Checkout IOK 2025-08-15
3816 // IOK 2025-08-14 "new" method is the current system; the legacy system has shipping method ids with different naming conventions. Again, to be deprecated. FIXME.
3817 $key = '$' . substr($methodid,0,58) . '$' . $rand;
3818 $vippsmethod = array();
3819 $vippsmethod['isDefault'] = @$method['default'] ? 'Y' :'N';
3820 $vippsmethod['priority'] = $method['priority'];
3821
3822 $rate_id_map[$key] = $rate->get_id();
3823
3824 // It seems woo actually computes rounding of prices and taxes *separately* when computing
3825 // shipping costs, but we can't really assume this (or that all plugins do this, and so on.)
3826 // Therefore we compute shipping cost with rounding *both ways* and choose the more expensive one -
3827 // this way we should reserve enough money to complete the order in all cases. IOK 2025-09-30
3828 $shippingcostA = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
3829 $shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,''));
3830 $shippingcost = max($shippingcostA, $shippingcostB);
3831
3832 $vippsmethod['shippingCost'] = $shippingcost;
3833 $vippsmethod['shippingMethod'] = html_entity_decode($rate->get_label());
3834 $vippsmethod['shippingMethodId'] = $key;
3835 $vippsmethods[]=$vippsmethod;
3836
3837 // Metadata and settings stored for later use for Checkout
3838 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3839 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3840 // finalize the order. IOK 2025-08-15
3841 $ratemap[$key]=$rate;
3842 $methodmap[$key]=$shipping_method;
3843 }
3844
3845 if (is_a(WC()->session, 'WC_Session')) {
3846 WC()->session->set('vipps_shipping_rate_id_map', $rate_id_map);
3847 } else {
3848 /* translators: order id */
3849 $this->log(sprintf(__('Could not store shipping rate id map in session for order %1$s, session was not ok', 'woo_vipps'), $order->get_id()), 'error');
3850 }
3851
3852
3853 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3854 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3855 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3856
3857 // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3858 // IOK 2025-08-15 and new Express yet another slightly different format.
3859 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3860 if ($ischeckout) {
3861 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
3862 } else { // New express format. LP 2025-05-26
3863 $return = $this->express_format_shipping_methods($return, $ratemap, $methodmap, $order);
3864 $return = $this->express_group_shipping_methods($return, $ratemap, $methodmap, $order);
3865 $return = apply_filters('woo_vipps_express_json_shipping_methods', $return, $order); // wat
3866 }
3867
3868 // We need to store the WC_Shipping_Rate objects with all its meta data in the database until return from Vipps. IOK 2020-02-17
3869 $storedmethods = array();
3870 $errormethods = array();
3871 foreach($ratemap as $key => $rate) {
3872 $serialized = '';
3873 try {
3874 // We use serialize here instead of json_encode because we need the object back.
3875 // we base64-encode the serialized object, because it is to be stored in a database in a text field.a IOK 2025-12-12
3876 $raw = @serialize($rate);
3877 $serialized = $raw ? @base64_encode($raw) : null;
3878 if (!$serialized) {
3879 throw new Exception("Could not serialize rate $key");
3880 }
3881 // Retrieve these precalculated rates on return from the store IOK 2020-02-14
3882 $storedmethods[$key] = $serialized;
3883 } catch (Exception $e) {
3884 $errormethods[] = $key;
3885 $this->log(sprintf(__("Cannot use shipping method %2\$s in %1\$s Express checkout: the shipping method isn't serializable.", 'woo-vipps'), $this->get_payment_method_name(), $label), 'error');
3886 $this->log($rate, 'error');
3887 continue;
3888 }
3889 }
3890
3891 // Remove any methods from the return that was not serializable
3892 if (!empty($errormethods)) {
3893 $fixedreturn = [];
3894 if ($ischeckout) {
3895 foreach($errormethods as $problem) {
3896 foreach($return['shippingDetails'] as $method) {
3897 $id = preg_replace('!:\d+$!', "", $method['id']);
3898 if ($id != $problem) $fixedreturn[] = $method;
3899 }
3900 }
3901 $return['shippingDetails'] = $fixedreturn;
3902 } else {
3903 foreach($errormethods as $problem) {
3904 foreach($return as $method) {
3905 $option = $method['options'][0];
3906 $id = preg_replace('!:\d+$!', "", $option['id']);
3907 if ($id != $problem) $fixedreturn[] = $method;
3908 }
3909 }
3910 $return = $fixedreturn;
3911 }
3912 }
3913
3914
3915 // We'll also store whether or not this set of rates include free shipping in some way. IOK 2025-09-16
3916 $storedmethods['_meta_has_free_shipping'] = $has_free_shipping;
3917 $storedmethods['_is_base64'] = true;
3918
3919 $order->update_meta_data('_vipps_express_checkout_shipping_method_table', $storedmethods);
3920 $order->save_meta_data();
3921 return $return;
3922 }
3923
3924 // Translate from the old to the new express format. LP 2025-05-26
3925 public function express_format_shipping_methods ($return, &$ratemap, $methodmap, $order) {
3926 $translated = array();
3927 $currency = $order->get_currency();
3928
3929 // First, we'll translate the legacy format originally used by express to the new one (that may
3930 // still be in use by filters etc), then add hooks to modify options and other new features.
3931 // IOK 2025-11-19
3932 foreach ($return['shippingDetails'] as $m) {
3933 $m2 = array();
3934 $options = [];
3935
3936 $m2['isDefault'] = ($m['isDefault']=='Y') ? true : false;
3937 $m2['priority'] = $m['priority'];
3938 $m2['brand'] = 'OTHER'; // the default. This is replaced for certain brands. LP 2025-05-26
3939 $m2['type'] = 'OTHER'; // default, replaced for certain types. LP 2025-05-26
3940
3941 $id = $m['shippingMethodId'];
3942 $rate = $ratemap[$id];
3943 $shipping_method = $methodmap[$id];
3944
3945 if ($rate->method_id == 'pickup_location') {
3946 $m2['type'] = 'PICKUP_POINT';
3947 }
3948
3949 // Each shipping method needs a list of options at this point.
3950 $options = [];
3951
3952
3953 // A rate can have a delivery time as a string in both Woo and Express
3954 $delivery_time = "";
3955 if (version_compare(WC_VERSION, '9.2.0', '>=')) {
3956 $delivery_time = $rate->get_delivery_time();
3957 }
3958
3959 // And some rates have metadata, such as pickup locations (local_delivery).
3960 $meta = $rate->get_meta_data();
3961 // We can also support descriptions, in the "meta" field
3962 $description = $rate->get_description();
3963
3964 $option = [];
3965 $option['priority'] = $m['priority'];
3966 $option['name'] = $m['shippingMethod'];
3967 $option['id'] = $id;
3968 $option['amount'] = [ 'value' => round(100*$m['shippingCost']), 'currency' => $currency ];
3969 if ($delivery_time) $option['estimatedDelivery'] = $delivery_time;
3970 if ($description) $entry['meta'] = $description;
3971 $options[] = $option;
3972
3973 if (isset($meta['brand'])) {
3974 $m2['brand'] = $meta['brand'];
3975 } else {
3976 // specialcase some known methods so they get brands, and put the label into the description
3977 if ($shipping_method && is_a($shipping_method, 'WC_Shipping_Method') && get_class($shipping_method) == 'WC_Shipping_Method_Bring_Pro') {
3978 $m2['brand'] = "POSTEN";
3979 }
3980 $m2['brand'] = apply_filters('woo_vipps_shipping_method_brand', $m2['brand'],$shipping_method, $rate);
3981 }
3982
3983 if ($m2['brand'] != "OTHER" && isset($meta['type'])) {
3984 $m2['type'] = apply_filters('woo_vipps_shipping_method_type', $meta['type'], $shipping_method, $rate);
3985 }
3986 $m2['options'] = $options;
3987
3988 // Now allow custom code to modify both the rate (adding metadata, mostly) and the Vipps shipping method (probably adding
3989 // options, changing the brand etc) IOK 2025-11-19
3990 // For an example, see the express_add_pickup_location_options method. IOK 2025-11-19
3991 list ($rate, $m2) = apply_filters('woo_vipps_modify_express_checkout_rate', [$rate, $m2], $shipping_method, $rate, $order);
3992 $ratemap[$id] = $rate; // Modify the ratemaps copy with any new data here - ratemap is passed by reference IOK 2025-11-19
3993
3994 $translated[] = $m2;
3995 }
3996
3997 return $translated;
3998 }
3999
4000 // This adds extra options for express checkout shipping rates that implement the 'woo_vipps_shipping_method_pickup_points' filter,
4001 // making these into groups with a dropdown for the exact shipping location as separate options.
4002 // This will create multiple pointers to the same shipping rate, which will be extended with a metadata field containing the pickup point.
4003 // That is, this is *not* for local_pickup, but for legacy local pickup and other shipping methods that have the same rate price, but
4004 // allows the user to select a location. IOK 2025-08-15
4005 public function express_add_pickup_location_options ( $data, $shipping_method, $rate, $order) {
4006 list ($rate, $m2) = $data;
4007 $pickup_points = apply_filters('woo_vipps_shipping_method_pickup_points', [], $rate, $shipping_method, $order);
4008 if (empty($pickup_points)) return $data;
4009 if (count($m2['options'])>1) return $data;
4010
4011 $index = 0;
4012 $pickup_point_table = [];
4013 $option = $m2['options'][0];
4014 $id = $option['id'];
4015
4016 foreach($pickup_points as $point) {
4017 $index++; // Start at 1
4018 $entry = $option; // This is a copy in PHP
4019
4020 $addr = [];
4021 foreach(['name', 'address', 'postalCode', 'city', 'country'] as $key) {
4022 $v = trim($point[$key]);
4023 if (!empty($v)) $addr[$key] = $v;
4024 }
4025 // To avoid confusion, force the keys to be strings. IOK 2025-08-15
4026 $pickup_point_table["i".$index] = $addr;
4027
4028 // This is for display in the App only IOK 2025-08-15
4029 $description = join(", ", array_values($addr));
4030 $description = trim(apply_filters('woo_vipps_shipping_option_meta', trim($description, " ,"), $rate, $shipping_method, $order));
4031 if ($description) $entry['meta'] = $description;
4032
4033 // IOK 2025-06-04 Since we are here mapping several Express rates to a single Woo rate,
4034 // we need to add a suffix, which is removed in gw->set_order_shipping_details().
4035 $entry['id'] = $id . ":" . $index;
4036 $entry['name'] = $point['name'];
4037 $options[] = $entry;
4038 }
4039 // If we have pickup points added, then store them in a table in the rate itself. We'll strip that value when finalizing the order. IOK 2025-08-15
4040 // This gets stored in the orders ratemap on return. IOK 2025-11-19
4041 if (!empty($pickup_point_table)) {
4042 $rate->add_meta_data('_vipps_pickupPoints', $pickup_point_table);
4043 }
4044 $m2['options'] = $options;
4045 $m2['type'] = 'PICKUP_POINT';
4046
4047 return [$rate, $m2];
4048 }
4049
4050
4051 // Group certain shipping methods together in the new express format, into a group of options for one method (for example pickup locations). LP 2025-06-04
4052 // $order not used, will keep for now so to have a similar signature to express_format_shipping_methods, also it might be used in future change of this method. LP 2025-08-18
4053 public function express_group_shipping_methods($methods, &$ratemap, $methodmap, $order) {
4054 if (!$methods) return $methods;
4055 $grouped = [];
4056 $maybe_groupable_methods = $methods;
4057 while (!empty($maybe_groupable_methods)) {
4058 $first = array_shift($maybe_groupable_methods);
4059 $first_id = preg_replace("!:.+$!", "", $first['options'][0]['id']); // strip option index from 'augmented' methods.
4060 $first_rate = $ratemap[$first_id];
4061 $first_method = $methodmap[$first_id];
4062
4063 $rest = [];
4064 foreach ($maybe_groupable_methods as $candidate) {
4065 $candidate_id = preg_replace("!:.+$!", "", $candidate['options'][0]['id']); // strip option index from 'augmented' methods.
4066 $candidate_rate = $ratemap[$candidate_id];
4067 $candidate_method = $methodmap[$candidate_id];
4068
4069 // By default, we will group all rates that are pickup_location-s. LP 2025-08-18
4070 $is_pickup = $first_rate->method_id === $candidate_rate->method_id && $first_rate->method_id === 'pickup_location';
4071 $should_group = apply_filters('woo_vipps_express_should_group_shipping_methods', $is_pickup, $first_rate, $first_method, $candidate_rate, $candidate_method);
4072
4073 if ($should_group) {
4074 $first_options = $first['options'];
4075 $second_options = $candidate['options'];
4076
4077 $first['options'] = array_merge($first_options, $second_options);
4078
4079 // Reset default-ness and priority to the highest value from the merged methods.
4080 if ($candidate['isDefault']) $first['isDefault'] = true;
4081 if ($candidate['priority'] < $first['priority']) $first['priority'] = $candidate['priority'];
4082
4083 } else {
4084 $rest[] = $candidate;
4085 }
4086
4087 }
4088 $grouped[] = $first;
4089
4090 // Start over again with the ones who weren't grouped to the first method of the list. LP 2025-08-18
4091 $maybe_groupable_methods = $rest;
4092 }
4093
4094 return $grouped;
4095 }
4096
4097
4098 // In certain situations the session may have no cart, which among other things makes it impossible for us to calculate shipping.
4099 // We must therefore reconstruct the cart as close to what it were before calculating shipping; and we must delete it afterwards
4100 // because it may not be correct wrt meta values and so forth. Based on cart-sessions "populate_cart_from_order" used in the "order again" path.
4101 // Returns "true" if cart is reconstructed from the order, else false.
4102 // IOK 2024-04-08
4103 private function maybe_reconstruct_cart($order_id) {
4104 if (!WC()->cart->is_empty()) return false;
4105 $this->log(sprintf(__("No cart, so will try to calculate shipping based on order contents for order %1\$d", 'woo-vipps'), $order_id), 'error');
4106 try {
4107 $order = wc_get_order( $order_id );
4108 $cart = array();
4109 $inital_cart_size = 0;
4110 $order_items = $order->get_items();
4111 foreach ( $order_items as $item ) {
4112 $product_id = (int) $item->get_product_id();
4113 $quantity = $item->get_quantity();
4114 $variation_id = (int) $item->get_variation_id();
4115 $variations = array();
4116 $cart_item_data = array();
4117 $product = $item->get_product();
4118 if ( ! $product ) {
4119 continue;
4120 }
4121 if ( ! $variation_id && $product->is_type( 'variable' ) ) continue;
4122 // We ignore the out-of-stock rule here, it doesn't matter for shipping in this case IOK 2024-04-09
4123 foreach ( $item->get_meta_data() as $meta ) {
4124 if ( taxonomy_is_product_attribute( $meta->key ) || meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) {
4125 $variations[ $meta->key ] = $meta->value;
4126 }
4127 }
4128 $cart_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data );
4129 $product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
4130 $cart[ $cart_id ] = array_merge(
4131 $cart_item_data,
4132 array(
4133 'key' => $cart_id,
4134 'product_id' => $product_id,
4135 'variation_id' => $variation_id,
4136 'variation' => $variations,
4137 'quantity' => $quantity,
4138 'data' => $product_data,
4139 'data_hash' => wc_get_cart_item_data_hash( $product_data ),
4140 )
4141 );
4142
4143 }
4144 WC()->cart->set_cart_contents($cart);
4145 WC()->cart->calculate_totals();
4146 WC()->cart->set_session();
4147 return true;
4148 } catch (Exception $e) {
4149 $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
4150 return false;
4151 }
4152 }
4153
4154
4155 // IOK 2020-02-13 This method implements the *old* style of providing shipping methods to Vipps Express Checkout.
4156 // It is 'stateless' in that it doesn't need to serialize shipping methods or anything like that - but precisely because of this,
4157 // metadata isn't possible to provide, and it reqires to send VAT separately coded into the shipping method ID which is pretty
4158 // clumsy. This method will currently only be used if a merchant has overridden the 'woo_vipps_shipping_methods' filter and hasn't chosen
4159 // the setting that overrides this.
4160 public function legacy_shipping_callback_handler ($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart) {
4161 do_action('woo_vipps_legacy_shipping_methods', $order); // This will probably be mostly for debugging.
4162
4163 // If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20
4164 if (!$acart->needs_shipping()) {
4165 $methods = array(array('isDefault'=>'Y','priority'=>'0','shippingCost'=>'0.00','shippingMethod'=>__('No shipping required','woo-vipps'),'shippingMethodId'=>'Free:Free;0'));
4166 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods);
4167 return $return;
4168 }
4169
4170 $free = 0;
4171 $defaultset = 0;
4172 $methods = array();
4173 foreach ($shipping_methods as $rate) {
4174 $method = array();
4175 $method['priority'] = 0;
4176 $tax = $rate->get_shipping_tax() ?: 0;
4177 $cost = $rate->get_cost() ?: 0;
4178
4179 $method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
4180 $method['shippingMethod'] = html_entity_decode($rate->get_label());
4181 // We may not really need the tax stashed here, but just to be sure.
4182 $method['shippingMethodId'] = $rate->get_id() . ";" . $tax;
4183 $methods[]= $method;
4184
4185 // If we qualify for free shipping, make it the default. Thanks to Emely Bakke for reporting. IOK 2019-11-15
4186 if (preg_match("!^free_shipping!",$rate->get_id())) {
4187 $free=1;
4188 $defaultset=1;
4189 $chosen = $rate->get_id();
4190 }
4191 }
4192 usort($methods, function($method1, $method2) {
4193 return $method1['shippingCost'] - $method2['shippingCost'];
4194 });
4195 $priority=0;
4196 foreach($methods as &$method) {
4197 $rateid = explode(";",$method['shippingMethodId'],2);
4198 if (!empty($rateid) && $rateid[0] == $chosen) {
4199 $defaultset=1;
4200 $method['isDefault'] = 'Y';
4201 } else {
4202 $method['isDefault'] = 'N';
4203 }
4204 $method['priority']=$priority;
4205 $priority++;
4206 }
4207 // If we don't have free shipping, select the first (cheapest) option, unless that is 'local pickup'. IOK 2019-11-26
4208 // Or pickup_location, same thing. IOK 2025-05-07
4209 if(!$defaultset && !empty($methods)) {
4210 foreach($methods as &$method) {
4211 if (!preg_match("!^(local_pickup|pickup_location)!",$method['shippingMethodId'])) {
4212 $defaultset=1;
4213 $method['isDefault'] = 'Y';
4214 break;
4215 }
4216 }
4217 }
4218 // Or the first if we stil have no default method.
4219 if (!$defaultset &&!empty($methods)) {
4220 $methods[0]['isDefault'] = 'Y';
4221 }
4222
4223 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods);
4224 $return = apply_filters('woo_vipps_shipping_methods', $return,$order,$acart);
4225
4226 return $return;
4227 }
4228
4229 public static function nocache() {
4230 wc_nocache_headers();
4231 header("X-Accel-Expires: 0");
4232 }
4233
4234
4235 public function woocommerce_payment_gateways($methods) {
4236 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4237 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
4238 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
4239 $gateway = $this->gateway();
4240 if ($gateway) {
4241 $methods[] = $gateway;
4242 } else {
4243 $methods[] = 'WC_Gateway_Vipps';
4244 }
4245
4246 $methods[] = 'WC_Gateway_VippsCard';
4247
4248 return $methods;
4249 }
4250
4251 // Runs after set_session, so if the session is just created, we'll get called. IOK 2018-06-06
4252 public function woocommerce_cart_updated() {
4253 $this->maybe_set_vipps_as_default();
4254 }
4255
4256 public function woocommerce_add_to_cart_redirect ($url) {
4257 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4258 return $url;
4259 }
4260 $url = $this->express_checkout_url();
4261 // At this point, there is always a query argument here. IOK 2026-09-21
4262 $nonce = wp_create_nonce('express');
4263 $url = $url . "&sec=$nonce";
4264
4265 return $url;
4266 }
4267
4268 // We can't allow a customer to re-call the Vipps Express checkout payment thing twice -
4269 // This would happen if a logged-in user tries to re-start the transaction after breaking it.
4270 // But for express checkout this breaks because there is no shipping method or address, and of course,
4271 // the order id is unique too.. IOK 2018-11-21
4272 public function woocommerce_my_account_my_orders_actions($actions, $order ) {
4273 $pm = $order->get_payment_method();
4274 if (!self::is_vipps_order($pm)) return $actions;
4275
4276 if (!static::order_is_vipps_retryable($order->get_id())) {
4277 unset($actions['pay']);
4278 }
4279 return $actions;
4280 }
4281
4282 // This job runs in the wp-cron context, and is intended to clean up signal files and other temporariy data. IOK 2020-04-01
4283 public function cron_cleanup_hook () {
4284 $this->cleanupCallbackSignals(); // Remove old callback signals (files in uploads)
4285 $this->delete_old_cancelled_orders(); // Remove cancelled express checkout orders if selected
4286 }
4287
4288 // This job runs in the wp-cron context and checks if there are *old* pending orders with payment method Vipps. If so, it will
4289 // check if the status of these orders are now known. This is intended to handle the case where a user does not return
4290 // to the store and the Vipps callback fails for whatever reason. IOK 2021-06-21
4291 public function cron_check_for_missing_callbacks() {
4292 $eightminutesago = time() - (60*8);
4293 $sevendaysago = time() - (60*60*24*7);
4294
4295 // This is compatible with both HPOS and old style order management. IOK 2026-05-27
4296 $pending_app = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
4297 $pending_cards = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps_card', 'date_created' => '>' . $sevendaysago ));
4298 $pending = array_merge($pending_app, $pending_cards);
4299
4300 if (empty($pending)) return;
4301 foreach ($pending as $o) {
4302 $then = $o->get_meta('_vipps_init_timestamp');
4303 if (! $then) continue; # Race condition! We may not have set the timestamp yet. IOK 2022-03-24
4304 if (!$o->get_meta('_vipps_orderid')) continue; # ditto
4305 if ($then > $eightminutesago) continue;
4306
4307 $vippstatus = $o->get_meta('_vipps_status');
4308 $currentstatus = $this->gateway()->interpret_vipps_order_status($vippstatus);
4309 if ($currentstatus != 'initiated') {
4310 $this->log(sprintf(__("Order %2\$d is 'pending' but its %1\$s order status is '%3\$s' - this means that the order has been erroneously set to 'pending' after completion or cancellation. Will not process further, please check status of order at %1\$s and set to correct status in WooCommerce", 'woo-vipps'), $this->get_payment_method_name(), $o->get_id(), $currentstatus), 'debug');
4311 return;
4312 }
4313 $this->check_status_of_pending_order($o, false);
4314 }
4315 }
4316
4317 // Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session -
4318 // e.g. wp-cron. IOK 2021-06-21
4319 // Stop restoring session in wp-cron too. IOK 2021-08-23
4320 // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4321 public function check_status_of_pending_order($order, $allow_retry=true) {
4322 $gw = $this->gateway();
4323
4324 $order_status = null;
4325 try {
4326 $order->add_order_note(sprintf(__("Callback from %1\$s delayed or never happened; order status checked by periodic job", 'woo-vipps'), $this->get_payment_method_name()));
4327
4328 // Poll status and correct woo status. LP 2026-05-19
4329 $order_data = $gw->get_payment_details($order);
4330
4331 // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4332 if ('CANCEL' === ($order_data['state'] ?? "")) {
4333 /* translators: company name */
4334 $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4335 return;
4336 }
4337
4338 $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4339 $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4340 $order_status = $order->get_status();
4341
4342 $this->log(sprintf(__("For order %2\$d order status at %1\$s is %3\$s", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id(), $order_status), 'debug');
4343 } catch (Exception $e) {
4344 $this->log(sprintf(__("Error getting order status at %1\$s for order %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id()), 'error');
4345 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
4346 }
4347 return $order_status;
4348 }
4349
4350 // This will probably be run in activate, but if the plugin is updated in other ways, will also be run on after_setup_theme. IOK 2020-04-01
4351 public static function maybe_add_cron_event() {
4352 if (!wp_next_scheduled('vipps_cron_cleanup_hook')) {
4353 wp_schedule_event(time(), 'hourly', 'vipps_cron_cleanup_hook');
4354 }
4355 if (!wp_next_scheduled('vipps_cron_missing_callback_hook')) {
4356 wp_schedule_event(time(), '5min', 'vipps_cron_missing_callback_hook');
4357 }
4358 }
4359
4360 public function activate () {
4361 static::maybe_add_cron_event();
4362 $gw = $this->gateway();
4363
4364 // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4365 if ($gw->get_option('orderprefix') == 'Woo') {
4366 $gw->update_option('orderprefix', $this->generate_order_prefix());
4367 }
4368 // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4369 $gw->initialize_webhooks();
4370 $this->payment_method_name = $gw->get_option('payment_method_name');
4371
4372
4373 // Check if the special page is noted and actually does exist
4374 $special = static::get_special_page_id();
4375 if ($special) {
4376 $special_page = get_post($special);
4377 if ($special_page && 'trash' !== $special_page->post_status) {
4378 // Ensure this page has the necessary shortcode. LP 2026-09-01
4379 if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4380 $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4381 wp_update_post([
4382 'ID' => $special,
4383 'post_content' => $new_content,
4384 ]);
4385 }
4386 } else {
4387 delete_option('woocommerce_vipps_special_page_page_id');
4388 }
4389 }
4390
4391 }
4392
4393 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4394 public static function deactivate() {
4395 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4396 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
4397 $timestamp = wp_next_scheduled('vipps_cron_missing_callback_hook');
4398 wp_unschedule_event($timestamp, 'vipps_cron_missing_callback_hook');
4399 // IOK 2023-12-20 Delete all webhooks for this instance
4400 $gw = WC_Gateway_Vipps::instance();
4401 $gw->delete_all_webhooks();
4402
4403 // Delete all settings if checked in settings menu. LP 2025-10-06
4404 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4405 if ($should_delete) {
4406 // Delete options.
4407 $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', 'vipps_button_options2', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4408 foreach($options as $option) {
4409 delete_option($option);
4410 }
4411 }
4412
4413 // Run deactivation logic for recurring
4414 if (class_exists('WC_Vipps_Recurring')) {
4415 WC_Vipps_Recurring::get_instance()->deactivate();
4416 }
4417 delete_option('woo_vipps_recurring_payments_activation');
4418
4419 }
4420
4421 /** Try manually setting locale to locale recieved in AcceptLanguage header.
4422 *
4423 * This should fix incorrect language recieved from ajax when using translate plugins like polylang, wpml.
4424 * E.g. for checkout widgets and product names: We send the correct locale to the frontend when first setting up Checkout,
4425 * then we send the locale back in the Accept-Language header to ajax endpoints. LP 2025-12-11
4426 */
4427 public static function set_locale_if_in_header() {
4428 $locales = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
4429
4430 // get first in list, but strip away semicolon and everything after. LP 2025-12-16
4431 $newlocale = trim(preg_replace("!;.*!", "", explode(",", $locales)[0]));
4432 if (empty($newlocale))
4433 return false;
4434 return switch_to_locale($newlocale); // note: this may fail and return a false. LP 2025-12-11
4435 }
4436
4437
4438 public function footer() {
4439 // Nothing yet
4440 }
4441
4442
4443 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4444 private function maybe_set_vipps_as_default() {
4445 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4446 $gw = $this->gateway();
4447 // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4448 if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4449 WC()->session->set('chosen_payment_method', $gw->id);
4450 }
4451 }
4452
4453 // Check order status in the database, and if it is pending for a long time, directly at Vipps
4454 // IOK 2018-05-04
4455 public function check_order_status($order) {
4456 if (!$order) return null;
4457 clean_post_cache($order->get_id()); // Get a fresh copy
4458 $order = wc_get_order($order->get_id());
4459 $order_status = $order->get_status();
4460
4461 if ($order_status != 'pending') return $order_status;
4462
4463 $gw = $this->gateway();
4464 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4465 $newstatus = $gw->poll_and_check_order_status($order);
4466 }
4467
4468 // In some situations we have to empty the cart when the user goes to Vipps, so
4469 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
4470 // Try to avoid this now 2018-12-10 - only do it for single-product checkouts. IOK 2018-10-12
4471 // Changed to use a serialized cart, which should be more compatible with subclassed carts and cart metadata.
4472 // Serialization errors are not yet handled - they can't be fixed but they could be signalled. IOK 2020-04-07
4473 public function save_cart($order,$cart_to_save) {
4474 $carts = WC()->session->get('_vipps_carts');
4475 if (!$carts) $carts = array();
4476 $serialized = base64_encode(@serialize($cart_to_save->get_cart_contents()));
4477 $carts[$order->get_id()] = $serialized;
4478 WC()->session->set('_vipps_carts',$carts);
4479 do_action('woo_vipps_cart_saved');
4480 }
4481 public function restore_cart($order) {
4482 global $woocommerce;
4483 $carts = $woocommerce->session->get('_vipps_carts');
4484 if (empty($carts)) return;
4485 $cart = null;
4486 $cartdata = @$carts[$order->get_id()];
4487 if ($cartdata) {
4488 $cart = @unserialize(@base64_decode($cartdata));
4489 }
4490 do_action('woo_vipps_restoring_cart',$order,$cart);
4491 unset($carts[$order->get_id()]);
4492 $woocommerce->session->set('_vipps_carts',$carts);
4493 // It will absolutely not work to just use set_cart_contents, because this will not
4494 // correctly initialize this 'new' cart. So we *have* to use add_to_cart at least once. IOK 2020-04-07
4495 if (!empty($cart)) {
4496 foreach ($cart as $cart_item_key => $values) {
4497 $id =$values['product_id'];
4498 $quant=$values['quantity'];
4499 $varid = @$values['variation_id'];
4500 $variation = @$values['variation'];
4501 // .. and there may be any number of other attributes, which we need to pass on.
4502 $cart_item_data = array();
4503 foreach($values as $key=>$value) {
4504 if (in_array($key,array('product_id','quantity','variation_id','variation'))) continue;
4505 $cart_item_data[$key] = $value;
4506 }
4507 $woocommerce->cart->add_to_cart($id,$quant,$varid,$variation,$cart_item_data);
4508 }
4509 }
4510 do_action('woo_vipps_cart_restored');
4511 }
4512
4513 // Should only be run when this is an order in our own session,
4514 // used in the ajax_check_order_status and vipps_payment methods, where we are
4515 // expecting a customer return that may be from Express Checkout. If it is, we may
4516 // have no customer email in the current session, which in 7.8.2 will stop the user from
4517 // viewing his or her orders. IOK 2023-07-17
4518 function maybe_set_session_customer_email($order) {
4519 if ($order->get_meta('_vipps_express_checkout')) {
4520 $email = $order->get_billing_email();
4521 if ($email && WC()->customer) {
4522 WC()->customer->set_email($email);
4523 WC()->customer->set_billing_email($email);
4524 WC()->customer->save();
4525 WC()->session->set('tstamp', time()); // Just to ensure it is 'dirty'
4526 } else {
4527 $this->log(__("Could not get user email from order before thankyou-page", 'woo-vipps'));
4528 }
4529 }
4530 }
4531
4532 // Maybe log in user
4533 // It is done on the thank-you page of the order, and only for express checkout.
4534 function maybe_log_in_user ($order) {
4535
4536 if (is_user_logged_in()) return;
4537 if (!$order || ! self::is_vipps_order($order)) return;
4538
4539 // We *do* want to log in express checkout customers, but not those that
4540 // use the Checkout solution - those can change their emails in the
4541 // checkout screen. IOK 2021-09-03
4542 $do_login = $order->get_meta('_vipps_express_checkout');
4543
4544 // We will not log in Checkout users unless the option for that is true
4545 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4546 $do_login = false;
4547 }
4548
4549
4550 // Make this filterable because you may want to only log on some users
4551 $do_login = apply_filters('woo_vipps_login_user_on_express_checkout', $do_login, $order);
4552 if (!$do_login) return;
4553
4554 $customer = $this->express_checkout_get_vipps_customer ($order);
4555 if( $customer) {
4556 $usermeta=get_userdata($customer->get_id());
4557 $iscustomer = (in_array('customer', $usermeta->roles) || in_array('subscriber', $usermeta->roles));
4558 // Ensure we don't have any admins with an additonal customer role logged in like this
4559 if($iscustomer && !user_can($customer->get_id(), 'manage_woocommerce') && !user_can($customer->get_id(),'manage_options')) {
4560 do_action('express_checkout_before_customer_login', $customer, $order);
4561
4562 $user = new WP_User( $customer->get_id());
4563 wp_set_current_user($customer->get_id(), $user->user_login);
4564
4565 wp_set_auth_cookie($customer->get_id());
4566 do_action('wp_login', $user->user_login, $user);
4567 }
4568 }
4569 }
4570
4571 // Get the customer that corresponds to the current order, maybe creating the customer if it does not exist yet and
4572 // the settings allow it.
4573 function express_checkout_get_vipps_customer($order) {
4574 if (!$order || ! self::is_vipps_order($order)) return null;
4575 // specific code for this by netthandelsgruppen if the below function exists
4576 if (function_exists('create_assign_user_on_vipps_callback')) return null;
4577
4578 // Both Checkout and Express Checkout have the below value set to true
4579 if (!$order->get_meta('_vipps_express_checkout')) return;
4580
4581 // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4582 // IOK 2023-07-27
4583 $ischeckout = $order->get_meta('_vipps_checkout');
4584 if ($ischeckout) {
4585 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
4586 } else {
4587 if ($this->gateway()->get_option('expresscreateuser') != 'yes') return null;
4588 }
4589
4590 if (is_user_logged_in()) return new WC_Customer(get_current_user_id());
4591 if ($order->get_user_id()) return new WC_Customer($order->get_user_id());
4592
4593 $email = $order->get_billing_email();
4594
4595 // Existing customer, so update the order (and possibly the site if multisite) and return the customer. IOK 2020-10-09
4596 if (email_exists($email)) {
4597 $user = get_user_by( 'email', $email);
4598 if (!$user) return null;
4599 $customerid = $user->ID;
4600 $order->set_customer_id( $user->ID );
4601 $order->save();
4602
4603 if (is_multisite() && ! is_user_member_of_blog($customerid, get_current_blog_id())) {
4604 add_user_to_blog( get_current_blog_id(), $customerid, 'customer' );
4605 }
4606 $customer = new WC_Customer($customerid);
4607 return $customer;
4608 }
4609
4610 // Previously this got the user data from Vipps here as a third argument; this is no longer available after refactoring.
4611 $user = [];
4612 $maybecreateuser = apply_filters('woo_vipps_create_user_on_express_checkout', true, $order, $user);
4613 if (! $maybecreateuser) return;
4614
4615 // No customer yet. As we want to create users like this (set in the settings) let's do so.
4616 // Username will be created from email, but the settings may stop generating passwords, so we force that to be generated. IOK 2020-10-09
4617 $firstname = $order->get_billing_first_name();
4618 $lastname = $order->get_billing_last_name();
4619 $name = $firstname;
4620 $userdata = array('user_nicename'=>$name, 'display_name'=>"$firstname $lastname", 'nickname'=>$firstname, 'first_name'=>$firstname, 'last_name'=>$lastname);
4621
4622 // Add filter to allow other ways of creating usernames.
4623 $newusername = apply_filters('woo_vipps_express_checkout_new_username', '', $email, $userdata, $order);
4624
4625 $customerid = wc_create_new_customer($email, $newusername, wp_generate_password(), $userdata);
4626 if ($customerid && !is_wp_error($customerid)) {
4627 $order->set_customer_id( $customerid );
4628 $order->save();
4629
4630 // Ensure the standard WP user fields are set too IOK 2020-11-03
4631 wp_update_user(array('ID' => $customerid, 'first_name' => $firstname, 'last_name' => $lastname, 'display_name' => "$firstname $lastname", 'nickname' => $firstname));
4632
4633 update_user_meta( $customerid, 'billing_address_1', $order->get_billing_address_1() );
4634 update_user_meta( $customerid, 'billing_address_2', $order->get_billing_address_2() );
4635 update_user_meta( $customerid, 'billing_city', $order->get_billing_city() );
4636 update_user_meta( $customerid, 'billing_company', $order->get_billing_company() );
4637 update_user_meta( $customerid, 'billing_country', $order->get_billing_country() );
4638 update_user_meta( $customerid, 'billing_email', $order->get_billing_email() );
4639 update_user_meta( $customerid, 'billing_first_name', $order->get_billing_first_name() );
4640 update_user_meta( $customerid, 'billing_last_name', $order->get_billing_last_name() );
4641 update_user_meta( $customerid, 'billing_phone', $order->get_billing_phone() );
4642 update_user_meta( $customerid, 'billing_postcode', $order->get_billing_postcode() );
4643 update_user_meta( $customerid, 'billing_state', $order->get_billing_state() );
4644 update_user_meta( $customerid, 'shipping_address_1', $order->get_shipping_address_1() );
4645 update_user_meta( $customerid, 'shipping_address_2', $order->get_shipping_address_2() );
4646 update_user_meta( $customerid, 'shipping_city', $order->get_shipping_city() );
4647 update_user_meta( $customerid, 'shipping_company', $order->get_shipping_company() );
4648 update_user_meta( $customerid, 'shipping_country', $order->get_shipping_country() );
4649 update_user_meta( $customerid, 'shipping_first_name', $order->get_shipping_first_name() );
4650 update_user_meta( $customerid, 'shipping_last_name', $order->get_shipping_last_name() );
4651 update_user_meta( $customerid, 'shipping_method', $order->get_shipping_method() );
4652 update_user_meta( $customerid, 'shipping_postcode', $order->get_shipping_postcode() );
4653 update_user_meta( $customerid, 'shipping_state', $order->get_shipping_state() );
4654
4655 // Integration with All-in-one WP security - these accounts are created by validated accounts in the app.
4656 update_user_meta( $customerid,'aiowps_account_status', 'approved');
4657
4658 $customer = new WC_Customer($customerid);
4659 do_action('woo_vipps_express_checkout_new_customer', $customer, $order->get_id());
4660
4661 return $customer;
4662 }
4663 if (is_wp_error($customerid)) {
4664 $this->log(__("Error creating customer in express checkout: ", 'woo-vipps') . $customerid->get_error_message());
4665 } else {
4666 $this->log(__("Unknown error customer in express checkout.", 'woo-vipps'));
4667 }
4668 return null;
4669 }
4670
4671 // This restores the cart on order complete, but only if the current order was a single product buy with an active cart.
4672 public function maybe_restore_cart($orderid,$failed=false) {
4673 if (!$orderid) return;
4674 $o = null;
4675 try {
4676 $o = wc_get_order($orderid);
4677 } catch (Exception $e) {
4678 // Well, we tried.
4679 }
4680 if (!$o) return;
4681 if (!$o->get_meta('_vipps_single_product_express')) return;
4682 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4683 // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4684 WC()->cart->empty_cart();
4685 $this->restore_cart($o);
4686 }
4687
4688
4689 // Actually create a express checkout order object, with no shipping or personal information, returning information about
4690 // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4691 // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4692 private function create_and_process_express_order() {
4693 $result = null;
4694 $gw = $this->gateway();
4695 try {
4696 $orderid = $gw->create_partial_order();
4697 do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4698 } catch (Exception $e) {
4699 $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4700 return $result;
4701 }
4702 if (!$orderid) {
4703 $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4704 return $result;
4705 }
4706
4707 try {
4708 $this->maybe_add_static_shipping($gw,$orderid);
4709 } catch (Exception $e) {
4710 $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4711 $this->log($e->getMessage(),'error');
4712 $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4713 return $result;
4714 }
4715
4716 // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4717 $ok = $gw->process_payment($orderid);
4718 if ($ok && $ok['result'] == 'success') {
4719 $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4720 return $result;
4721 }
4722 $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4723 return $result;
4724 }
4725
4726 // This creates a simple hash for the 'current order' which we will store in the session if we proceed to checkout. We use this to
4727 // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4728 public function create_order_hash($args=null) {
4729 // If we have no arguments, we'll hash the cart.
4730 if (empty($args)) {
4731 $cartitems = WC()->cart->get_cart();
4732 $orderspec = array();
4733 foreach($cartitems as $item => $values) {
4734 $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4735 }
4736 $args = $orderspec;
4737 }
4738 return md5(serialize($args));
4739 }
4740
4741
4742 // This method may provide HTML form elements to ask a user questions after starting
4743 // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4744 // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4745 public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4746 $elements = [];
4747 $html = "";
4748
4749 // First, let's check if we need to confirm the purchase.
4750 $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4751 if ($last_express_purchase_hash) {
4752 list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4753 $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4754 if ($hash == $current_hash && (time() <= $cutoff )) {
4755 $order = wc_get_order($orderid);
4756 $status = $order ? $order->get_status() : false;
4757 // IOK TODO/FIXME actually, if the order is pending/failed/cancelled and *identical* to our current productinfo, we could plausibly do a restart here. Would probably require careful checking though, and
4758 // a different flow. IOK 2026-09-17
4759 if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4760 $header = __("Are you sure?",'woo-vipps');
4761 $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
4762 $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4763 $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
4764 }
4765 }
4766 }
4767
4768 $gw = $this->gateway();
4769 $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4770 $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4771 $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4772
4773 if ($askForTerms) {
4774 $termsHTML = '';
4775 // Include shop terms
4776 ob_start();
4777 wc_get_template('checkout/terms.php');
4778 $termsHTML = ob_get_clean();
4779 $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4780 $elements['terms'] = $termsHTML;
4781 }
4782
4783 // Custom fields
4784 ob_start();
4785 do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4786 $extra_fields = ob_get_clean();
4787 if (!empty($extra_fields)) {
4788 $elements['extra'] = $extra_fields;
4789 }
4790
4791 if (!empty($elements)) {
4792 $html = join("\n", array_values($elements));
4793 $msg = join(",", array_keys($elements));
4794 return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4795 }
4796
4797 return false;
4798
4799 }
4800
4801 public function rest_do_express_checkout ($request) {
4802 Vipps::nocache();
4803 check_ajax_referer('express', 'sec');
4804 static::set_locale_if_in_header();
4805 $args = $request->get_json_params();
4806 if (!$args) {
4807 return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4808 }
4809
4810 // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4811 if ( is_null( WC()->cart ) ) {
4812 WC()->frontend_includes();
4813 if ( ! WC()->session instanceof WC_Session ) {
4814 WC()->session = new WC_Session_Handler();
4815 WC()->session->init();
4816 }
4817 if (is_null( WC()->customer)) {
4818 WC()->customer = new WC_Customer( get_current_user_id(), true );
4819 }
4820 WC()->cart = new WC_Cart();
4821 WC()->cart->get_cart_from_session();
4822 }
4823
4824
4825 $gw = $this->gateway();
4826 if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4827 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4828 return $result;
4829 }
4830 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4831 $toolate = false;
4832 $msg = "";
4833 $valid = WC()->cart->check_cart_item_validity();
4834 if ( is_wp_error( $valid) ) {
4835 $toolate = true;
4836 $msg = "<br>" . $valid->get_error_message();
4837 }
4838 $stock = WC()->cart->check_cart_item_stock();
4839 if ( is_wp_error( $stock) ) {
4840 $toolate = true;
4841 $msg = "<br>" . $stock->get_error_message();
4842 }
4843
4844 if ($toolate) {
4845 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4846 return $result;
4847 }
4848
4849 // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4850 // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4851 // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4852 $cookies = $args['cookies'] ?? [];
4853 foreach($cookies as $key => $value) {
4854 if (!isset($_COOKIE[$key])) {
4855 $_COOKIE[$key] = $value;
4856 }
4857 }
4858 // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4859 // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4860 $others =$args['post'] ?? [];
4861 foreach($args['post'] as $key=>$value) {
4862 $_POST[$key] = $value;
4863 }
4864
4865 // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4866 $current_hash = $this->create_order_hash();
4867 $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4868 if (!$confirmation) {
4869 $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4870 if (!empty($result)) {
4871 return $result;
4872 }
4873 }
4874
4875 $result = $this->create_and_process_express_order();
4876 if ($result['ok'] == 1) {
4877 $orderid = $result['orderid'];
4878 WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4879 WC()->session->save_data();
4880 }
4881 return $result;
4882
4883 }
4884
4885
4886 // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4887 public function rest_do_single_product_express_checkout ($request) {
4888 Vipps::nocache();
4889 static::set_locale_if_in_header();
4890 $args = $request->get_json_params();
4891 if (!$args) {
4892 return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4893 }
4894 $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4895
4896 // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4897 $varid = intval($args['variation_id'] ?? 0);
4898 $prodid = intval($args['product_id'] ?? 0);
4899 $sku = sanitize_text_field($args['sku'] ?? "");
4900 $quantity = max(1, intval($args['quantity'] ?? 0));
4901
4902
4903 // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4904 // We just need to sanitize them.
4905 $variations = [];
4906 $invars = $args['post'] ?? [];
4907 foreach ($invars as $key => $value) {
4908 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4909 continue;
4910 }
4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4912 }
4913
4914 // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4915 // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4916 // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4917 $cookies = $args['cookies'] ?? [];
4918 foreach($cookies as $key => $value) {
4919 if (!isset($_COOKIE[$key])) {
4920 $_COOKIE[$key] = $value;
4921 }
4922 }
4923
4924 // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4925 // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4926 $others =$args['post'] ?? [];
4927 foreach($args['post'] as $key=>$value) {
4928 $_POST[$key] = $value;
4929 }
4930
4931 // Since this is the REST api, we need to load the cart manually here. *Not* loading the cart could be an option but unpredictable. IOK 2026-08-27
4932 if ( is_null( WC()->cart ) ) {
4933 WC()->frontend_includes();
4934 if ( ! WC()->session instanceof WC_Session ) {
4935 WC()->session = new WC_Session_Handler();
4936 WC()->session->init();
4937
4938 // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
4939 if (! WC()->session->get_session_cookie()) {
4940 $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
4941 add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
4942 try {
4943 WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
4944 } finally {
4945 remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
4946 }
4947 }
4948 }
4949 if (is_null( WC()->customer)) {
4950 WC()->customer = new WC_Customer( get_current_user_id(), true );
4951 }
4952 WC()->cart = new WC_Cart();
4953 WC()->cart->get_cart_from_session();
4954 }
4955
4956 // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4957 // We calculate this here so we can add it to the session later. IOK 2026-09-09
4958 $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
4959 $current_hash = $this->create_order_hash($orderspec);
4960
4961 // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
4962 $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4963 if (!$confirmation) {
4964 $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
4965 if (!empty($result)) {
4966 $response = new WP_REST_Response($result);
4967 $response->set_status(200);
4968 return $response;
4969 }
4970 }
4971
4972 // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
4973 $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
4974 // And if we're going to express now so let's note the order. IOK 2026-08-27. Now this assumes success, but *basically* I think this is ok.
4975 // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
4976 if ($result['ok'] == 1) {
4977 $orderid = $result['orderid'];
4978 WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4979 WC()->session->save_data();
4980 }
4981
4982 $response = new WP_REST_Response($result);
4983 $response->set_status(200);
4984
4985 return $response;
4986 }
4987
4988 // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
4989 private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
4990 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4991 $gw = $this->gateway();
4992
4993 if (!$gw->express_checkout_available()) {
4994 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4995 return $result;
4996 }
4997 // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4998 // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4999
5000 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
5001 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
5002 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
5003 try {
5004 if ($varid) {
5005 $product = wc_get_product($varid);
5006 } elseif ($prodid) {
5007 $product = wc_get_product($prodid);
5008 } elseif ($sku) {
5009 $skuid = wc_get_product_id_by_sku($sku);
5010 $product = wc_get_product($skuid);
5011 }
5012 } catch (Exception $e) {
5013 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
5014 return $result;
5015 }
5016
5017 if (!$product) {
5018 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
5019 return $result;
5020 }
5021
5022 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
5023 $parent = $parentid ? wc_get_product($parentid) : null;
5024
5025 // This can't really happen, but if it did..
5026 if ($prodid && $parentid && ($prodid != $parentid)) {
5027 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
5028 return $result;
5029 }
5030 if (!$gw->product_supports_express_checkout($product)) {
5031 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
5032 return $result;
5033 }
5034
5035 // Somebody addded the wrong SKU
5036 if ($product->get_type() == 'variable'){
5037 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
5038 return $result;
5039 }
5040 // Final check of availability
5041 if (!$product->is_purchasable() || !$product->is_in_stock()) {
5042 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
5043 return $result;
5044 }
5045
5046 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
5047 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
5048 // because some plugins actually override this.
5049 // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
5050 $current_cart = clone WC()->cart;
5051 WC()->cart->empty_cart();
5052
5053 if ($parent && $parent->get_type() == 'variable') {
5054 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
5055 } else {
5056 WC()->cart->add_to_cart($product->get_id(),$quantity);
5057 }
5058 WC()->session->save_data();
5059
5060 $result = $this->create_and_process_express_order();
5061
5062 if ($result['ok'] ?? false) {
5063 // Single product purchase, so save any contents of the real cart
5064 $orderid = $result['orderid'];
5065 $order = wc_get_order($orderid);
5066 $order->update_meta_data('_vipps_single_product_express',true);
5067 $order->save();
5068 $this->save_cart($order,$current_cart);
5069 }
5070
5071 return $result;
5072 }
5073
5074 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
5075 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
5076 public function maybe_add_static_shipping($gw, $orderid, $ischeckout=false) {
5077 $key = $ischeckout ? 'enablestaticshipping_checkout' : 'enablestaticshipping';
5078 $ok = $gw->get_option($key) == 'yes';
5079 $ok = apply_filters('woo_vipps_enable_static_shipping', $ok, $orderid);
5080 if ($ok) {
5081 return $this->add_static_shipping($gw, $orderid, $ischeckout);
5082 }
5083 }
5084
5085 // And this function adds static shipping no matter what. It may need to be used in plugins, hence visible. IOK 2021-10-22
5086 public function add_static_shipping ($gw, $orderid, $ischeckout=false) {
5087 $order = wc_get_order($orderid);
5088 $prefix = $gw->get_orderprefix();
5089 $vippsorderid = apply_filters('woo_vipps_orderid', $prefix.$orderid, $prefix, $order);
5090 $addressinfo = $this->get_static_shipping_address_data();
5091
5092 // Both Checkout and new Express Checkout supports LocalPickup, so add it (it is normally only present for Gutenberg checkout)
5093 // Add special shipping methods (LocalPickup etc);
5094 $this->load_extra_shipping_methods($order, $addressinfo, $ischeckout);
5095
5096 $options = $this->vipps_shipping_details_callback_handler($order, $addressinfo,$vippsorderid, $ischeckout);
5097
5098 if ($options) {
5099 $order->update_meta_data('_vipps_static_shipping', $options);
5100 $order->save();
5101 }
5102 }
5103
5104 // Support local pickup. This is normally only registered when the Gutenberg Checkout block is either on the
5105 // 'checkout-page' or in some template; but that's not nececssarily the case if Vipps MobilePay checkout is active.
5106 // Supported also in express checkout. 2026-02-25
5107 // We'll add this if admin has stored *any* pickup locations at any point. IOK 2026-02-25
5108 // Afterwards, we need to post-process this, because *each* location gets a different rate. See the VippsCheckout class.
5109 function maybe_load_pickup_locations () {
5110 $locations = get_option('pickup_location_pickup_locations', array());
5111 if (!empty($locations) && class_exists('Automattic\WooCommerce\Blocks\Shipping\PickupLocation')) {
5112 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
5113 }
5114 }
5115
5116 // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5117 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
5118 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
5119 // If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06
5120 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
5121 // Previously we loaded PickupLocations here; we now do that if any are defined at all. The old custom filter still runs though,
5122 // and last. IOK 2026-02-25
5123 do_action('woo_vipps_express_load_shipping_methods', $order, $addressdata);
5124 }, 99);
5125 }
5126
5127
5128 // Check the status of the order if it is a part of our session, and return a result to the handler function IOK 2018-05-04
5129 public function ajax_check_order_status () {
5130 check_ajax_referer('vippsstatus','sec');
5131 static::set_locale_if_in_header();
5132 Vipps::nocache();
5133
5134 $orderid= wc_get_order_id_by_order_key(sanitize_text_field(@$_POST['key']));
5135 $transaction = sanitize_text_field(@$_POST['transaction']);
5136
5137 $sessionorders= WC()->session->get('_vipps_session_orders');
5138 if (!isset($sessionorders[$orderid])) {
5139 wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
5140 }
5141
5142 $order = wc_get_order($orderid);
5143 if (!$order) {
5144 wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5145 }
5146 $order_status = $this->check_order_status($order);
5147 // No callback has occured yet. If this has been going on for a while, check directly with Vipps
5148 if ($order_status == 'pending') {
5149 wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps')));
5150 return false;
5151 }
5152 if ($order_status == 'cancelled' || $order_status == 'failed') {
5153 $this->maybe_restore_cart($orderid,'failed');
5154 wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps'), 'order_status' => $order_status));
5155 return false;
5156 }
5157
5158 // Order status isn't pending anymore, but there can be custom statuses, so check the payment status instead.
5159 $order = wc_get_order($orderid); // Reload
5160 $gw = $this->gateway();
5161 $payment = $gw->check_payment_status($order);
5162 if ($payment == 'initiated') {
5163 wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps')));
5164 return false;
5165 }
5166
5167
5168 if ($payment == 'authorized') {
5169 // IOK Previously handled in the thankyou hook 2023-07-17
5170 $this->woocommerce_before_thankyou($order->get_id());
5171 wp_send_json(array('status'=>'ok', 'msg'=>__('Payment authorized', 'woo-vipps')));
5172 return false;
5173 }
5174 if ($payment == 'complete') {
5175 // IOK Previously handled in the thankyou hook 2023-07-17
5176 $this->woocommerce_before_thankyou($order->get_id());
5177 wp_send_json(array('status'=>'ok', 'msg'=>__('Payment captured', 'woo-vipps')));
5178 return false;
5179 }
5180 if ($payment == 'cancelled') {
5181 $this->maybe_restore_cart($orderid,'failed');
5182 wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps')));
5183 return false;
5184 }
5185 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
5186 return false;
5187 }
5188
5189 // The various return URLs for special pages of the Vipps stuff. Previously used a fake page and had to check permalink_structure. LP 2026-08-26
5190 private function make_special_page_url($action) {
5191 return add_query_arg('action', $action, $this->get_special_page_url());
5192 }
5193
5194 public function payment_return_url() {
5195 return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
5196 }
5197 public function express_checkout_url() {
5198 return $this->make_special_page_url('do_express_checkout');
5199 }
5200 public function buy_product_url() {
5201 return $this->make_special_page_url('buy_product');
5202 }
5203
5204 public static function is_special_page() {
5205 $id = static::get_special_page_id();
5206 return $id && is_page($id);
5207 }
5208
5209 public static function get_special_page_id() {
5210 $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5211 return $id > 0 ? $id : null;
5212 }
5213
5214 public static function get_special_page_url() {
5215 return get_permalink(static::get_special_page_id());
5216 }
5217
5218 // Just create a spinner and a overlay.
5219 public function spinner () {
5220 $flavour = sanitize_title($this->get_payment_method_name());
5221 ob_start();
5222 ?>
5223 <div class="vippsoverlay">
5224 <div id="floatingCirclesG" class="vippsspinner <?php echo esc_attr($flavour); ?>">
5225 <div class="f_circleG" id="frotateG_01"></div>
5226 <div class="f_circleG" id="frotateG_02"></div>
5227 <div class="f_circleG" id="frotateG_03"></div>
5228 <div class="f_circleG" id="frotateG_04"></div>
5229 <div class="f_circleG" id="frotateG_05"></div>
5230 <div class="f_circleG" id="frotateG_06"></div>
5231 <div class="f_circleG" id="frotateG_07"></div>
5232 <div class="f_circleG" id="frotateG_08"></div>
5233 </div>
5234 </div>
5235 <?php
5236 return apply_filters('woo_vipps_spinner', ob_get_clean());
5237 }
5238
5239
5240 // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5241 // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5242 public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5243 return $this->get_html_button_for_context($context);
5244 }
5245
5246 // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5247 // Get payment logo based on payment method, then language NT 2023-11-30
5248 // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5249 // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5250 public function get_payment_logo($context = 'global') {
5251 return $this->get_express_logo(null, null, null, $context);
5252 }
5253
5254 // Get express banner logo based on payment method. LP 2025-09-03
5255 private function get_express_banner_logo() {
5256 $payment_method = $this->get_payment_method_name();
5257
5258 if($payment_method === "Vipps"){
5259 return plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
5260 } else if($payment_method === "MobilePay"){
5261 return plugins_url('img/mobilepay-white.svg',__FILE__);
5262 }
5263 return null;
5264 }
5265
5266 // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5267 // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5268 // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5269 public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
5270 $disabled = $disabled ? 'disabled' : '';
5271 $data = array();
5272
5273 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
5274 if ($product_id && !$variation_id) {
5275 $product = wc_get_product($product_id);
5276 if ($product && is_a($product, 'WC_Product_Variation')) {
5277 $variation_id = $product_id;
5278 $product_id = $product->get_parent_id();
5279 }
5280 }
5281
5282 if ($sku) $data['product_sku'] = $sku;
5283 if ($product_id) $data['product_id'] = $product_id;
5284 if ($variation_id) $data['variation_id'] = $variation_id;
5285
5286 $buttoncode = "<a href='javascript:void(0)' $disabled ";
5287 foreach($data as $key=>$value) {
5288 $value = esc_attr($value);
5289 $buttoncode .= " data-$key='$value' ";
5290 }
5291
5292 $payment_method = $this->get_payment_method_name();
5293 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
5294
5295 if (is_array($button_args_override) && $button_args_override) {
5296 $button_args = $button_args_override;
5297 } else {
5298 $button_args = $this->get_html_button_attrs_for_context($context);
5299 }
5300 $short = ($button_args['compact'] ?? 'false') === 'true';
5301 $button = $this->get_html_button($button_args);
5302
5303 # Extra classes, if passed IOK 2019-02-26
5304 if (is_array($classes)) {
5305 $classes = join(" ", $classes);
5306 }
5307 if ($classes) $classes = " $classes";
5308 if ($short) $classes = "short $classes";
5309
5310 $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5311
5312
5313
5314 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
5315 }
5316
5317 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
5318 public function single_product_buy_now_button () {
5319 $gw = $this->gateway();
5320 $how = $gw->get_option('singleproductexpress');
5321 if ($how == 'none') return;
5322 if (!$gw->express_checkout_available()) return;
5323
5324 global $product;
5325 $prodid = $product->get_id();
5326 if (!$gw->product_supports_express_checkout($product)) return;
5327
5328 // Vipps does not support 0,- products, so we need to check.
5329 // get_price() should normally return the lowest price for variable products, but that can fail,
5330 // so we dispatch on the type and use the *minimum* price instead, requiring that to be nonzero. IOK 2022-06-08
5331 $showit = true;
5332 if (is_a($product, 'WC_Product_Variable')) {
5333 $minprice = $product->get_variation_price('min', 0);
5334 if ($minprice > 0) $showit = true;
5335 } else {
5336 if ($product->get_price() <= 0) $showit = false;
5337 }
5338
5339 if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false;
5340 $showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product);
5341 if (!$showit) return;
5342
5343 $classes = array();
5344 $disabled="";
5345 if ($product->is_type('variable')) {
5346 $disabled="disabled";
5347 $classes[] = 'variable-product';
5348 }
5349
5350 # If true, add a class that signals that the button should be added in 'compat mode', which is compatible with
5351 # more plugins because it does not handle tha product add itself. IOK 2019-02-26
5352 $compat = ($gw->get_option('singleproductbuynowcompatmode') == 'yes');
5353 $compat = apply_filters('woo_vipps_single_product_compat_mode', $compat, $product);
5354
5355 if ($compat) $classes[] ='compat-mode';
5356 $classes = apply_filters('woo_vipps_single_product_buy_now_classes', $classes, $product);
5357
5358 $button = $this->get_buy_now_button(false,false,false, ($product->is_type('variable') ? 'disabled' : false), $classes, 'product');
5359 $code = "<div class='vipps_buy_now_wrapper noloop'>$button</div>";
5360 echo $code;
5361 }
5362
5363
5364 // True for products that are purchasable using Vipps Express Checkout
5365 public function loop_single_product_is_express_checkout_purchasable($product) {
5366 if (!$product) return false;
5367 if (!$product->is_purchasable() || !$product->is_in_stock() || !$product->supports( 'ajax_add_to_cart' )) return false;
5368 $gw = $this->gateway();
5369
5370 if (!$gw->express_checkout_available()) return false;
5371 if (!$gw->product_supports_express_checkout($product)) return false;
5372 if ($gw->get_option('singleproductexpressarchives') != 'yes') return false;
5373
5374 $how = $gw->get_option('singleproductexpress');
5375 if ($how == 'none') return false;
5376 $prodid = $product->get_id();
5377
5378 $showit = true;
5379 if ($product->get_price() <= 0) $showit = false;
5380 if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false;
5381 $showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product);
5382 $showit = apply_filters('woo_vipps_show_single_product_buy_now_in_loop', $showit, $product);
5383 return $showit;
5384 }
5385
5386 // Print a "buy now with vipps" for products in the loop, like on a category page
5387 public function loop_single_product_buy_now_button() {
5388 global $product;
5389
5390 if (!$this->loop_single_product_is_express_checkout_purchasable($product)) return;
5391
5392 $sku = $product->get_sku();
5393 $button = $this->get_buy_now_button($product->get_id(),false,$sku, false, '', 'catalog');
5394 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5395 }
5396
5397
5398 // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5399 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5400 // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5401 public function woocommerce_create_pages ($data) {
5402 // Vipps Checkout page
5403 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5404 if ($vipps_checkout_activated) {
5405 $data['vipps_checkout'] = array(
5406 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5407 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5408 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5409 );
5410 }
5411
5412 // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5413 $data['vipps_special_page'] = [
5414 'name' => 'vipps-payment', // slug
5415 /* translators: company name */
5416 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5417 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5418 ];
5419 return $data;
5420 }
5421
5422 // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5423 // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5424 public function maybe_create_vipps_pages () {
5425 $make_pages = false;
5426
5427 // Vipps Checkout page. LP 2026-08-18
5428 $checkoutid = wc_get_page_id('vipps_checkout');
5429 if (!$checkoutid || ! get_post_status($checkoutid)) {
5430 delete_option('woocommerce_vipps_checkout_page_id');
5431 $make_pages = true;
5432 }
5433
5434 // vipps special page, previously a fake page. LP 2026-08-18
5435 $builtin_special_page_id = static::get_special_page_id();
5436 if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5437 delete_option('woocommerce_vipps_special_page_page_id');
5438 $make_pages = true;
5439 }
5440
5441 if ($make_pages) {
5442 WC_Install::create_pages();
5443 }
5444 }
5445
5446 public function vipps_special_page_shortcode($atts, $content) {
5447 // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5448 if (is_admin()) return;
5449 if (wp_doing_ajax()) return;
5450 if (defined('REST_REQUEST') && REST_REQUEST) return;
5451 if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5452
5453 $action = $_GET['action'] ?? '';
5454 $html = "";
5455 switch ($action) {
5456 case 'wait_for_payment':
5457 $html = $this->vipps_wait_for_payment();
5458 break;
5459 case 'do_express_checkout':
5460 $html = $this->vipps_express_checkout();
5461 break;
5462 case 'buy_product':
5463 $html = $this->vipps_buy_product();
5464 break;
5465 default:
5466 $html = '';
5467 }
5468 // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5469 $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5470
5471 // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5472 return $html;
5473 }
5474
5475
5476 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5477 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5478 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5479 public function vipps_buy_product() {
5480 do_action('woo_vipps_express_checkout_page');
5481
5482 $session = WC()->session;
5483 $posted = $session->get('__vipps_buy_product');
5484 $session->set('__vipps_buy_product', false); // Reloads won't work but that's ok.
5485
5486
5487 if (!$posted) {
5488 // Find product/variation using an external shareable link
5489 if (array_key_exists('pr',$_REQUEST)) {
5490 global $wpdb;
5491 $externalkey = sanitize_text_field($_REQUEST['pr']);
5492 $search = '_vipps_shareable_link_'.esc_sql($externalkey);
5493 $existing = $wpdb->get_row("SELECT post_id from {$wpdb->prefix}postmeta where meta_key='$search' limit 1",'ARRAY_A');
5494 if (!empty($existing)) {
5495 $posted = get_post_meta($existing['post_id'], $search, true);
5496 }
5497 }
5498 }
5499
5500 $productinfo = false;
5501 if (is_array($posted)) {
5502 $productinfo = $posted;
5503 } else {
5504 $productinfo = $posted ? @json_decode($posted,true) : false;
5505 }
5506
5507 if (!$productinfo) {
5508 $title = __("Product is no longer available",'woo-vipps');
5509 $content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps');
5510 return $this->special_page_html($title,$content);
5511 }
5512
5513 // Pass the productinfo to the express checkout form
5514 $args = array();
5515 $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5516 $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5517 $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5518 $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5519
5520 $payment_method = $this->get_payment_method_name();
5521 $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5522 $bclass = esc_attr($payment_method);
5523
5524 $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5525 $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5526 $content .= "<a href='javascript:void(0)' class='single-product button vipps-buy-now $bclass' data-vipps-autostart='true' data-vipps-purchase='single' data-product_id='{$args['product_id']}' data-variation_id='{$args['variation_id']}' data-product_sku='{$args['sku']}' data-quantity='{$args['quantity']}' title='{$btitle}';
5527 >";
5528 $content .= $this->get_html_button_for_context('global');
5529 $content .= "</a>";
5530 $content .= "</div>";
5531
5532 return $content;
5533 }
5534
5535 public function vipps_express_checkout_consistency_check() {
5536 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5537 // IOK 2018-05-28
5538 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5539
5540 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5541 if (!$backurl) $backurl = home_url();
5542
5543 if (!$ok) {
5544 wc_add_notice(__('Link expired, please try again', 'woo-vipps'));
5545 wp_redirect($backurl);
5546 exit();
5547 }
5548
5549 if ( WC()->cart->get_cart_contents_count() == 0 ) {
5550 wc_add_notice(__('Your shopping cart is empty','woo-vipps'),'error');
5551 wp_redirect($backurl);
5552 exit();
5553 }
5554
5555 add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5556 }
5557
5558 // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5559 // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5560 public function vipps_express_checkout() {
5561 // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5562 if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5563 $content = __('Link expired, please try again', 'woo-vipps');
5564 return $content;
5565 }
5566
5567 do_action('woo_vipps_express_checkout_page');
5568
5569 $payment_method = $this->get_payment_method_name();
5570 $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5571 $bclass = esc_attr($payment_method);
5572 $sec = esc_attr($_REQUEST['sec']);
5573 $content = "";
5574 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5575 $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5576 $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5577 $content .= $this->get_html_button_for_context('global');
5578 $content .="</a>";
5579 $content .="</div>";
5580
5581 return $content;
5582 }
5583
5584 // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5585 private function handle_payment_poll_and_redirect () {
5586 $orderid = WC()->session->get('_vipps_pending_order');
5587
5588 $order = null;
5589 $gw = $this->gateway();
5590
5591 // Failsafe for when the session disappears IOK 2018-11-19
5592 $no_session = $orderid ? false : true;
5593 $limited_session = sanitize_text_field(@$_GET['ls']);
5594
5595 // Now we *should* have a session at this point, but the session may have been deleted,
5596 // or the session may be in another browser, because we get here by the Vipps app opening the app.
5597 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5598 // simulating the session with that.
5599 // IOK 2019-11-19, changed to using GET 2023-01-23
5600 if ($no_session && $limited_session) {
5601 $orderid = intval($_GET['id'] ?? false);
5602 }
5603 if ($orderid) {
5604 clean_post_cache($orderid);
5605 $order = wc_get_order($orderid);
5606 }
5607
5608 // if we came here with no session, check to see if we are allowed to do stuff with the order.
5609 if ($order && $no_session) {
5610 if (!$order->get_meta('_vipps_limited_session') || (!wp_check_password($limited_session, $order->get_meta('_vipps_limited_session')))) {
5611 $this->log("Wrong order session id on Vipps payment return url", 'error');
5612 $order = null; $orderid=0;
5613 } else {
5614 $session = WC()->session;
5615 if (!$session->has_session()) {
5616 $session->set_customer_session_cookie(true);
5617 }
5618
5619 $sessionorders= WC()->session->get('_vipps_session_orders');
5620 $sessionorders[$orderid] = 1;
5621 WC()->session->set('_vipps_session_orders',$sessionorders);
5622 $session->set('_vipps_pending_order', $orderid);
5623 WC()->session->save_data();
5624 }
5625 }
5626
5627 $deleted_order=0;
5628 if ($orderid && !$order) {
5629 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5630 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5631 $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5632 $deleted_order=1;
5633 }
5634
5635 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
5636
5637 // If we are done, we are done, so go directly to the end. IOK 2018-05-16
5638 $status = $deleted_order ? 'cancelled' : $order->get_status();
5639
5640 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5641 $do_poll = true;
5642
5643 // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5644 if ($do_poll && $status == 'pending') {
5645 // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5646 $newstatus = $gw->poll_and_check_order_status($order);
5647 $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5648 if ($status != $newstatus) {
5649 $status = $newstatus;
5650 clean_post_cache($orderid);
5651 $order = wc_get_order($orderid); // Reload order object
5652 }
5653 } else {
5654 // No need to do anyting here. IOK 2020-01-26
5655 }
5656
5657 // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5658 // the payment status at Vipps without polls, which will tell us if the payment succeeded in case people use custom order statuses and so on. IOK 2026-09-29
5659 $payment = 'notchecked';
5660 if ($do_poll) {
5661 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5662 }
5663
5664 // All these payment statuses are successes so go to the thankyou page.
5665 if ($payment == 'authorized' || $payment == 'complete') {
5666 // IOK 2023-07-17 this used to be called in the woocommerce_thankyou hook, now we do it here instead, since
5667 // we may need to be logged in to be able to get to that hook.
5668 $this->woocommerce_before_thankyou($order->get_id());
5669 wp_redirect($gw->get_return_url($order));
5670 exit();
5671 }
5672
5673 // We are done, but in failure. Don't poll.
5674 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5675
5676 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5677 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
5678 if ('failed' == $status) {
5679 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5680 wp_redirect($failure_redirect);
5681 exit();
5682 }
5683
5684 if ($status == 'cancelled' || $payment == 'cancelled') {
5685 $this->maybe_restore_cart($orderid,'failed');
5686 if ($failure_redirect){
5687 wp_redirect($failure_redirect);
5688 exit();
5689 }
5690 } else {
5691 // If not, enqueue the status checker IOK 2026-09-21
5692 wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5693 }
5694
5695 $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5696
5697 // Communicate this to the shortcode IOK 2026-09-21
5698 add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5699 return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5700 });
5701
5702 }
5703
5704 public function vipps_wait_for_payment() {
5705 // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5706 $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5707
5708 $orderid = $data['orderid'] ?? 0;
5709 $status = $data['status'] ?? "";
5710 $payment = $data['payment'] ?? "";
5711
5712 $order = wc_get_order($orderid);
5713 if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5714
5715 do_action('woo_vipps_wait_for_payment_page',$order);
5716 $gw = $this->gateway();
5717
5718 $content = "";
5719 if ($status == 'cancelled' || $payment == 'cancelled') {
5720 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5721 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5722 $content .= "</div>";
5723 return $this->special_page_html('', $content);
5724 }
5725
5726 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5727 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5728 $signal = $this->callbackSignal($order);
5729 $content = "";
5730 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5731
5732 if ($signal && !is_file($signal)) $signal = '';
5733 $signalurl = $this->callbackSignalURL($signal);
5734
5735 $content .= "</p></div>";
5736
5737 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5738
5739 // Carry the order status to the checking script IOK 2026-09-21
5740 $content .= "<form id='vippsdata'>";
5741 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5742 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5743 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5744 $content .= wp_nonce_field('vippsstatus','sec',1,false);
5745 $content .= "</form>";
5746
5747 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5748 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5749 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5750 $content .= "</div>";
5751
5752 $content .= "<div id=success style='display:none'><p>". __('Order confirmed', 'woo-vipps') . '</p>';
5753 $content .= "<p><a class='btn button' id='continueToThankYou' href='" . $gw->get_return_url($order) . "'>".__('Continue','woo-vipps') ."</a></p>";
5754 $content .= '</div>';
5755
5756 $content .= "<div id=failure style='display:none'><p>". __('Order cancelled', 'woo-vipps') . '</p>';
5757 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5758 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5759 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5760 $content .= "</div>";
5761
5762 return $this->special_page_html('', $content);
5763 }
5764
5765 // Returns formatted html for the vipps special page. LP 2026-08-27
5766 public function special_page_html($header, $content) {
5767 $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5768 $html = <<<EOF
5769 $header_html
5770 <div class="vipps-special-page-content">$content</div>
5771 EOF;
5772 return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5773 }
5774
5775
5776 // Support the interactivity API with data about our cart IOK 2026-02-23
5777 public function woo_vipps_store_api_cart_data() {
5778 // Reverting the condition with the directive data-wp-bind--hidden does not work, so we need the flipped bool here (hide instead of show). LP 2026-02-10
5779
5780 $checkout_page = $this->gateway()->vipps_checkout_available();
5781 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5782 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5783
5784 $cart_data = array(
5785 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5786 'cart_supports_checkout' => (bool) $checkout_page,
5787 'checkout_url' => $checkout_url,
5788 );
5789
5790 return $cart_data;
5791 }
5792
5793 public function woo_vipps_store_api_cart_schema() {
5794 return array(
5795 'cart_hide_express' => array(
5796 'description' => sprintf(__( 'Whether to hide the %1$s Express Checkout in the cart', 'woo-vipps' ), $this->get_payment_method_name()),
5797 'type' => array( 'boolean', 'null' ),
5798 'readonly' => true,
5799 ),
5800 'cart_supports_checkout' => array(
5801 'description' => sprintf(__( 'True if %1$s is active and the cart supports it', 'woo-vipps' ), $this->CheckoutName()),
5802 'type' => array( 'boolean', 'null' ),
5803 'readonly' => true,
5804 ),
5805 'checkout_url' => array(
5806 'description' => sprintf(__( 'Current checkout url based on cart state', 'woo-vipps' ), $this->get_payment_method_name()),
5807 'type' => array( 'string', 'null' ),
5808 'readonly' => true,
5809 ),
5810 );
5811 }
5812
5813 // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5814 // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5815 private function init_button_options() {
5816 /* New structure as of now
5817 * [
5818 * 'version' => x.x,
5819 * 'express' => [
5820 * 'version' => x.x, used to migrate from previous iterations
5821 * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5822 * 'global' => ['compact' => ..., 'verb' => ..., ...],
5823 * 'cart' => [...],
5824 * 'product' => [...],
5825 * 'checkout' => [...],
5826 * ...
5827 * ],
5828 * 'product_configs' => [ overrides for specific products
5829 * 1532 => ['compact' => ..., 'verb' => ..., ...],
5830 * ...
5831 * ],
5832 * ],
5833 * ]
5834 */
5835 $options = get_option('vipps_button_options2');
5836 if (!empty($options)) return;
5837
5838 $old_options = get_option('vipps_button_options');
5839 $default_config = $this->get_html_button_default_attrs();
5840 unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5841 $default_compact = array_replace($default_config, ['compact' => 'true']);
5842
5843 $default_options = [
5844 'version' => $this->button_options_version,
5845 'express' => [
5846 'version' => $this->button_options_express_version,
5847 'configs' => [
5848 'global' => $default_config,
5849 // Need compact version by default for below pages. LP 2026-07-07
5850 'catalog' => $default_compact,
5851 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5852 ],
5853 'product_configs' => [],
5854 ],
5855 ];
5856
5857 $new_options = $default_options;
5858
5859 //Actually, we have some options from the old structure IOK 2026-07-15
5860 if (!empty($old_options)) {
5861 $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5862 unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5863 }
5864
5865 // Migrate context/page mini override to new context config. LP 2026-06-26
5866 if (is_array($old_options['express']['force-mini'] ?? null)) {
5867 foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5868 if ("yes" === $use_mini) {
5869 $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5870 unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5871 $config['compact'] = 'true';
5872 $new_options['express']['configs'][$context] = $config;
5873 }
5874 }
5875 }
5876
5877 if ($this->get_payment_method_name() !== 'MobilePay') {
5878 // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5879 foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5880 if ('fi' === ($config['language'] ?? '')) {
5881 $config['language'] = 'store';
5882 $new_options['express']['configs'][$context] = $config;
5883 }
5884 }
5885 }
5886
5887 /* translators: placeholders are arrays */
5888 $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5889
5890
5891
5892 update_option('vipps_button_options2', $new_options);
5893 }
5894
5895 // Old variant string => new config array. LP 2026-06-26
5896 public function migrate_button_variant_to_config($variant_slug) {
5897 if (!is_string($variant_slug)) return [];
5898 $config = $this->get_html_button_default_attrs();
5899 $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5900 $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5901 if (str_contains($variant_slug, 'buy-now')) {
5902 $config['verb'] = 'buy';
5903 } else if (str_contains($variant_slug, 'express')) {
5904 $config['verb'] = 'express';
5905 }
5906 return $config;
5907 }
5908
5909 // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5910 public function get_express_logo_variants() {
5911 return [
5912 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5913 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5914 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5915 'express-pill' => __('Express pill', 'woo-vipps'),
5916 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5917 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5918 ];
5919 }
5920
5921
5922 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5923 public static function order_is_vipps_retryable($order_id) {
5924 $order = wc_get_order($order_id);
5925 if (!$order) return false;
5926 $api = $order->get_meta('_vipps_api');
5927 $nonexpress_epayment = 'epayment' === $api && !$order->get_meta('_vipps_express_checkout');
5928 $shipping_set = $order->get_meta('_vipps_shipping_set');
5929
5930 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5931 return $nonexpress_epayment || $shipping_set;
5932 }
5933
5934 /** Returns the plugin's rest api namespace including the version.
5935 * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
5936 public static function get_rest_namespace($version = 'latest') {
5937 $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
5938 return self::REST_NAMESPACE_BASE . "/$version";
5939 }
5940
5941 /** Returns the plugin's rest api url.
5942 * $version accepts 'latest', but you probably don't want to do that.
5943 * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
5944 public static function get_rest_url($version, $route) {
5945 return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5946 }
5947 }
5948