PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.3
Pay with Vipps and MobilePay for WooCommerce v6.2.3
6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 5.4.1 5.4.0 All 185 releases
woo-vipps / payment / Vipps.class.php

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

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