PluginProbe
Pay with Vipps and MobilePay for WooCommerce / trunk
Pay with Vipps and MobilePay for WooCommerce vtrunk
6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 5.4.1 5.4.0 5.3.4 trunk 1.10.0 All 182 releases
woo-vipps / payment / Vipps.class.php

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

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