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

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

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