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

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

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