PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 5.4.3
Pay with Vipps and MobilePay for WooCommerce v5.4.3
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.3, at payment/Vipps.class.php

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