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

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

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