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

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

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