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

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

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