PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.1.10
Pay with Vipps and MobilePay for WooCommerce v6.1.10
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.10, at payment/Vipps.class.php

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