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

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

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