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

5,664 lines 292.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 This class is for hooks and plugin managent, and is instantiated as a singleton and set globally as $Vipps. IOK 2018-02-07
4 For WP-specific interactions.
5
6
7 This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce
8 Copyright (c) 2019 WP-Hosting AS
9
10 MIT License
11
12 Copyright (c) 2019 WP-Hosting AS
13
14 Permission is hereby granted, free of charge, to any person obtaining a copy
15 of this software and associated documentation files (the "Software"), to deal
16 in the Software without restriction, including without limitation the rights
17 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 copies of the Software, and to permit persons to whom the Software is
19 furnished to do so, subject to the following conditions:
20
21 The above copyright notice and this permission notice shall be included in all
22 copies or substantial portions of the Software.
23
24 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30 SOFTWARE.
31
32
33 */
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit; // Exit if accessed directly
36 }
37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38
39 class Vipps {
40 private static $instance = null;
41
42 /* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */
43 public static $installed_gateways = [];
44
45 /* This directory stores the files used to speed up the callbacks checking the order status. IOK 2018-05-04 */
46 private $callbackDirname = 'wc-vipps-status';
47 private $countrymap = null;
48 // Used to provide the order in a callback to the session handler etc. IOK 2019-10-21
49 public $callbackorder = 0;
50
51 // True if HPOS is being used
52 public $HPOSActive = null;
53
54 // used in the fake locking mechanism using transients
55 private $lockKey = null;
56
57 public $vippsJSConfig = array();
58
59 // IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction
60 public static function CompanyName() {
61 return __("Vipps MobilePay", 'woo-vipps');
62 }
63 public static function CheckoutName($order=null) {
64 return "Vipps MobilePay Checkout"; // Do not translate
65 }
66 public static function ExpressCheckoutName($order=null) {
67 return __("Vipps Express Checkout", 'woo-vipps');
68 }
69 public static function LoginName() {
70 return __("Login with Vipps", 'woo-vipps');
71 }
72
73 public static function instance() {
74 if (!static::$instance) static::$instance = new Vipps();
75 return static::$instance;
76 }
77
78 // 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 = "<div class='express-header'>$header_text</div>";
1552 $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1553 echo "<div class='$div_classes'>$header";
1554 $this->cart_express_checkout_button_html();
1555 echo '</div>';
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 $capremain = intval($order->get_meta('_vipps_capture_remaining'));
2736 if ($captured && (!$capremain || $capremain < 2)) {
2737 print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>";
2738 return;
2739 }
2740
2741 $logo = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2742
2743 print '<button type="button" class="button vippsbutton generate-items vipps-action"
2744 data-orderid="' . $order->get_id() . '" data-action="do_capture"
2745 style="background-color:#ff5b24;border-color:#ff5b24;color:#ffffff" >
2746 <img border=0 style="display:inline;height:2ex;vertical-align:text-bottom" class="inline" alt=0 src="'.$logo.'"/> ' . __('Capture payment','woo-vipps') . '</button>';
2747
2748 }
2749
2750
2751 // This is the main callback from Vipps when payments are returned. IOK 2018-04-20
2752 public function vipps_callback() {
2753 $this->log("Callback received");
2754
2755 Vipps::nocache();
2756 // Required for Checkout, we send this early as error recovery here will be tricky anyhow.
2757 status_header(202, "Accepted");
2758
2759
2760 $raw_post = @file_get_contents( 'php://input' );
2761 $result = @json_decode($raw_post,true);
2762
2763 // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
2764 // .. and the epayment webhooks 2023-12-19
2765 $ischeckout = false;
2766 $iswebhook = false;
2767 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2768 // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
2769 if ($callback == 'checkout') {
2770 $ischeckout = true;
2771 }
2772 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
2773 if ($callback == 'webhook' || (!$ischeckout && ($result['pspReference'] ?? false))) {
2774 $iswebhook = true;
2775 }
2776
2777 $vippsorderid = ($result && isset($result['orderId'])) ? $result['orderId'] : "";
2778 // For checkout, the orderId has been renamed to "reference" IOK 2022-02-11
2779 // We set the orderId here very early so old filters and hooks will continue working - mostly used for debugging.
2780 if (!$vippsorderid && $result && isset($result['reference'])) {
2781 $vippsorderid = $result['reference'];
2782 $result['orderId'] = $result['reference'];
2783 }
2784
2785 do_action('woo_vipps_vipps_callback', $result,$raw_post);
2786
2787 if (!$result) {
2788 $error = json_last_error_msg();
2789 $this->log(sprintf(__("Did not understand callback from %1\$s:",'woo-vipps'), $this->get_payment_method_name()) . " " . $raw_post, 'error');
2790 $this->log(sprintf(__("Error was: %1\$s",'woo-vipps'), $error));
2791 return false;
2792 }
2793
2794 // For testing sites that appear not to receive callbacks
2795 if (isset($result['testing_callback'])) {
2796 $this->log(__("Received a test callback, exiting" , 'woo-vipps'), 'debug');
2797 print '{"status": 1, "msg": "Test ok"}';
2798 exit();
2799 }
2800
2801 // 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
2802 // order for it, normalize the callback data and then handle the callback. IOK 2023-12-21
2803 if ($iswebhook) {
2804 // The webhook payloads spell the msn differently. IOK 2023-12-21
2805 $msn = ($result['msn'] ?? '') ? $result['msn'] : ($result['merchantSerialNumber'] ?? '');
2806 if ($msn) {
2807 $result['msn'] = $msn;
2808 $result['merchantSerialNumber'] = $msn;
2809 }
2810 $hookdata = $this->gateway()->get_local_webhook($msn);
2811 $secret = $hookdata ? ($hookdata['secret'] ?? false) : false;
2812 if (!$secret) {
2813 $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');
2814 return false;
2815 }
2816 $verified = $this->verify_webhook($raw_post, $secret);
2817 if (!$verified) {
2818 $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');
2819 return;
2820 }
2821
2822 // 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
2823 $event = $result['name'] ?? '';
2824 $payment_events = ["CREATED", "ABORTED", "EXPIRED", "CANCELLED", "CAPTURED", "REFUNDED", "AUTHORIZED", "TERMINATED"];
2825 $callback_events = ["ABORTED","EXPIRED", "AUTHORIZED", "TERMINATED"];
2826
2827 // If this is a payment event, we should have an order too so try to retrieve it. IOK 2023-12-21
2828 $order = null;
2829 $pending = false;
2830 if ($vippsorderid && $msn && in_array($event, $payment_events)) {
2831 // Then check if the reference/vippsorderid is a pending order
2832 $order = $this->get_pending_vipps_order($vippsorderid);
2833 if ($order) {
2834 $pending = true;
2835 } else {
2836 // If it isn't, but it is a payment event, get the order id from the epayment metadata. IOK 2023-12-21
2837 try {
2838 $polldata = $this->gateway()->api->epayment_get_payment($vippsorderid, $msn);
2839 if ($polldata && isset($polldata['metadata'])) {
2840 $orderid = $polldata['metadata']['orderid'];
2841 if ($orderid) {
2842 $order = wc_get_order($orderid);
2843 if (!$order || $vippsorderid != $order->get_meta('_vipps_orderid')) {
2844 $this->log(
2845 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'),
2846 $vippsorderid, $orderid, $event), 'debug');
2847 $order = null;
2848 return;
2849 $order = null;
2850 }
2851 }
2852 }
2853 } catch (Exception $e) {
2854 $this->log(sprintf(__("Could not get orderid of reference %2\$s from %1\$s: ", 'woo-vipps'), Vipps::CompanyName(), $vippsorderid) . $e->getMessage(), 'debug');
2855 }
2856 }
2857 }
2858
2859 // This will run for all events, not just the one this handler handles IOK 2023-12-21
2860 do_action('woo_vipps_webhook_event', $result, $order);
2861
2862 // We are not interested in Checkout orders - they have their own callback systems
2863 if ($order && $order->get_meta('_vipps_checkout')) {
2864 $this->log(sprintf(__('Received webhook callback for Checkout order %1$d - ignoring since full callback should come', 'woo-vipps'), $order->get_id()), 'debug');
2865 return;
2866 }
2867 // Now we will handle everything that is a callback event. IOK 2023-12-21
2868 if (!in_array($event, $callback_events)) {
2869 return;
2870 }
2871
2872 if (!$pending) {
2873 // If the order is no longer pending, then we can safely ignore it. IOK 2023-12-21
2874 $this->log(sprintf(__('Received webhook callback for order %1$s but this is no longer pending.', 'woo-vipps'), $vippsorderid), 'debug');
2875 return;
2876 }
2877 do_action('woo_vipps_callback_webhook', $result);
2878
2879 $ok = $this->gateway()->handle_callback($result, $order, false, $iswebhook);
2880 if ($ok) {
2881 // This runs only if the callback actually handled the order, if not, then the order was handled by poll.
2882 do_action('woo_vipps_callback_handled_order', $order);
2883 }
2884
2885 exit();
2886 }
2887
2888 // This branch is only for non-webhook callbacks; which currently means Checkout only. IOK 2025-08-13
2889 $orderid = intval(@$_REQUEST['id']);
2890
2891 if (!$orderid) {
2892 $this->log(sprintf(__("There is no order with this %1\$s orderid, callback fails:",'woo-vipps'), $this->get_payment_method_name()) . " " . $vippsorderid, 'error');
2893 return false;
2894 }
2895
2896 $order = wc_get_order($orderid);
2897 if (!is_a($order, 'WC_Order')) {
2898 $this->log(__("There is no order with this order id, callback fails:",'woo-vipps') . " " . $orderid, 'error');
2899 return false;
2900 }
2901
2902 // a small bit of security
2903 if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) {
2904 $this->log("Wrong authtoken on Vipps payment details callback", 'error');
2905 exit();
2906 }
2907
2908 do_action('woo_vipps_callback_checkout', $result);
2909
2910 $gw = $this->gateway();
2911
2912 // If neccessary, the order session will be restored in this method, and if so it will be reset before the exit happens
2913 // to reduce issues with users simultaneously returning to the store. IOK 2023-07-18
2914 $ok = $gw->handle_callback($result, $order, $ischeckout);
2915 if ($ok) {
2916 // This runs only if the callback actually handled the order, if not, then the order was handled by poll.
2917 do_action('woo_vipps_callback_handled_order', $order);
2918 }
2919
2920 exit();
2921 }
2922
2923 // Returns true iff we can verify that the webhook we just received is valid and that we know its secret IOK 2023-12-21
2924 public function verify_webhook($serialized, $secret) {
2925 // Extract the necessary headers.
2926 $expected_auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['HTTP_X_VIPPS_AUTHORIZATION'] ?? "");
2927 $expected_date = $_SERVER['HTTP_X_MS_DATE'] ?? '';
2928
2929 // Check if the date header is present and within an acceptable range (e.g., +/- 5 minutes) NT 2023-12-22
2930 if (!$this->isDateValid($expected_date)) {
2931 return false; // Date is not valid or not within the acceptable range
2932 }
2933
2934 // Prepare the data for signing.
2935 $hashed_payload = base64_encode(hash('sha256', $serialized, true));
2936 $path_and_query = $_SERVER['REQUEST_URI'];
2937 $host = $_SERVER['HTTP_HOST'];
2938
2939 // Construct the string to sign.
2940 $toSign = "POST\n{$path_and_query}\n{$expected_date};{$host};{$hashed_payload}";
2941
2942 // Generate the HMAC signature.
2943 $signature = base64_encode(hash_hmac('sha256', $toSign, $secret, true));
2944
2945 // Construct the authorization string.
2946 $auth = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={$signature}";
2947
2948 // Compare the generated auth string with the expected one.
2949 // Hash_equals is used to mitigate timing attacks NT 2023-12-22
2950 return hash_equals($auth, $expected_auth);
2951 }
2952
2953 // Helper function to validate the date NT 2023-12-22
2954 private function isDateValid($dateHeader) {
2955 // Define the acceptable time leeway (e.g., 5 minutes)
2956 $leewayInSeconds = 300;
2957
2958 // Convert the header date to a Unix timestamp
2959 $headerTime = strtotime($dateHeader);
2960
2961 // Check if the date is valid
2962 if ($headerTime === false) {
2963 return false; // Invalid date
2964 }
2965
2966 // Get the current time
2967 $currentTime = time();
2968
2969 // Check if the date is within the acceptable range
2970 return abs($currentTime - $headerTime) <= $leewayInSeconds;
2971 }
2972
2973
2974 // Helper function to get ISO-3166 two-letter country codes from country names as supplied by Vipps
2975 // IOK 2021-11-22 Seems as if Vipps is now sending two-letter country codes at least some times
2976 public function country_to_code($countryname) {
2977 if (!$this->countrymap) $this->countrymap = unserialize(file_get_contents(dirname(__FILE__) . "/lib/countrycodes.php"));
2978 $mapped = @$this->countrymap[strtoupper($countryname)];
2979 $code = WC()->countries->get_base_country();
2980 if ($mapped) {
2981 $code = $mapped;
2982 } else if (strlen($countryname)==2) {
2983 $code = strtoupper($countryname);
2984 }
2985 $code = apply_filters('woo_vipps_country_to_code', $code, $countryname);
2986 return $code;
2987 }
2988
2989 // To be added to the 'woocommerce_session_handler' filter IOK 2021-06-21
2990 public static function getCallbackSessionClass ($handler) {
2991 return "VippsCallbackSessionHandler";
2992 }
2993
2994 // Go back to the basic woocommerce session handler if we have temporarily restored session from an Vipps order 2021-06-21
2995 // Only to be called by wp-cron, callbacks etc. Will not actually destroy the stored session, just the current session.
2996 public function callback_destroy_session () {
2997 $this->callbackorder = null;
2998 remove_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass'));
2999 if (version_compare(WC_VERSION, '3.6.4', '>=')) {
3000 // This will replace the old session with this one. IOK 2019-10-22
3001 WC()->initialize_session();
3002 } else {
3003 // Do this manually for 3.6.3 and below
3004 WC()->session = new WC_Session_Handler();
3005 WC()->session->init();
3006 }
3007 }
3008
3009 // When we get callbacks from Vipps, we want to restore the Woo session in place for the order.
3010 // For many plugins this is strictly neccessary because they don't check to see if there is a session
3011 // or not - and for many others, wrong results are produced without the (correct) session. IOK 2019-10-22
3012 public function callback_restore_session ($orderid) {
3013 $this->callbackorder = $orderid;
3014 require_once(dirname(__FILE__) . "/VippsCallbackSessionHandler.class.php");
3015 add_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass'));
3016 // Support older versions of Woo by inlining initialize session IOK 2019-12-12
3017 if (version_compare(WC_VERSION, '3.6.4', '>=')) {
3018 // This will replace the old session with this one. IOK 2019-10-22
3019 WC()->initialize_session();
3020 } else {
3021 // Do this manually for 3.6.3 and below
3022 $session_class = "VippsCallbackSessionHandler";
3023 WC()->session = new $session_class();
3024 WC()->session->init();
3025 }
3026
3027 $customerid= 0;
3028 if (WC()->session && is_a(WC()->session, 'WC_Session_Handler')) {
3029 $customerid = WC()->session->get('express_customer_id');
3030 }
3031 if ($customerid) {
3032 WC()->customer = new WC_Customer($customerid); // Reset from session, logged in user
3033 } else {
3034 WC()->customer = new WC_Customer(); // Reset from session
3035 }
3036 // This is to provide defaults; real address will come from Vipps in this sitation. IOK 2019-10-25
3037 WC()->customer->set_billing_address_to_base();
3038 WC()->customer->set_shipping_address_to_base();
3039
3040 // The normal "restore cart from session" thing runs on wp_loaded, and only there, and cannot
3041 // be called from outside the WC_Cart object. We cannot easily run this on wp_loaded, and it does
3042 // do much more than it should for this particular use:
3043 // We have already created the order, so we only want this cart for the shipping calculations.
3044 // Therefore, we will just recreate the 'data' bit of the contents and set the cart contents directly
3045 // from the now restored session. IOK 2020-04-08
3046 // IOK 2022-06-28 Updated to also call the woocommerce_get_cart_item_from_session filters and to correctly handle
3047 // coupons.
3048 $newcart = array();
3049 if (WC()->session->get('cart', false)) {
3050 foreach(WC()->session->get('cart',[]) as $key => $values) {
3051 $product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );
3052 $session_data = array_merge($values, array( 'data' => $product));
3053 $newcart[$key] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );
3054 }
3055 } else {
3056 $this->log(sprintf(__("Could not restore cart from session of order %1\$d", 'woo-vipps'), $orderid));
3057 }
3058 if (WC()->cart) {
3059
3060 // When doing "calculate_totals" on a cart, Woo will now compare "previous shipping methods" with
3061 // "current shipping methods" and reset the chosen shipping methods even if it is still available.
3062 // This becomes a problem because Woo only loads the pickup location methods in a few places - mostly checkout -
3063 // so if we chose a shipping method while these were available, we'd get ourselves reset just by calculating
3064 // cart totals. Fix this by saving and restoring this value. IOK 2025-11-05
3065 $all_chosen = WC()->session->get( 'chosen_shipping_methods' );
3066
3067 WC()->cart->set_totals( WC()->session->get( 'cart_totals', null ) );
3068 WC()->cart->set_applied_coupons( WC()->session->get( 'applied_coupons', array() ) );
3069 WC()->cart->set_coupon_discount_totals( WC()->session->get( 'coupon_discount_totals', array() ) );
3070 WC()->cart->set_coupon_discount_tax_totals( WC()->session->get( 'coupon_discount_tax_totals', array() ) );
3071 WC()->cart->set_removed_cart_contents( WC()->session->get( 'removed_cart_contents', array() ) );
3072 WC()->cart->set_cart_contents($newcart);
3073 // IOK 2020-07-01 plugins expect this to be called: hopefully they'll not get confused by it happening twice
3074 do_action( 'woocommerce_cart_loaded_from_session', WC()->cart);
3075 WC()->cart->calculate_totals(); // And if any of them changed anything, recalculate the totals again!
3076 // See above: Reset chosen shipping methods to avoid having it be reset by Woo for no good reason.
3077 if ($all_chosen) {
3078 WC()->session->set('chosen_shipping_methods', $all_chosen);
3079 }
3080 } else {
3081 // Apparently this happens quite a lot, so don't log it or anything. IOK 2021-06-21
3082 }
3083 return WC()->session;
3084 }
3085
3086
3087
3088 // Based on either a logged-in user, or the stores' default address, get the address to use when using
3089 // the Express Checkout static shipping feature
3090 // This is neccessary because WC()->customer->set_shipping_address_to_base() only sets country and state.
3091 // IOK 2020-03-18
3092 public function get_static_shipping_address_data () {
3093 // This is the format used by the Vipps callback, we are going to mimic this.
3094 // IOK 2025-05-08 now also using the format used by Checkout in addition to Express. -- streetAddress, postalCode, region
3095 $defaultdata = array('addressId'=>0, "addressLine1"=>"", "addressLine2"=>"", "streetAddress"=>"", "country"=>"NO", "city"=>"", "postalCode"=>"", "postCode"=>"", "addressType"=>"Home");
3096 // 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.
3097 $countries=new WC_Countries();
3098 $defaultdata['country'] = $countries->get_base_country();
3099 $defaultdata['city'] = $countries->get_base_city();
3100 $defaultdata['region'] = $countries->get_base_city();
3101 $defaultdata['postalCode'] = $countries->get_base_postcode();
3102 $defaultdata['postCode'] = $countries->get_base_postcode();
3103 $defaultdata['streetAddress'] = $countries->get_base_address();
3104 $defaultdata['addressLine1'] = $countries->get_base_address();
3105 return $defaultdata;
3106 }
3107
3108 // Getting shipping methods/costs for a given order to Vipps for express checkout
3109 public function vipps_shipping_details_callback() {
3110 Vipps::nocache();
3111
3112 $raw_post = @file_get_contents( 'php://input' );
3113 $result = @json_decode($raw_post,true);
3114
3115 if (!$result) {
3116 if (empty(trim($raw_post))) {
3117 status_header(400, "Empty address info");
3118 print "No address";
3119 } else {
3120 status_header(400, "Invalid JSON");
3121 print "Invalid JSON";
3122 }
3123 $error = json_last_error_msg();
3124 $this->log(sprintf(__("Error getting customer data in the %1\$s shipping details callback: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $error));
3125 $this->log(__("Raw input was ", 'woo-vipps'));
3126 $this->log($raw_post);
3127 exit();
3128 }
3129
3130 // IOK 2025-08-15 Express Checkout (now) passes the reference/order-id in the data, but Checkout passes it in the URL, which we
3131 // capture in a callback= parameter added at the end. Format is
3132 // '/v3/checkout/woodigitalt4780/shippingDetails'
3133 $vippsorderid = "";
3134 $callback = sanitize_text_field($_REQUEST['callback'] ?? "");
3135 do_action('woo_vipps_shipping_details_callback', $result,$raw_post,$callback); // This is for debugging. IOK 2025-08-15
3136
3137 if ($callback) {
3138 $data = array_reverse(explode("/",$callback));
3139 $vippsorderid = !empty($data) ? ($data[1] ?? "") : ""; // Second element - callback is /v3/checkout/woodigitalt4780/shippingDetails
3140 } elseif (isset($result['reference'])) {
3141 $vippsorderid = $result['reference'];
3142 }
3143
3144 $orderid = intval($_REQUEST['id'] ?? 0);
3145 if (!$orderid) {
3146 status_header(404, "Unknown order");
3147 print "Unknown order";
3148 $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');
3149 exit();
3150 }
3151
3152 // This is for debugging sites where shipping handling fails because of blocks etc IOK 2026-01-15
3153 $this->log(sprintf(__("Received shipping callback for order %d", 'woo-vipps'), $orderid));
3154
3155 do_action('woo_vipps_shipping_details_callback_order', $orderid, $vippsorderid);
3156
3157 $order = wc_get_order($orderid);
3158 if (!$order) {
3159 status_header(404, "Unknown order");
3160 print "Unknown order";
3161 $this->log(__('Could not find Woo order with id:', 'woo-vipps') . " " . $orderid, 'error');
3162 exit();
3163 }
3164 if (!self::is_vipps_order($order)) {
3165 status_header(400, "Invalid order");
3166 print "Invalid order";
3167 $this->log(__('Invalid order for shipping callback:', 'woo-vipps') . " " . $orderid, 'error');
3168 exit();
3169 }
3170 // a small bit of security
3171 if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) {
3172 status_header(403, "Wrong auth");
3173 print "Wrong auth";
3174 $this->log("Wrong authtoken on shipping details callback", 'error');
3175 exit();
3176 }
3177 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3178 status_header(400, "Invalid order id");
3179 print "Invalid order id";
3180 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3181 exit();
3182 }
3183
3184 // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3185 // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3186 // This needs to be done before "calculate totals".
3187 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3188 $ischeckout = $order->get_meta('_vipps_checkout');
3189
3190 $this->callback_restore_session($orderid);
3191
3192 // 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
3193 // here we will add support for PickupLocations. Also called for static shipping.
3194 // IOK 2025-08-14 now also supported for Express Checkout
3195 $this->load_extra_shipping_methods($order, $result, $ischeckout);
3196
3197 $return = $this->vipps_shipping_details_callback_handler($order, $result,$vippsorderid, $ischeckout);
3198
3199 // Express checkout wants the data wrapped in a object with a 'groups' attribute, Checkout wants thing unwrapped.
3200 // Dispatch on the known type. IOK 2025-08-15
3201 if ($ischeckout) {
3202 $return = $return['shippingDetails'];
3203 } else {
3204 // Note that this is of course different from both Checkout and static shipping.
3205 $return = [ "groups" => $return ];
3206 }
3207
3208 $json = json_encode($return);
3209
3210 header("Content-type: application/json; charset=UTF-8");
3211 print $json;
3212 // Just to be sure, save any changes made to the session by plugins/hooks IOK 2019-10-22
3213 if (is_a(WC()->session, 'WC_Session_Handler')) WC()->session->save_data();
3214 exit();
3215 }
3216
3217 // This function calculates and returns one of two possible JSON representations to Vipps MobilePay, one for Express and one for Checkout.
3218 // First, an intermediate representation is created, based on the original Express API. This is kept because users may still have filters
3219 // that expects this representation. Later, these are transformed and augmented for the newer APIs. IOK 2025-08-14
3220 // Also used for Static Shipping for both representations. IOK 2025-08-14
3221 public function vipps_shipping_details_callback_handler($order, $vippsdata,$vippsorderid, $ischeckout) {
3222 // 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
3223 if ($ischeckout) add_filter('woo_vipps_is_vipps_checkout', '__return_true');
3224
3225 // We may have an address already in the Order, and no *new* address, when recalculating shipping options after modifying the order.
3226 // We'll still create a $vippsdata struct so that old filters can do whatever is neccessary. IOK 2025-09-16
3227 $new_address = !empty($vippsdata);
3228 if (!$new_address) {
3229 $vippsdata['addressLine1'] = $order->get_shipping_address_1();
3230 $vippsdata['addressLine2'] = $order->get_shipping_address_2();
3231 $vippsdata['postCode'] = $order->get_shipping_postcode();
3232 $vippsdata['city'] = $order->get_shipping_city();
3233 $vippsdata['country'] = $order->get_shipping_country();
3234 }
3235
3236 // Since we have legacy users that may have filters defined on these values, we will translate newer apis to the older ones.
3237 // so filters will continue to work for newer apis/checkout
3238 if (isset($vippsdata['streetAddress'])){
3239 $vippsdata['addressLine1'] = $vippsdata['streetAddress'];
3240 $vippsdata['addressLine2'] = "";
3241 }
3242 if (isset($vippsdata['region'])) {
3243 $vippsdata['city'] = $vippsdata['region'];
3244 }
3245 if (isset($vippsdata['postalCode'])) {
3246 $vippsdata['postCode'] = $vippsdata['postalCode'];
3247 }
3248 // Translations for different versions of the API end
3249
3250 $addressid = isset($vippsdata['addressId']) ? $vippsdata['addressId'] : "";
3251 $addressline1 = $vippsdata['addressLine1'];
3252 $addressline2 = $vippsdata['addressLine2'];
3253
3254 // IOK 2019-08-26 apparently the apps contain a lot of addresses with duplicate lines
3255 if ($addressline1 == $addressline2) $addressline2 = '';
3256 if (!$addressline2) $addressline2 = '';
3257
3258 $country = $vippsdata['country'];
3259 $city = $vippsdata['city'];
3260 $postcode= $vippsdata['postCode'];
3261
3262 // Old code here treated "Sofienberggata 12" as a special Vipps pro-forma address; this is no longer necessary.
3263 // If we have gotten a new address from Express or Checkout, update the order. IOK 2025-09-16.
3264 if ($new_address) {
3265 $order->set_billing_address_1($addressline1);
3266 $order->set_billing_address_2($addressline2);
3267 $order->set_billing_city($city);
3268 $order->set_billing_postcode($postcode);
3269 $order->set_billing_country($country);
3270 $order->set_shipping_address_1($addressline1);
3271 $order->set_shipping_address_2($addressline2);
3272 $order->set_shipping_city($city);
3273 $order->set_shipping_postcode($postcode);
3274 $order->set_shipping_country($country);
3275 $order->save();
3276 }
3277
3278 // This is *essential* to get VAT calculated correctly. That calculation uses the customer, which uses the session.IOK 2019-10-25
3279 // We don't *save* this to the customer, because this may happen in a callback from Checkout where the customers' session is live and
3280 // the address info is from Checkout (and not necessarily the customers real address). IOK 2025-09-12
3281 if (WC()->customer) {
3282 WC()->customer->set_billing_location($country,'',$postcode,$city);
3283 WC()->customer->set_shipping_location($country,'',$postcode,$city);
3284 } else {
3285 $this->log("No customer! when trying to calculate shipping");
3286 }
3287
3288 // If you need to do something before the cart is manipulated, this is where it must be done.
3289 // It is possible for a plugin to require a session when manipulating the cart, which could
3290 // currently crash the system. This could be used to avoid that. IOK 2019-10-09
3291 do_action('woo_vipps_shipping_details_before_cart_creation', $order, $vippsorderid, $vippsdata);
3292
3293 // calculate_totals() overwrites the session chosen_shipping_methods to default if it think it changed,
3294 // which will be true if the pickup points are missing from previously. Pickup points only get loaded in woos checkout.
3295 // So reset this to what it was before calling calculate_totals(). LP 2025-11-05
3296 // To be more specific if the *list of available methods* change, it will reset the chosen shipping method,
3297 // even if the chosen shipping method is actually still available. We need to call calculate_totals on the cart,
3298 // so we need to save + restore this.
3299 $chosen = null;
3300 $all_chosen = null;
3301 if (is_a(WC()->session, 'WC_Session_Handler')) {
3302 $all_chosen = WC()->session->get( 'chosen_shipping_methods' );
3303 if (!empty($all_chosen)) $chosen= $all_chosen[0];
3304 }
3305
3306 // Previously, we would create a shoppingcart at this point, because we would not have access to the 'live' one,
3307 // but it turns out this isn't actually possible. Any cart so created will become "the" cart for the Woo front end,
3308 // and anyway, some plugins override the class of the cart, so just using WC_Cart will sometimes break.
3309 // Now however, the session is stored in the order, and the cart will not have been deleted, so we should
3310 // now be able to calculate shipping for the actual cart with no further manipulation. IOK 2020-04-08
3311
3312 // Turns out it is possible for the session - and the cart - to have been deleted at this point, for whatever reason.
3313 // Login will do it, probably some other plugins as well. So if we have no cart at this point, we will ressurect the
3314 // probable cart based on the order. This is only neccessary because Woo will not let us calculate shipping for an *order*.
3315 // IOK 2024-04-09
3316 $cart_is_reconstructed = $this->maybe_reconstruct_cart($order->get_id());
3317
3318 WC()->cart->calculate_totals();
3319
3320 // See above. Restore chosen shipping methods if neccessary. IOK 2025-11-05
3321 if ($all_chosen) {
3322 WC()->session->set('chosen_shipping_methods', $all_chosen);
3323 }
3324
3325 $acart = WC()->cart;
3326
3327 $shipping_methods = array();
3328 $shipping_tax_rates = WC_Tax::get_shipping_tax_rates();
3329
3330
3331 // If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20
3332 if (!$acart->needs_shipping()) {
3333 $no_shipping_taxes = WC_Tax::calc_shipping_tax('0', $shipping_tax_rates);
3334 $shipping_methods['none_required:0'] = new WC_Shipping_Rate('none_required:0',__('No shipping required','woo-vipps'),0,$no_shipping_taxes, 'none_required', 0);
3335 } else {
3336 // Ensure the shipping packages we use has the current order address IOK 2025-09-12
3337 $destination = [ 'country' => $country, 'state' => '', 'postcode' => $postcode, 'city'=> $city, 'address' => $addressline1, 'address_1' => $addressline1, 'address_2' => $addressline2 ];
3338 add_filter('woocommerce_cart_shipping_packages', function ($packages) use($destination) {
3339 $new = [];
3340 foreach($packages as $package) {
3341 $package['destination'] = $destination;
3342 $new[] = $package;
3343 }
3344 return $new;
3345 });
3346
3347 $packages = apply_filters('woo_vipps_shipping_callback_packages', WC()->cart->get_shipping_packages());
3348 $shipping = WC()->shipping->calculate_shipping($packages);
3349
3350 $shipping_methods = WC()->shipping->packages[0]['rates']; // the 'rates' of the first package is what we want.
3351 }
3352
3353 // No exit here, because developers can add more methods using the filter below. IOK 2018-09-20
3354 if (empty($shipping_methods)) {
3355 $name = $ischeckout ? Vipps::CheckoutName() : Vipps::ExpressCheckoutName();
3356 $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');
3357 $this->log(sprintf(__('Address given for %1$s was %2$s', 'woo-vipps'), $order->get_id(),
3358 ($addressline1 . " " . $addressline2 . " " . $city . " " . $postcode . " " . $country)
3359 ), 'debug');
3360
3361 }
3362
3363 // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3364 // 'dynamic pricing' 2023-01-26
3365 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3366 $taxrate = 0;
3367 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
3368 $taxrate = current($shipping_tax_rates)['rate'];
3369 }
3370 $order->update_meta_data('_vipps_shipping_tax_rates', $taxrate);
3371
3372 // Merchant is using the old 'woo_vipps_shipping_methods' filter, and hasn't chosen to disable it. Use legacy methd.
3373 // IOK 2025-08-14 I think we should add a deprecation notice to this now. It really should not be used anymore. FIXME
3374 if (has_action('woo_vipps_shipping_methods') && $this->gateway()->get_option('newshippingcallback') != 'new') {
3375 return $this->legacy_shipping_callback_handler($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart);
3376 }
3377
3378 // Earlier we sorted shipping methods based on price; currently we just use WooCommerce's order, but we
3379 // provide this filter for people who would prefer the old logic.
3380 $shipping_methods = apply_filters('woo_vipps_sort_shipping_methods', $shipping_methods, $order);
3381
3382 // 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
3383 $methods = array();
3384 $i=-1;
3385
3386 foreach ($shipping_methods as $key=>$rate) {
3387 $i++;
3388 $method = array();
3389 $method['priority'] = $i;
3390 $method['default'] = false;
3391 $method['rate'] = $rate;
3392 $methods[$key]= $method;
3393 }
3394 $chosen = apply_filters('woo_vipps_default_shipping_method', $chosen, $shipping_methods, $order);
3395
3396 if ($chosen && !isset($methods[$chosen])) {
3397 $chosen = null; // Actually that isn't available
3398 $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()));
3399 }
3400
3401 if (!$chosen) {
3402 // Find first method that isn't 'local_pickup'
3403 // or pickup_location. IOK 2025-05-07
3404 foreach($methods as $key=>&$data) {
3405 $mid = $data['rate']->get_method_id();
3406 if ($mid != 'local_pickup' && $mid != 'pickup_location') {
3407 $chosen = $key;
3408 break;
3409 }
3410 }
3411 // Ok, just pick the first
3412 if (!$chosen) {
3413 foreach($methods as $key=>&$data) {
3414 $chosen = $key;
3415 break;
3416 }
3417
3418 }
3419 }
3420 if (isset($methods[$chosen])) {
3421 $methods[$chosen]['default'] = true;
3422 }
3423 $methods = apply_filters('woo_vipps_express_checkout_shipping_rates', $methods, $order, $acart);
3424
3425 // Just to be sure, if the current cart was reconstructed from an order, we will delete it now after
3426 // last use of $acart
3427 if ($cart_is_reconstructed) {
3428 WC()->cart->empty_cart();
3429 }
3430
3431 $vippsmethods = array();
3432
3433 // Just a utility from shippingMethodIds to the non-serialized rates, and from the same to the non-serialized
3434 // shipping methods - the last stores settings, the first store metadata
3435 // 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
3436 $ratemap = array();
3437 $methodmap = array();
3438
3439 // We need access to the extended settings of the shipping methods.
3440 // This is for the 'new' local pickup feature for Woo. IOK 2025-08-14
3441 $methods_classes = WC()->shipping->get_shipping_method_class_names();
3442 $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.
3443
3444 // 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,
3445 // e.g used in the Checkout ajax poll shipping-change event. LP 2026-03-20
3446 $rate_id_map = [];
3447
3448 $has_free_shipping = false;
3449 foreach($methods as $method) {
3450 $rate = $method['rate'];
3451 $methodid = $rate->get_method_id();
3452
3453 // Extended settings are stored in these objects
3454 $methodclass = $methods_classes[$methodid] ?? null;
3455 $shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null;
3456
3457 $tax = $rate->get_shipping_tax() ?: 0;
3458 $cost = $rate->get_cost() ?: 0;
3459 $label = $rate->get_label();
3460
3461 if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) {
3462 $has_free_shipping = true;
3463 }
3464
3465 // 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.
3466 // Actually, we probably *can* use the method id, because other addresses are irellevant. But still, add a random factor
3467 $rand = md5($methodid . bin2hex(random_bytes(32))); // Random enough, 32 chars
3468 // Ensure this never is over 100 chars. Use a dollar sign to indicate 'new method' IOK 2020-02-14
3469 // Reserve 8 chars to contain a : and an option index for Express Checkout IOK 2025-08-15
3470 // 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.
3471 $key = '$' . substr($methodid,0,58) . '$' . $rand;
3472 $vippsmethod = array();
3473 $vippsmethod['isDefault'] = @$method['default'] ? 'Y' :'N';
3474 $vippsmethod['priority'] = $method['priority'];
3475
3476 $rate_id_map[$key] = $rate->get_id();
3477
3478 // It seems woo actually computes rounding of prices and taxes *separately* when computing
3479 // shipping costs, but we can't really assume this (or that all plugins do this, and so on.)
3480 // Therefore we compute shipping cost with rounding *both ways* and choose the more expensive one -
3481 // this way we should reserve enough money to complete the order in all cases. IOK 2025-09-30
3482 $shippingcostA = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
3483 $shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,''));
3484 $shippingcost = max($shippingcostA, $shippingcostB);
3485
3486 $vippsmethod['shippingCost'] = $shippingcost;
3487 $vippsmethod['shippingMethod'] = $rate->get_label();
3488 $vippsmethod['shippingMethodId'] = $key;
3489 $vippsmethods[]=$vippsmethod;
3490
3491 // Metadata and settings stored for later use for Vipps Checkout
3492 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3493 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3494 // finalize the order. IOK 2025-08-15
3495 $ratemap[$key]=$rate;
3496 $methodmap[$key]=$shipping_method;
3497 }
3498
3499 if (is_a(WC()->session, 'WC_Session')) {
3500 WC()->session->set('vipps_shipping_rate_id_map', $rate_id_map);
3501 } else {
3502 /* translators: order id */
3503 $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');
3504 }
3505
3506
3507 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3508 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3509 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3510
3511 // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3512 // IOK 2025-08-15 and new Express yet another slightly different format.
3513 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3514 if ($ischeckout) {
3515 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
3516 } else { // New express format. LP 2025-05-26
3517 $return = $this->express_format_shipping_methods($return, $ratemap, $methodmap, $order);
3518 $return = $this->express_group_shipping_methods($return, $ratemap, $methodmap, $order);
3519 $return = apply_filters('woo_vipps_express_json_shipping_methods', $return, $order); // wat
3520 }
3521
3522 // 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
3523 $storedmethods = array();
3524 $errormethods = array();
3525 foreach($ratemap as $key => $rate) {
3526 $serialized = '';
3527 try {
3528 // We use serialize here instead of json_encode because we need the object back.
3529 // we base64-encode the serialized object, because it is to be stored in a database in a text field.a IOK 2025-12-12
3530 $raw = @serialize($rate);
3531 $serialized = $raw ? @base64_encode($raw) : null;
3532 if (!$serialized) {
3533 throw new Exception("Could not serialize rate $key");
3534 }
3535 // Retrieve these precalculated rates on return from the store IOK 2020-02-14
3536 $storedmethods[$key] = $serialized;
3537 } catch (Exception $e) {
3538 $errormethods[] = $key;
3539 $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');
3540 $this->log($rate, 'error');
3541 continue;
3542 }
3543 }
3544
3545 // Remove any methods from the return that was not serializable
3546 if (!empty($errormethods)) {
3547 $fixedreturn = [];
3548 if ($ischeckout) {
3549 foreach($errormethods as $problem) {
3550 foreach($return['shippingDetails'] as $method) {
3551 $id = preg_replace('!:\d+$!', "", $method['id']);
3552 if ($id != $problem) $fixedreturn[] = $method;
3553 }
3554 }
3555 $return['shippingDetails'] = $fixedreturn;
3556 } else {
3557 foreach($errormethods as $problem) {
3558 foreach($return as $method) {
3559 $option = $method['options'][0];
3560 $id = preg_replace('!:\d+$!', "", $option['id']);
3561 if ($id != $problem) $fixedreturn[] = $method;
3562 }
3563 }
3564 $return = $fixedreturn;
3565 }
3566 }
3567
3568
3569 // We'll also store whether or not this set of rates include free shipping in some way. IOK 2025-09-16
3570 $storedmethods['_meta_has_free_shipping'] = $has_free_shipping;
3571 $storedmethods['_is_base64'] = true;
3572
3573 $order->update_meta_data('_vipps_express_checkout_shipping_method_table', $storedmethods);
3574 $order->save_meta_data();
3575 return $return;
3576 }
3577
3578 // Translate from the old to the new express format. LP 2025-05-26
3579 public function express_format_shipping_methods ($return, &$ratemap, $methodmap, $order) {
3580 $translated = array();
3581 $currency = $order->get_currency();
3582
3583 // First, we'll translate the legacy format originally used by express to the new one (that may
3584 // still be in use by filters etc), then add hooks to modify options and other new features.
3585 // IOK 2025-11-19
3586 foreach ($return['shippingDetails'] as $m) {
3587 $m2 = array();
3588 $options = [];
3589
3590 $m2['isDefault'] = ($m['isDefault']=='Y') ? true : false;
3591 $m2['priority'] = $m['priority'];
3592 $m2['brand'] = 'OTHER'; // the default. This is replaced for certain brands. LP 2025-05-26
3593 $m2['type'] = 'OTHER'; // default, replaced for certain types. LP 2025-05-26
3594
3595 $id = $m['shippingMethodId'];
3596 $rate = $ratemap[$id];
3597 $shipping_method = $methodmap[$id];
3598
3599 if ($rate->method_id == 'pickup_location') {
3600 $m2['type'] = 'PICKUP_POINT';
3601 }
3602
3603 // Each shipping method needs a list of options at this point.
3604 $options = [];
3605
3606
3607 // A rate can have a delivery time as a string in both Woo and Express
3608 $delivery_time = "";
3609 if (version_compare(WC_VERSION, '9.2.0', '>=')) {
3610 $delivery_time = $rate->get_delivery_time();
3611 }
3612
3613 // And some rates have metadata, such as pickup locations (local_delivery).
3614 $meta = $rate->get_meta_data();
3615 // We can also support descriptions, in the "meta" field
3616 $description = $rate->get_description();
3617
3618 $option = [];
3619 $option['priority'] = $m['priority'];
3620 $option['name'] = $m['shippingMethod'];
3621 $option['id'] = $id;
3622 $option['amount'] = [ 'value' => round(100*$m['shippingCost']), 'currency' => $currency ];
3623 if ($delivery_time) $option['estimatedDelivery'] = $delivery_time;
3624 if ($description) $entry['meta'] = $description;
3625 $options[] = $option;
3626
3627 if (isset($meta['brand'])) {
3628 $m2['brand'] = $meta['brand'];
3629 } else {
3630 // specialcase some known methods so they get brands, and put the label into the description
3631 if ($shipping_method && is_a($shipping_method, 'WC_Shipping_Method') && get_class($shipping_method) == 'WC_Shipping_Method_Bring_Pro') {
3632 $m2['brand'] = "POSTEN";
3633 }
3634 $m2['brand'] = apply_filters('woo_vipps_shipping_method_brand', $m2['brand'],$shipping_method, $rate);
3635 }
3636
3637 if ($m2['brand'] != "OTHER" && isset($meta['type'])) {
3638 $m2['type'] = apply_filters('woo_vipps_shipping_method_type', $meta['type'], $shipping_method, $rate);
3639 }
3640 $m2['options'] = $options;
3641
3642 // Now allow custom code to modify both the rate (adding metadata, mostly) and the Vipps shipping method (probably adding
3643 // options, changing the brand etc) IOK 2025-11-19
3644 // For an example, see the express_add_pickup_location_options method. IOK 2025-11-19
3645 list ($rate, $m2) = apply_filters('woo_vipps_modify_express_checkout_rate', [$rate, $m2], $shipping_method, $rate, $order);
3646 $ratemap[$id] = $rate; // Modify the ratemaps copy with any new data here - ratemap is passed by reference IOK 2025-11-19
3647
3648 $translated[] = $m2;
3649 }
3650
3651 return $translated;
3652 }
3653
3654 // This adds extra options for express checkout shipping rates that implement the 'woo_vipps_shipping_method_pickup_points' filter,
3655 // making these into groups with a dropdown for the exact shipping location as separate options.
3656 // This will create multiple pointers to the same shipping rate, which will be extended with a metadata field containing the pickup point.
3657 // That is, this is *not* for local_pickup, but for legacy local pickup and other shipping methods that have the same rate price, but
3658 // allows the user to select a location. IOK 2025-08-15
3659 public function express_add_pickup_location_options ( $data, $shipping_method, $rate, $order) {
3660 list ($rate, $m2) = $data;
3661 $pickup_points = apply_filters('woo_vipps_shipping_method_pickup_points', [], $rate, $shipping_method, $order);
3662 if (empty($pickup_points)) return $data;
3663 if (count($m2['options'])>1) return $data;
3664
3665 $index = 0;
3666 $pickup_point_table = [];
3667 $option = $m2['options'][0];
3668 $id = $option['id'];
3669
3670 foreach($pickup_points as $point) {
3671 $index++; // Start at 1
3672 $entry = $option; // This is a copy in PHP
3673
3674 $addr = [];
3675 foreach(['name', 'address', 'postalCode', 'city', 'country'] as $key) {
3676 $v = trim($point[$key]);
3677 if (!empty($v)) $addr[$key] = $v;
3678 }
3679 // To avoid confusion, force the keys to be strings. IOK 2025-08-15
3680 $pickup_point_table["i".$index] = $addr;
3681
3682 // This is for display in the App only IOK 2025-08-15
3683 $description = join(", ", array_values($addr));
3684 $description = trim(apply_filters('woo_vipps_shipping_option_meta', trim($description, " ,"), $rate, $shipping_method, $order));
3685 if ($description) $entry['meta'] = $description;
3686
3687 // IOK 2025-06-04 Since we are here mapping several Express rates to a single Woo rate,
3688 // we need to add a suffix, which is removed in gw->set_order_shipping_details().
3689 $entry['id'] = $id . ":" . $index;
3690 $entry['name'] = $point['name'];
3691 $options[] = $entry;
3692 }
3693 // 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
3694 // This gets stored in the orders ratemap on return. IOK 2025-11-19
3695 if (!empty($pickup_point_table)) {
3696 $rate->add_meta_data('_vipps_pickupPoints', $pickup_point_table);
3697 }
3698 $m2['options'] = $options;
3699 $m2['type'] = 'PICKUP_POINT';
3700
3701 return [$rate, $m2];
3702 }
3703
3704
3705 // 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
3706 // $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
3707 public function express_group_shipping_methods($methods, &$ratemap, $methodmap, $order) {
3708 if (!$methods) return $methods;
3709 $grouped = [];
3710 $maybe_groupable_methods = $methods;
3711 while (!empty($maybe_groupable_methods)) {
3712 $first = array_shift($maybe_groupable_methods);
3713 $first_id = preg_replace("!:.+$!", "", $first['options'][0]['id']); // strip option index from 'augmented' methods.
3714 $first_rate = $ratemap[$first_id];
3715 $first_method = $methodmap[$first_id];
3716
3717 $rest = [];
3718 foreach ($maybe_groupable_methods as $candidate) {
3719 $candidate_id = preg_replace("!:.+$!", "", $candidate['options'][0]['id']); // strip option index from 'augmented' methods.
3720 $candidate_rate = $ratemap[$candidate_id];
3721 $candidate_method = $methodmap[$candidate_id];
3722
3723 // By default, we will group all rates that are pickup_location-s. LP 2025-08-18
3724 $is_pickup = $first_rate->method_id === $candidate_rate->method_id && $first_rate->method_id === 'pickup_location';
3725 $should_group = apply_filters('woo_vipps_express_should_group_shipping_methods', $is_pickup, $first_rate, $first_method, $candidate_rate, $candidate_method);
3726
3727 if ($should_group) {
3728 $first_options = $first['options'];
3729 $second_options = $candidate['options'];
3730
3731 $first['options'] = array_merge($first_options, $second_options);
3732
3733 // Reset default-ness and priority to the highest value from the merged methods.
3734 if ($candidate['isDefault']) $first['isDefault'] = true;
3735 if ($candidate['priority'] < $first['priority']) $first['priority'] = $candidate['priority'];
3736
3737 } else {
3738 $rest[] = $candidate;
3739 }
3740
3741 }
3742 $grouped[] = $first;
3743
3744 // Start over again with the ones who weren't grouped to the first method of the list. LP 2025-08-18
3745 $maybe_groupable_methods = $rest;
3746 }
3747
3748 return $grouped;
3749 }
3750
3751
3752 // In certain situations the session may have no cart, which among other things makes it impossible for us to calculate shipping.
3753 // We must therefore reconstruct the cart as close to what it were before calculating shipping; and we must delete it afterwards
3754 // 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.
3755 // Returns "true" if cart is reconstructed from the order, else false.
3756 // IOK 2024-04-08
3757 private function maybe_reconstruct_cart($order_id) {
3758 if (!WC()->cart->is_empty()) return false;
3759 $this->log(sprintf(__("No cart, so will try to calculate shipping based on order contents for order %1\$d", 'woo-vipps'), $order_id), 'error');
3760 try {
3761 $order = wc_get_order( $order_id );
3762 $cart = array();
3763 $inital_cart_size = 0;
3764 $order_items = $order->get_items();
3765 foreach ( $order_items as $item ) {
3766 $product_id = (int) $item->get_product_id();
3767 $quantity = $item->get_quantity();
3768 $variation_id = (int) $item->get_variation_id();
3769 $variations = array();
3770 $cart_item_data = array();
3771 $product = $item->get_product();
3772 if ( ! $product ) {
3773 continue;
3774 }
3775 if ( ! $variation_id && $product->is_type( 'variable' ) ) continue;
3776 // We ignore the out-of-stock rule here, it doesn't matter for shipping in this case IOK 2024-04-09
3777 foreach ( $item->get_meta_data() as $meta ) {
3778 if ( taxonomy_is_product_attribute( $meta->key ) || meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) {
3779 $variations[ $meta->key ] = $meta->value;
3780 }
3781 }
3782 $cart_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data );
3783 $product_data = wc_get_product( $variation_id ? $variation_id : $product_id );
3784 $cart[ $cart_id ] = array_merge(
3785 $cart_item_data,
3786 array(
3787 'key' => $cart_id,
3788 'product_id' => $product_id,
3789 'variation_id' => $variation_id,
3790 'variation' => $variations,
3791 'quantity' => $quantity,
3792 'data' => $product_data,
3793 'data_hash' => wc_get_cart_item_data_hash( $product_data ),
3794 )
3795 );
3796
3797 }
3798 WC()->cart->set_cart_contents($cart);
3799 WC()->cart->calculate_totals();
3800 WC()->cart->set_session();
3801 return true;
3802 } catch (Exception $e) {
3803 $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
3804 return false;
3805 }
3806 }
3807
3808
3809 // IOK 2020-02-13 This method implements the *old* style of providing shipping methods to Vipps Express Checkout.
3810 // It is 'stateless' in that it doesn't need to serialize shipping methods or anything like that - but precisely because of this,
3811 // metadata isn't possible to provide, and it reqires to send VAT separately coded into the shipping method ID which is pretty
3812 // clumsy. This method will currently only be used if a merchant has overridden the 'woo_vipps_shipping_methods' filter and hasn't chosen
3813 // the setting that overrides this.
3814 public function legacy_shipping_callback_handler ($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart) {
3815 do_action('woo_vipps_legacy_shipping_methods', $order); // This will probably be mostly for debugging.
3816
3817 // If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20
3818 if (!$acart->needs_shipping()) {
3819 $methods = array(array('isDefault'=>'Y','priority'=>'0','shippingCost'=>'0.00','shippingMethod'=>__('No shipping required','woo-vipps'),'shippingMethodId'=>'Free:Free;0'));
3820 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods);
3821 return $return;
3822 }
3823
3824 $free = 0;
3825 $defaultset = 0;
3826 $methods = array();
3827 foreach ($shipping_methods as $rate) {
3828 $method = array();
3829 $method['priority'] = 0;
3830 $tax = $rate->get_shipping_tax() ?: 0;
3831 $cost = $rate->get_cost() ?: 0;
3832
3833 $method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
3834 $method['shippingMethod'] = $rate->get_label();
3835 // We may not really need the tax stashed here, but just to be sure.
3836 $method['shippingMethodId'] = $rate->get_id() . ";" . $tax;
3837 $methods[]= $method;
3838
3839 // If we qualify for free shipping, make it the default. Thanks to Emely Bakke for reporting. IOK 2019-11-15
3840 if (preg_match("!^free_shipping!",$rate->get_id())) {
3841 $free=1;
3842 $defaultset=1;
3843 $chosen = $rate->get_id();
3844 }
3845 }
3846 usort($methods, function($method1, $method2) {
3847 return $method1['shippingCost'] - $method2['shippingCost'];
3848 });
3849 $priority=0;
3850 foreach($methods as &$method) {
3851 $rateid = explode(";",$method['shippingMethodId'],2);
3852 if (!empty($rateid) && $rateid[0] == $chosen) {
3853 $defaultset=1;
3854 $method['isDefault'] = 'Y';
3855 } else {
3856 $method['isDefault'] = 'N';
3857 }
3858 $method['priority']=$priority;
3859 $priority++;
3860 }
3861 // If we don't have free shipping, select the first (cheapest) option, unless that is 'local pickup'. IOK 2019-11-26
3862 // Or pickup_location, same thing. IOK 2025-05-07
3863 if(!$defaultset && !empty($methods)) {
3864 foreach($methods as &$method) {
3865 if (!preg_match("!^(local_pickup|pickup_location)!",$method['shippingMethodId'])) {
3866 $defaultset=1;
3867 $method['isDefault'] = 'Y';
3868 break;
3869 }
3870 }
3871 }
3872 // Or the first if we stil have no default method.
3873 if (!$defaultset &&!empty($methods)) {
3874 $methods[0]['isDefault'] = 'Y';
3875 }
3876
3877 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods);
3878 $return = apply_filters('woo_vipps_shipping_methods', $return,$order,$acart);
3879
3880 return $return;
3881 }
3882
3883 public static function nocache() {
3884 wc_nocache_headers();
3885 header("X-Accel-Expires: 0");
3886 }
3887
3888
3889
3890 // Handle DELETE on a vipps consent removal callback
3891 public function vipps_consent_removal_callback ($callback) {
3892 Vipps::nocache();
3893 // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3894 // to escape here when the API is changed. IOK 2020-10-14
3895 $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3896 }
3897
3898 public function woocommerce_payment_gateways($methods) {
3899 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
3900 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3901 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
3902 $gateway = $this->gateway();
3903 if ($gateway) {
3904 $methods[] = $gateway;
3905 } else {
3906 $methods[] = 'WC_Gateway_Vipps';
3907 }
3908
3909 $methods[] = 'WC_Gateway_VippsCard';
3910
3911 return $methods;
3912 }
3913
3914 // Runs after set_session, so if the session is just created, we'll get called. IOK 2018-06-06
3915 public function woocommerce_cart_updated() {
3916 $this->maybe_set_vipps_as_default();
3917 }
3918
3919 public function woocommerce_add_to_cart_redirect ($url) {
3920 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
3921 return $url;
3922 }
3923 $url = $this->express_checkout_url();
3924 $url = wp_nonce_url($url,'express','sec');
3925
3926 return $url;
3927 }
3928
3929 // We can't allow a customer to re-call the Vipps Express checkout payment thing twice -
3930 // This would happen if a logged-in user tries to re-start the transaction after breaking it.
3931 // But for express checkout this breaks because there is no shipping method or address, and of course,
3932 // the order id is unique too.. IOK 2018-11-21
3933 public function woocommerce_my_account_my_orders_actions($actions, $order ) {
3934 $pm = $order->get_payment_method();
3935 if (!self::is_vipps_order($pm)) return $actions;
3936
3937 if (!static::order_is_vipps_retryable($order->get_id())) {
3938 unset($actions['pay']);
3939 }
3940 return $actions;
3941 }
3942
3943 // This job runs in the wp-cron context, and is intended to clean up signal files and other temporariy data. IOK 2020-04-01
3944 public function cron_cleanup_hook () {
3945 $this->cleanupCallbackSignals(); // Remove old callback signals (files in uploads)
3946 $this->delete_old_cancelled_orders(); // Remove cancelled express checkout orders if selected
3947 }
3948
3949 // This job runs in the wp-cron context and checks if there are *old* pending orders with payment method Vipps. If so, it will
3950 // check if the status of these orders are now known. This is intended to handle the case where a user does not return
3951 // to the store and the Vipps callback fails for whatever reason. IOK 2021-06-21
3952 public function cron_check_for_missing_callbacks() {
3953 $eightminutesago = time() - (60*8);
3954 $sevendaysago = time() - (60*60*24*7);
3955
3956 // This is compatible with both HPOS and old style order management. IOK 2026-05-27
3957 $pending_app = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
3958 $pending_cards = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps_card', 'date_created' => '>' . $sevendaysago ));
3959 $pending = array_merge($pending_app, $pending_cards);
3960
3961 if (empty($pending)) return;
3962 foreach ($pending as $o) {
3963 $then = $o->get_meta('_vipps_init_timestamp');
3964 if (! $then) continue; # Race condition! We may not have set the timestamp yet. IOK 2022-03-24
3965 if (!$o->get_meta('_vipps_orderid')) continue; # ditto
3966 if ($then > $eightminutesago) continue;
3967
3968 $vippstatus = $o->get_meta('_vipps_status');
3969 $currentstatus = $this->gateway()->interpret_vipps_order_status($vippstatus);
3970 if ($currentstatus != 'initiated') {
3971 $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');
3972 return;
3973 }
3974 $this->check_status_of_pending_order($o, false, false);
3975 }
3976 }
3977
3978 // 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 -
3979 // e.g. wp-cron. IOK 2021-06-21
3980 // Stop restoring session in wp-cron too. IOK 2021-08-23
3981 public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3982 $express = $order->get_meta('_vipps_express_checkout');
3983 $vippstatus = $order->get_meta('_vipps_status');
3984 if ($express && $maybe_restore_session) {
3985 $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3986 $this->callback_restore_session($order->get_id());
3987 }
3988 $gw = $this->gateway();
3989
3990 $order_status = null;
3991 try {
3992 $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()));
3993 $order_status = $gw->callback_check_order_status($order, $allow_retry);
3994 $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');
3995 } catch (Exception $e) {
3996 $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');
3997 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
3998 }
3999 // Ensure we don't keep using an old session for more than one order here.
4000 if ($express && $maybe_restore_session) {
4001 $this->callback_destroy_session();
4002 }
4003 return $order_status;
4004 }
4005
4006 // 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
4007 public static function maybe_add_cron_event() {
4008 if (!wp_next_scheduled('vipps_cron_cleanup_hook')) {
4009 wp_schedule_event(time(), 'hourly', 'vipps_cron_cleanup_hook');
4010 }
4011 if (!wp_next_scheduled('vipps_cron_missing_callback_hook')) {
4012 wp_schedule_event(time(), '5min', 'vipps_cron_missing_callback_hook');
4013 }
4014 }
4015
4016 public function activate () {
4017 static::maybe_add_cron_event();
4018 $gw = $this->gateway();
4019
4020 // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4021 if ($gw->get_option('orderprefix') == 'Woo') {
4022 $gw->update_option('orderprefix', $this->generate_order_prefix());
4023 }
4024 // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4025 $gw->initialize_webhooks();
4026 $this->payment_method_name = $gw->get_option('payment_method_name');
4027 }
4028
4029 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4030 public static function deactivate() {
4031 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4032 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
4033 $timestamp = wp_next_scheduled('vipps_cron_missing_callback_hook');
4034 wp_unschedule_event($timestamp, 'vipps_cron_missing_callback_hook');
4035 // IOK 2023-12-20 Delete all webhooks for this instance
4036 $gw = WC_Gateway_Vipps::instance();
4037 $gw->delete_all_webhooks();
4038
4039 // Delete all settings if checked in settings menu. LP 2025-10-06
4040 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4041 if ($should_delete) {
4042 // Delete options.
4043 $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4044 foreach($options as $option) {
4045 delete_option($option);
4046 }
4047 }
4048
4049 // Run deactivation logic for recurring
4050 if (class_exists('WC_Vipps_Recurring')) {
4051 WC_Vipps_Recurring::get_instance()->deactivate();
4052 }
4053 delete_option('woo_vipps_recurring_payments_activation');
4054
4055 }
4056
4057 /** Try manually setting locale to locale recieved in AcceptLanguage header.
4058 *
4059 * This should fix incorrect language recieved from ajax when using translate plugins like polylang, wpml.
4060 * E.g. for checkout widgets and product names: We send the correct locale to the frontend when first setting up Checkout,
4061 * then we send the locale back in the Accept-Language header to ajax endpoints. LP 2025-12-11
4062 */
4063 public static function set_locale_if_in_header() {
4064 $locales = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
4065
4066 // get first in list, but strip away semicolon and everything after. LP 2025-12-16
4067 $newlocale = trim(preg_replace("!;.*!", "", explode(",", $locales)[0]));
4068 if (empty($newlocale))
4069 return false;
4070 return switch_to_locale($newlocale); // note: this may fail and return a false. LP 2025-12-11
4071 }
4072
4073
4074 public function footer() {
4075 // Nothing yet
4076 }
4077
4078
4079 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4080 private function maybe_set_vipps_as_default() {
4081 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4082 $gw = $this->gateway();
4083 if ($gw->get_option('vippsdefault')=='yes') {
4084 WC()->session->set('chosen_payment_method', $gw->id);
4085 }
4086 }
4087
4088 // Check order status in the database, and if it is pending for a long time, directly at Vipps
4089 // IOK 2018-05-04
4090 public function check_order_status($order) {
4091 if (!$order) return null;
4092 clean_post_cache($order->get_id()); // Get a fresh copy
4093 $order = wc_get_order($order->get_id());
4094 $order_status = $order->get_status();
4095
4096 if ($order_status != 'pending') return $order_status;
4097 // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4098 // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4099 if ($order_status == 'pending') {
4100 if (WC()->session) {
4101 $now = time();
4102 $then = WC()->session->get('_vipps_check_' . $order->get_id());
4103 if (!$then) {
4104 $then = $now;
4105 WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4106 }
4107 if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4108 return $order_status;
4109 }
4110 } else {
4111 // No session shouldn't be possible, but if it is..
4112 return $order_status;
4113 }
4114 }
4115 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4116 return $this->check_status_of_pending_order($order);
4117 }
4118
4119 // In some situations we have to empty the cart when the user goes to Vipps, so
4120 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
4121 // Try to avoid this now 2018-12-10 - only do it for single-product checkouts. IOK 2018-10-12
4122 // Changed to use a serialized cart, which should be more compatible with subclassed carts and cart metadata.
4123 // Serialization errors are not yet handled - they can't be fixed but they could be signalled. IOK 2020-04-07
4124 public function save_cart($order,$cart_to_save) {
4125 $carts = WC()->session->get('_vipps_carts');
4126 if (!$carts) $carts = array();
4127 $serialized = base64_encode(@serialize($cart_to_save->get_cart_contents()));
4128 $carts[$order->get_id()] = $serialized;
4129 WC()->session->set('_vipps_carts',$carts);
4130 do_action('woo_vipps_cart_saved');
4131 }
4132 public function restore_cart($order) {
4133 global $woocommerce;
4134 $carts = $woocommerce->session->get('_vipps_carts');
4135 if (empty($carts)) return;
4136 $cart = null;
4137 $cartdata = @$carts[$order->get_id()];
4138 if ($cartdata) {
4139 $cart = @unserialize(@base64_decode($cartdata));
4140 }
4141 do_action('woo_vipps_restoring_cart',$order,$cart);
4142 unset($carts[$order->get_id()]);
4143 $woocommerce->session->set('_vipps_carts',$carts);
4144 // It will absolutely not work to just use set_cart_contents, because this will not
4145 // correctly initialize this 'new' cart. So we *have* to use add_to_cart at least once. IOK 2020-04-07
4146 if (!empty($cart)) {
4147 foreach ($cart as $cart_item_key => $values) {
4148 $id =$values['product_id'];
4149 $quant=$values['quantity'];
4150 $varid = @$values['variation_id'];
4151 $variation = @$values['variation'];
4152 // .. and there may be any number of other attributes, which we need to pass on.
4153 $cart_item_data = array();
4154 foreach($values as $key=>$value) {
4155 if (in_array($key,array('product_id','quantity','variation_id','variation'))) continue;
4156 $cart_item_data[$key] = $value;
4157 }
4158 $woocommerce->cart->add_to_cart($id,$quant,$varid,$variation,$cart_item_data);
4159 }
4160 }
4161 do_action('woo_vipps_cart_restored');
4162 }
4163
4164 // Should only be run when this is an order in our own session,
4165 // used in the ajax_check_order_status and vipps_payment methods, where we are
4166 // expecting a customer return that may be from Express Checkout. If it is, we may
4167 // have no customer email in the current session, which in 7.8.2 will stop the user from
4168 // viewing his or her orders. IOK 2023-07-17
4169 function maybe_set_session_customer_email($order) {
4170 if ($order->get_meta('_vipps_express_checkout')) {
4171 $email = $order->get_billing_email();
4172 if ($email && WC()->customer) {
4173 WC()->customer->set_email($email);
4174 WC()->customer->set_billing_email($email);
4175 WC()->customer->save();
4176 WC()->session->set('tstamp', time()); // Just to ensure it is 'dirty'
4177 } else {
4178 $this->log(__("Could not get user email from order before thankyou-page", 'woo-vipps'));
4179 }
4180 }
4181 }
4182
4183 // Maybe log in user
4184 // It is done on the thank-you page of the order, and only for express checkout.
4185 function maybe_log_in_user ($order) {
4186 if (is_user_logged_in()) return;
4187 if (!$order || ! self::is_vipps_order($order)) return;
4188
4189 // We *do* want to log in express checkout customers, but not those that
4190 // use the Vipps Checkout solution - those can change their emails in the
4191 // checkout screen. IOK 2021-09-03
4192 $do_login = $order->get_meta('_vipps_express_checkout');
4193
4194 // We will not log in Vipps Checkout users unless the option for that is true
4195 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4196 $do_login = false;
4197 }
4198
4199
4200 // Make this filterable because you may want to only log on some users
4201 $do_login = apply_filters('woo_vipps_login_user_on_express_checkout', $do_login, $order);
4202 if (!$do_login) return;
4203
4204 $customer = $this->express_checkout_get_vipps_customer ($order);
4205 if( $customer) {
4206 $usermeta=get_userdata($customer->get_id());
4207 $iscustomer = (in_array('customer', $usermeta->roles) || in_array('subscriber', $usermeta->roles));
4208 // Ensure we don't have any admins with an additonal customer role logged in like this
4209 if($iscustomer && !user_can($customer->get_id(), 'manage_woocommerce') && !user_can($customer->get_id(),'manage_options')) {
4210 do_action('express_checkout_before_customer_login', $customer, $order);
4211
4212 $user = new WP_User( $customer->get_id());
4213 wp_set_current_user($customer->get_id(), $user->user_login);
4214
4215 wp_set_auth_cookie($customer->get_id());
4216 do_action('wp_login', $user->user_login, $user);
4217 }
4218 }
4219 }
4220
4221 // Get the customer that corresponds to the current order, maybe creating the customer if it does not exist yet and
4222 // the settings allow it.
4223 function express_checkout_get_vipps_customer($order) {
4224 if (!$order || ! self::is_vipps_order($order)) return null;
4225 // specific code for this by netthandelsgruppen if the below function exists
4226 if (function_exists('create_assign_user_on_vipps_callback')) return null;
4227
4228 // Both Checkout and Express Checkout have the below value set to true
4229 if (!$order->get_meta('_vipps_express_checkout')) return;
4230
4231 // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4232 // IOK 2023-07-27
4233 $ischeckout = $order->get_meta('_vipps_checkout');
4234 if ($ischeckout) {
4235 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
4236 } else {
4237 if ($this->gateway()->get_option('expresscreateuser') != 'yes') return null;
4238 }
4239
4240 if (is_user_logged_in()) return new WC_Customer(get_current_user_id());
4241 if ($order->get_user_id()) return new WC_Customer($order->get_user_id());
4242
4243 $email = $order->get_billing_email();
4244
4245 // Existing customer, so update the order (and possibly the site if multisite) and return the customer. IOK 2020-10-09
4246 if (email_exists($email)) {
4247 $user = get_user_by( 'email', $email);
4248 if (!$user) return null;
4249 $customerid = $user->ID;
4250 $order->set_customer_id( $user->ID );
4251 $order->save();
4252
4253 if (is_multisite() && ! is_user_member_of_blog($customerid, get_current_blog_id())) {
4254 add_user_to_blog( get_current_blog_id(), $customerid, 'customer' );
4255 }
4256 $customer = new WC_Customer($customerid);
4257 return $customer;
4258 }
4259
4260 // Previously this got the user data from Vipps here as a third argument; this is no longer available after refactoring.
4261 $user = [];
4262 $maybecreateuser = apply_filters('woo_vipps_create_user_on_express_checkout', true, $order, $user);
4263 if (! $maybecreateuser) return;
4264
4265 // No customer yet. As we want to create users like this (set in the settings) let's do so.
4266 // Username will be created from email, but the settings may stop generating passwords, so we force that to be generated. IOK 2020-10-09
4267 $firstname = $order->get_billing_first_name();
4268 $lastname = $order->get_billing_last_name();
4269 $name = $firstname;
4270 $userdata = array('user_nicename'=>$name, 'display_name'=>"$firstname $lastname", 'nickname'=>$firstname, 'first_name'=>$firstname, 'last_name'=>$lastname);
4271
4272 // Add filter to allow other ways of creating usernames.
4273 $newusername = apply_filters('woo_vipps_express_checkout_new_username', '', $email, $userdata, $order);
4274
4275 $customerid = wc_create_new_customer($email, $newusername, wp_generate_password(), $userdata);
4276 if ($customerid && !is_wp_error($customerid)) {
4277 $order->set_customer_id( $customerid );
4278 $order->save();
4279
4280 // Ensure the standard WP user fields are set too IOK 2020-11-03
4281 wp_update_user(array('ID' => $customerid, 'first_name' => $firstname, 'last_name' => $lastname, 'display_name' => "$firstname $lastname", 'nickname' => $firstname));
4282
4283 update_user_meta( $customerid, 'billing_address_1', $order->get_billing_address_1() );
4284 update_user_meta( $customerid, 'billing_address_2', $order->get_billing_address_2() );
4285 update_user_meta( $customerid, 'billing_city', $order->get_billing_city() );
4286 update_user_meta( $customerid, 'billing_company', $order->get_billing_company() );
4287 update_user_meta( $customerid, 'billing_country', $order->get_billing_country() );
4288 update_user_meta( $customerid, 'billing_email', $order->get_billing_email() );
4289 update_user_meta( $customerid, 'billing_first_name', $order->get_billing_first_name() );
4290 update_user_meta( $customerid, 'billing_last_name', $order->get_billing_last_name() );
4291 update_user_meta( $customerid, 'billing_phone', $order->get_billing_phone() );
4292 update_user_meta( $customerid, 'billing_postcode', $order->get_billing_postcode() );
4293 update_user_meta( $customerid, 'billing_state', $order->get_billing_state() );
4294 update_user_meta( $customerid, 'shipping_address_1', $order->get_shipping_address_1() );
4295 update_user_meta( $customerid, 'shipping_address_2', $order->get_shipping_address_2() );
4296 update_user_meta( $customerid, 'shipping_city', $order->get_shipping_city() );
4297 update_user_meta( $customerid, 'shipping_company', $order->get_shipping_company() );
4298 update_user_meta( $customerid, 'shipping_country', $order->get_shipping_country() );
4299 update_user_meta( $customerid, 'shipping_first_name', $order->get_shipping_first_name() );
4300 update_user_meta( $customerid, 'shipping_last_name', $order->get_shipping_last_name() );
4301 update_user_meta( $customerid, 'shipping_method', $order->get_shipping_method() );
4302 update_user_meta( $customerid, 'shipping_postcode', $order->get_shipping_postcode() );
4303 update_user_meta( $customerid, 'shipping_state', $order->get_shipping_state() );
4304
4305 // Integration with All-in-one WP security - these accounts are created by validated accounts in the app.
4306 update_user_meta( $customerid,'aiowps_account_status', 'approved');
4307
4308 $customer = new WC_Customer($customerid);
4309 do_action('woo_vipps_express_checkout_new_customer', $customer, $order->get_id());
4310
4311 return $customer;
4312 }
4313 if (is_wp_error($customerid)) {
4314 $this->log(__("Error creating customer in express checkout: ", 'woo-vipps') . $customerid->get_error_message());
4315 } else {
4316 $this->log(__("Unknown error customer in express checkout.", 'woo-vipps'));
4317 }
4318 return null;
4319 }
4320
4321 // This restores the cart on order complete, but only if the current order was a single product buy with an active cart.
4322 public function maybe_restore_cart($orderid,$failed=false) {
4323 if (!$orderid) return;
4324 $o = null;
4325 try {
4326 $o = wc_get_order($orderid);
4327 } catch (Exception $e) {
4328 // Well, we tried.
4329 }
4330 if (!$o) return;
4331 if (!$o->get_meta('_vipps_single_product_express')) return;
4332 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4333 if ($failed) WC()->cart->empty_cart();
4334 $this->restore_cart($o);
4335 }
4336
4337
4338 public function ajax_vipps_buy_single_product () {
4339 Vipps::nocache();
4340 static::set_locale_if_in_header();
4341 // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4342 // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4343 $session = WC()->session;
4344 if (!$session->has_session()) {
4345 $session->set_customer_session_cookie(true);
4346 }
4347 $session->set('__vipps_buy_product', json_encode($_REQUEST));
4348
4349 // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4350 // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4351 $url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url());
4352
4353 $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4354 wp_send_json($result);
4355 exit();
4356 }
4357
4358 public function ajax_do_express_checkout () {
4359 check_ajax_referer('do_express','sec');
4360 Vipps::nocache();
4361 static::set_locale_if_in_header();
4362 $gw = $this->gateway();
4363
4364 if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4365 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4366 wp_send_json($result);
4367 exit();
4368 }
4369
4370
4371
4372
4373 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4374 $toolate = false;
4375 $msg = "";
4376 $valid = WC()->cart->check_cart_item_validity();
4377 if ( is_wp_error( $valid) ) {
4378 $toolate = true;
4379 $msg = "<br>" . $valid->get_error_message();
4380 }
4381 $stock = WC()->cart->check_cart_item_stock();
4382 if ( is_wp_error( $stock) ) {
4383 $toolate = true;
4384 $msg = "<br>" . $stock->get_error_message();
4385 }
4386
4387 if ($toolate) {
4388 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4389 wp_send_json($result);
4390 exit();
4391 }
4392
4393 try {
4394 $orderid = $gw->create_partial_order();
4395 do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4396 } catch (Exception $e) {
4397 $this->log($e->getMessage(),'error');
4398 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4399 wp_send_json($result);
4400 exit();
4401 }
4402 if (!$orderid) {
4403 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4404 wp_send_json($result);
4405 exit();
4406 }
4407
4408 try {
4409 $this->maybe_add_static_shipping($gw,$orderid);
4410 } catch (Exception $e) {
4411 $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4412 $this->log($e->getMessage(),'error');
4413 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4414 wp_send_json($result);
4415 exit();
4416 }
4417
4418 $ok = $gw->process_payment($orderid);
4419 if ($ok && $ok['result'] == 'success') {
4420 $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4421 wp_send_json($result);
4422 exit();
4423 }
4424 $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4425 wp_send_json($result);
4426 exit();
4427 }
4428
4429 // Same as ajax_do_express_checkout, but for a single product/variation. Duplicate code because we want to manipulate the cart differently here. IOK 2018-09-25
4430 public function ajax_do_single_product_express_checkout() {
4431 check_ajax_referer('do_express','sec');
4432 Vipps::nocache();
4433 static::set_locale_if_in_header();
4434 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4435 $gw = $this->gateway();
4436
4437 if (!$gw->express_checkout_available()) {
4438 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4439 wp_send_json($result);
4440 exit();
4441 }
4442
4443
4444 // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4445 // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4446 $varid = intval(@$_POST['variation_id']);
4447 $prodid = intval(@$_POST['product_id']);
4448 $sku = sanitize_text_field(@$_POST['sku']);
4449 $quant = intval(@$_POST['quantity']);
4450
4451 // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4452 $variations = array();
4453 foreach ($_POST as $key => $value ) {
4454 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4455 continue;
4456 }
4457 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4458 }
4459
4460 $product = null;
4461 $variant = null;
4462 $parent = null;
4463 $parentid = null;
4464 $quantity = 1;
4465 if ($quant && $quant>1) $quantity=$quant;
4466
4467 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4468 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
4469 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4470 try {
4471 if ($varid) {
4472 $product = wc_get_product($varid);
4473 } elseif ($prodid) {
4474 $product = wc_get_product($prodid);
4475 } elseif ($sku) {
4476 $skuid = wc_get_product_id_by_sku($sku);
4477 $product = wc_get_product($skuid);
4478 }
4479 } catch (Exception $e) {
4480 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4481 wp_send_json($result);
4482 exit();
4483 }
4484
4485
4486 if (!$product) {
4487 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4488 wp_send_json($result);
4489 exit();
4490 }
4491
4492 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4493 $parent = $parentid ? wc_get_product($parentid) : null;
4494
4495 // This can't really happen, but if it did..
4496 if ($prodid && $parentid && ($prodid != $parentid)) {
4497 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4498 wp_send_json($result);
4499 exit();
4500 }
4501 if (!$gw->product_supports_express_checkout($product)) {
4502 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4503 wp_send_json($result);
4504 exit();
4505 }
4506
4507 // Somebody addded the wrong SKU
4508 if ($product->get_type() == 'variable'){
4509 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4510 wp_send_json($result);
4511 exit();
4512 }
4513 // Final check of availability
4514 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4515 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4516 wp_send_json($result);
4517 exit();
4518 }
4519
4520 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4521
4522 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4523 // because some plugins actually override this.
4524 $current_cart = clone WC()->cart;
4525 WC()->cart->empty_cart();
4526
4527 if ($parent && $parent->get_type() == 'variable') {
4528 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4529 } else {
4530 WC()->cart->add_to_cart($product->get_id(),$quantity);
4531 }
4532
4533 try {
4534 $orderid = $gw->create_partial_order();
4535 do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4536 } catch (Exception $e) {
4537 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4538 wp_send_json($result);
4539 exit();
4540 }
4541
4542 if (!$orderid) {
4543 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4544 wp_send_json($result);
4545 exit();
4546 }
4547
4548 try {
4549 $this->maybe_add_static_shipping($gw,$orderid);
4550 } catch (Exception $e) {
4551 $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4552 $this->log($e->getMessage(),'error');
4553 $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4554 wp_send_json($result);
4555 exit();
4556 }
4557
4558
4559 // Single product purchase, so save any contents of the real cart
4560 $order = wc_get_order($orderid);
4561 $order->update_meta_data('_vipps_single_product_express',true);
4562 $order->save();
4563 $this->save_cart($order,$current_cart);
4564
4565 $ok = $gw->process_payment($orderid);
4566 if ($ok && $ok['result'] == 'success') {
4567 $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4568 wp_send_json($result);
4569 exit();
4570 }
4571 $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4572 wp_send_json($result);
4573 exit();
4574 }
4575
4576 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4577 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
4578 public function maybe_add_static_shipping($gw, $orderid, $ischeckout=false) {
4579 $key = $ischeckout ? 'enablestaticshipping_checkout' : 'enablestaticshipping';
4580 $ok = $gw->get_option($key) == 'yes';
4581 $ok = apply_filters('woo_vipps_enable_static_shipping', $ok, $orderid);
4582 if ($ok) {
4583 return $this->add_static_shipping($gw, $orderid, $ischeckout);
4584 }
4585 }
4586
4587 // And this function adds static shipping no matter what. It may need to be used in plugins, hence visible. IOK 2021-10-22
4588 public function add_static_shipping ($gw, $orderid, $ischeckout=false) {
4589 $order = wc_get_order($orderid);
4590 $prefix = $gw->get_orderprefix();
4591 $vippsorderid = apply_filters('woo_vipps_orderid', $prefix.$orderid, $prefix, $order);
4592 $addressinfo = $this->get_static_shipping_address_data();
4593
4594 // Both Checkout and new Express Checkout supports LocalPickup, so add it (it is normally only present for Gutenberg checkout)
4595 // Add special shipping methods (LocalPickup etc);
4596 $this->load_extra_shipping_methods($order, $addressinfo, $ischeckout);
4597
4598 $options = $this->vipps_shipping_details_callback_handler($order, $addressinfo,$vippsorderid, $ischeckout);
4599
4600 if ($options) {
4601 $order->update_meta_data('_vipps_static_shipping', $options);
4602 $order->save();
4603 }
4604 }
4605
4606 // Support local pickup. This is normally only registered when the Gutenberg Checkout block is either on the
4607 // 'checkout-page' or in some template; but that's not nececssarily the case if Vipps MobilePay checkout is active.
4608 // Supported also in express checkout. 2026-02-25
4609 // We'll add this if admin has stored *any* pickup locations at any point. IOK 2026-02-25
4610 // Afterwards, we need to post-process this, because *each* location gets a different rate. See the VippsCheckout class.
4611 function maybe_load_pickup_locations () {
4612 $locations = get_option('pickup_location_pickup_locations', array());
4613 if (!empty($locations) && class_exists('Automattic\WooCommerce\Blocks\Shipping\PickupLocation')) {
4614 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4615 }
4616 }
4617
4618 // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4619 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4620 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4621 // If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06
4622 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
4623 // Previously we loaded PickupLocations here; we now do that if any are defined at all. The old custom filter still runs though,
4624 // and last. IOK 2026-02-25
4625 do_action('woo_vipps_express_load_shipping_methods', $order, $addressdata);
4626 }, 99);
4627 }
4628
4629
4630 // Check the status of the order if it is a part of our session, and return a result to the handler function IOK 2018-05-04
4631 public function ajax_check_order_status () {
4632 check_ajax_referer('vippsstatus','sec');
4633 static::set_locale_if_in_header();
4634 Vipps::nocache();
4635
4636 $orderid= wc_get_order_id_by_order_key(sanitize_text_field(@$_POST['key']));
4637 $transaction = sanitize_text_field(@$_POST['transaction']);
4638
4639 $sessionorders= WC()->session->get('_vipps_session_orders');
4640 if (!isset($sessionorders[$orderid])) {
4641 wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
4642 }
4643
4644 $order = wc_get_order($orderid);
4645 if (!$order) {
4646 wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
4647 }
4648 $order_status = $this->check_order_status($order);
4649 // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4650 if ($order_status == 'pending') {
4651 wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps')));
4652 return false;
4653 }
4654 if ($order_status == 'cancelled' || $order_status == 'failed') {
4655 $this->maybe_restore_cart($orderid,'failed');
4656 wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps'), 'order_status' => $order_status));
4657 return false;
4658 }
4659
4660 // Order status isn't pending anymore, but there can be custom statuses, so check the payment status instead.
4661 $order = wc_get_order($orderid); // Reload
4662 $gw = $this->gateway();
4663 $payment = $gw->check_payment_status($order);
4664 if ($payment == 'initiated') {
4665 wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps')));
4666 return false;
4667 }
4668
4669
4670 if ($payment == 'authorized') {
4671 // IOK Previously handled in the thankyou hook 2023-07-17
4672 $this->woocommerce_before_thankyou($order->get_id());
4673 wp_send_json(array('status'=>'ok', 'msg'=>__('Payment authorized', 'woo-vipps')));
4674 return false;
4675 }
4676 if ($payment == 'complete') {
4677 // IOK Previously handled in the thankyou hook 2023-07-17
4678 $this->woocommerce_before_thankyou($order->get_id());
4679 wp_send_json(array('status'=>'ok', 'msg'=>__('Payment captured', 'woo-vipps')));
4680 return false;
4681 }
4682 if ($payment == 'cancelled') {
4683 $this->maybe_restore_cart($orderid,'failed');
4684 wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps')));
4685 return false;
4686 }
4687 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4688 return false;
4689 }
4690
4691 // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4692 // These are for the "fallback URL" mostly. IOK 2018-05-18
4693 private function make_vipps_url($what) {
4694 if ( !get_option('permalink_structure')) {
4695 return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4696 }
4697 return trailingslashit(home_url($what, 'https'));
4698 }
4699 public function payment_return_url() {
4700 return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
4701 }
4702 public function express_checkout_url() {
4703 return $this->make_vipps_url('vipps-express-checkout');
4704 }
4705 public function buy_product_url() {
4706 return $this->make_vipps_url('vipps-buy-product');
4707 }
4708
4709 // Return the method in the Vipps
4710 public function is_special_page() {
4711 $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4712 $method = null;
4713 if ( get_option('permalink_structure')) {
4714 foreach($specials as $special=>$specialmethod) {
4715 // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4716 $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4717 if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4718 $method = $specialmethod; break;
4719 }
4720 }
4721 } else {
4722 if (isset($_GET['VippsSpecialPage'])) {
4723 $method = @$specials[$_GET['VippsSpecialPage']];
4724 }
4725 }
4726 return $method;
4727 }
4728
4729 // Just create a spinner and a overlay.
4730 public function spinner () {
4731 $flavour = sanitize_title($this->get_payment_method_name());
4732 ob_start();
4733 ?>
4734 <div class="vippsoverlay">
4735 <div id="floatingCirclesG" class="vippsspinner <?php echo esc_attr($flavour); ?>">
4736 <div class="f_circleG" id="frotateG_01"></div>
4737 <div class="f_circleG" id="frotateG_02"></div>
4738 <div class="f_circleG" id="frotateG_03"></div>
4739 <div class="f_circleG" id="frotateG_04"></div>
4740 <div class="f_circleG" id="frotateG_05"></div>
4741 <div class="f_circleG" id="frotateG_06"></div>
4742 <div class="f_circleG" id="frotateG_07"></div>
4743 <div class="f_circleG" id="frotateG_08"></div>
4744 </div>
4745 </div>
4746 <?php
4747 return apply_filters('woo_vipps_spinner', ob_get_clean());
4748 }
4749
4750
4751 // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4752 // Fallbacks to defaults for each payment method. LP 2025-12-15
4753 public function get_express_logo($payment_method, $lang, $variant) {
4754 $base = plugins_url('img', __FILE__);
4755
4756 // A much more concise approach could be to name the variant files directly and do a oneliner, but harder to grep after the files' usage. LP 2025-12-12
4757 $img_map = [
4758 "vipps" => [
4759 "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4760 "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4761 "en" => [
4762 "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4763 "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4764 "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4765 "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4766 "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4767 "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4768 "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4769 "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4770
4771 ],
4772 "no" => [
4773 "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4774 "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4775 "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4776 "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4777 "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4778 "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4779 "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4780 "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4781 ],
4782 "se" => [
4783 "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4784 "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4785 "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4786 "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4787 "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4788 "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4789 "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4790 "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4791
4792 ],
4793 ],
4794 "mobilepay" => [
4795 "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4796 "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4797 "en" => [
4798 "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4799 "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4800 "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4801 "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4802 "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4803 "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4804 "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4805 "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4806
4807 ],
4808 "dk" => [
4809 "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4810 "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4811 "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4812 "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4813 "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4814 "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4815 "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4816 "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4817 ],
4818 "fi" => [
4819 "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4820 "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4821 "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4822 "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4823 "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4824 "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4825 "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4826 "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4827
4828 ],
4829 ],
4830
4831 ];
4832
4833 $payment = strtolower($payment_method);
4834 if ($lang === 'store') $lang = $this->get_customer_language();
4835
4836 // Dont give a default if payment method not found. LP 2025-12-12
4837 if (!array_key_exists($payment, $img_map)) {
4838 return null;
4839 }
4840 $payment_map = $img_map[$payment];
4841
4842 $img = null;
4843 if (array_key_exists($lang, $payment_map)
4844 && is_array($payment_map[$lang])
4845 && array_key_exists($variant, $payment_map[$lang])) {
4846 $img = @$payment_map[$lang][$variant];
4847 }
4848
4849 // Default fallback behaviour
4850 if (!$img) {
4851 $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4852
4853 // First try getting default for payment method + language. LP 2026-01-16
4854 if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4855 /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4856 $this->log(sprintf(__('Could not find chosen express logo for payment method %1$s, language %2$s, and variant %3$s, attempting to fall back on language and payment method, else only language.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4857 $img = @$payment_map[$lang][$default];
4858 }
4859
4860 // If not found, then try global default for payment method. LP 2026-01-16
4861 if (!$img) {
4862 $img = @$payment_map[$default];
4863 }
4864
4865 // Found no logo at all, log this. LP 2026-01-16
4866 if (!$img) {
4867 /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4868 $this->log(sprintf(__('Found no express logo fallback for payment method %1$s, language %2$s, and variant %3$s.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4869 }
4870 }
4871 return $img;
4872 }
4873
4874 // Get payment logo based on payment method, then language NT 2023-11-30
4875 // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4876 public function get_payment_logo($page = null) {
4877 $lang = $this->get_customer_language();
4878 $payment_method = $this->get_payment_method_name();
4879 $variant = $this->get_express_logo_page_variant($page);
4880 $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4881 return $logo_url;
4882 }
4883
4884 /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4885 private function get_express_logo_page_variant($page = null) {
4886 $options = get_option('vipps_button_options');
4887
4888 // Init defaults, use mini version by default in below pages. LP 2025-12-17
4889 $use_mini = in_array($page, ['catalog']);
4890 $variant = "";
4891
4892 // Find correct variant from button settings. LP 2025-12-17
4893 if (is_array($options) && array_key_exists('express', $options)) {
4894 if (array_key_exists($page, $options['express']['force-mini'])) {
4895 $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4896 }
4897 $key = $use_mini ? 'mini-variant' : 'variant';
4898 $variant = sanitize_title($options['express'][$key]) ?? '';
4899 }
4900
4901 if (!$variant) {
4902 $variant = $use_mini ? "default-mini" : "default";
4903 }
4904 return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4905 }
4906
4907 // Get express banner logo based on payment method. LP 2025-09-03
4908 private function get_express_banner_logo() {
4909 $payment_method = $this->get_payment_method_name();
4910
4911 if($payment_method === "Vipps"){
4912 return plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
4913 } else if($payment_method === "MobilePay"){
4914 return plugins_url('img/mobilepay-white.svg',__FILE__);
4915 }
4916 return null;
4917 }
4918
4919 // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4920 public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
4921 $disabled = $disabled ? 'disabled' : '';
4922 $data = array();
4923
4924 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
4925 if ($product_id && !$variation_id) {
4926 $product = wc_get_product($product_id);
4927 if ($product && is_a($product, 'WC_Product_Variation')) {
4928 $variation_id = $product_id;
4929 $product_id = $product->get_parent_id();
4930 }
4931 }
4932
4933 if ($sku) $data['product_sku'] = $sku;
4934 if ($product_id) $data['product_id'] = $product_id;
4935 if ($variation_id) $data['variation_id'] = $variation_id;
4936
4937
4938 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4939 foreach($data as $key=>$value) {
4940 $value = esc_attr($value);
4941 $buttoncode .= " data-$key='$value' ";
4942 }
4943
4944 $payment_method = $this->get_payment_method_name();
4945 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4946 $short = str_ends_with($logo_variant, 'mini');
4947 $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4948
4949 $message =" <img border=0 src='$logo' alt='$payment_method'/>";
4950
4951 # Extra classes, if passed IOK 2019-02-26
4952 if (is_array($classes)) {
4953 $classes = join(" ", $classes);
4954 }
4955 if ($classes) $classes = " $classes";
4956 if ($short) $classes = "short $classes";
4957
4958 $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
4959 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4960 }
4961
4962 // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4963 public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4964 $logo_lang = $this->get_customer_language();
4965 $logo_variant = $this->get_express_logo_page_variant($page);
4966 return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4967 }
4968
4969 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4970 public function single_product_buy_now_button () {
4971 $gw = $this->gateway();
4972 $how = $gw->get_option('singleproductexpress');
4973 if ($how == 'none') return;
4974 if (!$gw->express_checkout_available()) return;
4975
4976 global $product;
4977 $prodid = $product->get_id();
4978 if (!$gw->product_supports_express_checkout($product)) return;
4979
4980 // Vipps does not support 0,- products, so we need to check.
4981 // get_price() should normally return the lowest price for variable products, but that can fail,
4982 // so we dispatch on the type and use the *minimum* price instead, requiring that to be nonzero. IOK 2022-06-08
4983 $showit = true;
4984 if (is_a($product, 'WC_Product_Variable')) {
4985 $minprice = $product->get_variation_price('min', 0);
4986 if ($minprice > 0) $showit = true;
4987 } else {
4988 if ($product->get_price() <= 0) $showit = false;
4989 }
4990
4991 if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false;
4992 $showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product);
4993 if (!$showit) return;
4994
4995 $classes = array();
4996 $disabled="";
4997 if ($product->is_type('variable')) {
4998 $disabled="disabled";
4999 $classes[] = 'variable-product';
5000 }
5001
5002 # If true, add a class that signals that the button should be added in 'compat mode', which is compatible with
5003 # more plugins because it does not handle tha product add itself. IOK 2019-02-26
5004 $compat = ($gw->get_option('singleproductbuynowcompatmode') == 'yes');
5005 $compat = apply_filters('woo_vipps_single_product_compat_mode', $compat, $product);
5006
5007 if ($compat) $classes[] ='compat-mode';
5008 $classes = apply_filters('woo_vipps_single_product_buy_now_classes', $classes, $product);
5009
5010 $button = $this->get_buy_now_button(false,false,false, ($product->is_type('variable') ? 'disabled' : false), $classes, 'product');
5011 $code = "<div class='vipps_buy_now_wrapper noloop'>$button</div>";
5012 echo $code;
5013 }
5014
5015
5016 // True for products that are purchasable using Vipps Express Checkout
5017 public function loop_single_product_is_express_checkout_purchasable($product) {
5018 if (!$product) return false;
5019 if (!$product->is_purchasable() || !$product->is_in_stock() || !$product->supports( 'ajax_add_to_cart' )) return false;
5020 $gw = $this->gateway();
5021
5022 if (!$gw->express_checkout_available()) return false;
5023 if (!$gw->product_supports_express_checkout($product)) return false;
5024 if ($gw->get_option('singleproductexpressarchives') != 'yes') return false;
5025
5026 $how = $gw->get_option('singleproductexpress');
5027 if ($how == 'none') return false;
5028 $prodid = $product->get_id();
5029
5030 $showit = true;
5031 if ($product->get_price() <= 0) $showit = false;
5032 if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false;
5033 $showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product);
5034 $showit = apply_filters('woo_vipps_show_single_product_buy_now_in_loop', $showit, $product);
5035 return $showit;
5036 }
5037
5038 // Print a "buy now with vipps" for products in the loop, like on a category page
5039 public function loop_single_product_buy_now_button() {
5040 global $product;
5041
5042 if (!$this->loop_single_product_is_express_checkout_purchasable($product)) return;
5043
5044 $sku = $product->get_sku();
5045 $button = $this->get_buy_now_button($product->get_id(),false,$sku, false, '', 'catalog');
5046 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5047 }
5048
5049
5050
5051 // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5052 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5053 public function woocommerce_create_pages ($data) {
5054 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5055 if (!$vipps_checkout_activated) return $data;
5056
5057 $data['vipps_checkout'] = array(
5058 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5059 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5060 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5061 );
5062
5063 return $data;
5064 }
5065
5066 // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5067 public function maybe_create_vipps_pages () {
5068 $checkoutid = wc_get_page_id('vipps_checkout');
5069 $makeit = !$checkoutid || ! get_post_status($checkoutid);
5070 if ($makeit) {
5071 delete_option('woocommerce_vipps_checkout_page_id');
5072 }
5073
5074 if ($makeit) {
5075 WC_Install::create_pages();
5076 }
5077 }
5078
5079
5080 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5081 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5082 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5083 public function vipps_buy_product() {
5084 status_header(200,'OK');
5085 Vipps::nocache();
5086
5087 add_filter('body_class', function ($classes) {
5088 $classes[] = 'vipps-express-checkout';
5089 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5090 return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5091 });
5092
5093 do_action('woo_vipps_express_checkout_page');
5094
5095 $session = WC()->session;
5096 $posted = $session->get('__vipps_buy_product');
5097 $session->set('__vipps_buy_product', false); // Reloads won't work but that's ok.
5098
5099
5100 if (!$posted) {
5101 // Find product/variation using an external shareable link
5102 if (array_key_exists('pr',$_REQUEST)) {
5103 global $wpdb;
5104 $externalkey = sanitize_text_field($_REQUEST['pr']);
5105 $search = '_vipps_shareable_link_'.esc_sql($externalkey);
5106 $existing = $wpdb->get_row("SELECT post_id from {$wpdb->prefix}postmeta where meta_key='$search' limit 1",'ARRAY_A');
5107 if (!empty($existing)) {
5108 $posted = get_post_meta($existing['post_id'], $search, true);
5109 }
5110 }
5111 }
5112
5113 $productinfo = false;
5114 if (is_array($posted)) {
5115 $productinfo = $posted;
5116 } else {
5117 $productinfo = $posted ? @json_decode($posted,true) : false;
5118 }
5119
5120 if (!$productinfo) {
5121 $title = __("Product is no longer available",'woo-vipps');
5122 $content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps');
5123 return $this->fakepage($title,$content);
5124 }
5125
5126 // Pass the productinfo to the express checkout form
5127 $args = array();
5128 $args['quantity'] = 1;
5129 if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5130 if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5131 if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5132 if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']);
5133
5134 // For variable products where some of the attributes are "any", we need to add these as well. This is from woos form-handler for these.
5135 foreach ($productinfo as $key => $value) {
5136 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5137 continue;
5138 }
5139 $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5140 }
5141
5142 $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5143 }
5144
5145 // This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts.
5146 public function vipps_express_checkout() {
5147 status_header(200,'OK');
5148 Vipps::nocache();
5149 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5150 // IOK 2018-05-28
5151 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5152
5153
5154 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5155 if (!$backurl) $backurl = home_url();
5156
5157 if (!$ok) {
5158 wc_add_notice(__('Link expired, please try again', 'woo-vipps'));
5159 wp_redirect($backurl);
5160 exit();
5161 }
5162
5163 if ( WC()->cart->get_cart_contents_count() == 0 ) {
5164 wc_add_notice(__('Your shopping cart is empty','woo-vipps'),'error');
5165 wp_redirect($backurl);
5166 exit();
5167 }
5168
5169 add_filter('body_class', function ($classes) {
5170 $classes[] = 'vipps-express-checkout';
5171 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5172 return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5173 });
5174
5175 do_action('woo_vipps_express_checkout_page');
5176
5177 $this->print_express_checkout_page(true, 'do_express_checkout');
5178 }
5179
5180 // This method tries to ensure that a customer does not 'lose' the return page and
5181 // starts ordering the same products twice. IOK 2020-01-22
5182 protected function validate_express_checkout_orderspec ($orderspec) {
5183 if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5184
5185 // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5186 $orderset = array();
5187 foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5188
5189 // Then get open orders
5190 $sessionorders = array();
5191 $sessionorderdata = WC()->session->get('_vipps_session_orders');
5192 if ($sessionorderdata) {
5193 foreach(array_keys($sessionorderdata) as $oid) {
5194 $orderobject = wc_get_order($oid);
5195 // Check to see that this hasn't been deleted yet IOK 2020-01-07
5196 if ($orderobject instanceof WC_Order) {
5197 $sessionorders[] = $orderobject;
5198 }
5199 }
5200 }
5201 // Nothing more to do here
5202 if (empty($sessionorders)) return true;
5203
5204 // And create a similar hash table for each of the open orders
5205 $openorderdata = array();
5206 foreach ($sessionorders as $open_order) {
5207 $status = $open_order->get_status();
5208 if ($status == 'cancelled' || $status == 'pending') continue;
5209 $when = strtotime($open_order->get_date_modified());
5210 $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5211 if (time() > $cutoff) {
5212 continue;
5213 }
5214 $orderdata = array();
5215 foreach($open_order->get_items() as $item) {
5216 $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5217 $orderdata[] = $productspec;
5218 }
5219 $openorderdata[]=$orderdata;
5220 }
5221
5222 // Now: For each entry in the orderhash, check if there is an order that has a) all of them and b) not any more of them.
5223 foreach($openorderdata as $prevorder) {
5224 $a = array_diff($prevorder, $orderset);
5225 $b = array_diff($orderset, $prevorder);
5226 if (empty($a) && empty($b)) {
5227 $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
5228 return false;
5229 }
5230 }
5231 // Else, order is good.
5232 return true;
5233 }
5234
5235 // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5236 // Return value is like in a cart.
5237 // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5238 protected function get_orderspec_from_arguments ($productinfo) {
5239 if (!$productinfo) return array();
5240 $variantid = 0;
5241 $productid = 0;
5242 $quantity = intval(@$productinfo['quantity']);
5243 if (!$quantity) $quantity = 1;
5244 if (isset($productinfo['sku']) && $productinfo['sku']) {
5245 $sku = $productinfo['sku'];
5246 $skuid = wc_get_product_id_by_sku($sku);
5247 $product = wc_get_product($skuid);
5248 $parentid = $product ? $product->get_parent_id() : null;
5249 if ($product) {
5250 if ($parentid) {
5251 $variantid = $skuid; $productid = $parentid;
5252 } else {
5253 $productid = $skuid;
5254 }
5255 }
5256 } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5257 $productid = intval($productinfo['product_id']);
5258 $variantid = intval(@$productinfo['variation_id']);
5259 }
5260 if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5261 return array();
5262 }
5263 // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5264 protected function get_orderspec_from_cart () {
5265 $cartitems = WC()->cart->get_cart();
5266 $orderspec = array();
5267 foreach($cartitems as $item => $values) {
5268 $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5269 }
5270 return $orderspec;
5271 }
5272
5273 // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5274 protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5275 $gw = $this->gateway();
5276
5277 $expressCheckoutMessages = array();
5278 $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5279 $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5280 $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5281
5282 wp_register_script('vipps-express-checkout',plugins_url('js/express-checkout.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/express-checkout.js"), 'true');
5283 wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5284 wp_enqueue_script('vipps-express-checkout');
5285 // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5286 // to actually perform the express checkout.
5287 $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5288
5289
5290 $orderspec = $this->get_orderspec_from_arguments($productinfo);
5291 if (empty($orderspec)) {
5292 $orderspec = $this->get_orderspec_from_cart();
5293 }
5294 $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5295 $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5296
5297 $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5298 $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5299 $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5300
5301 $askForConfirmationHTML = '';
5302 if (!$orderisOK) {
5303 $header = __("Are you sure?",'woo-vipps');
5304 $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
5305 $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5306 }
5307 // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5308 $execute = $execute && $orderisOK && !$askForTerms;
5309 $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5310
5311 $content = $this->spinner();
5312
5313 // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5314 // The form data below is sent on order creation; the sec is also used to poll session status
5315 $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5316 $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5317 $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5318 if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5319 // This is for the new order attribution feature of woo. IOK 2024-01-09
5320 $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5321 ob_start();
5322 do_action( 'woocommerce_after_order_notes');
5323 $content .= ob_get_clean();
5324 }
5325 $content .= wp_nonce_field('do_express','sec',1,false);
5326
5327 $termsHTML = '';
5328 if ($askForTerms) {
5329 // Include shop terms
5330 ob_start();
5331 wc_get_template('checkout/terms.php');
5332 $termsHTML = ob_get_clean();
5333 $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5334 }
5335 $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5336
5337 if ($productinfo) {
5338 foreach($productinfo as $key=>$value) {
5339 $k = esc_attr($key);
5340 $v = esc_attr($value);
5341 $content .= "<input type='hidden' name='$k' value='$v' />";
5342 }
5343 }
5344 ob_start();
5345 $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5346 $content .= ob_get_clean();
5347 $content .= "</form>";
5348
5349 $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5350 $pressTheButtonHTML = "";
5351 if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5352 $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5353 }
5354
5355 if ($execute) {
5356 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5357 $content .= "<div id='vipps-status-message'></div>";
5358 $this->fakepage(__('Order in progress','woo-vipps'), $content);
5359 return;
5360 } else {
5361 $content .= $askForConfirmationHTML;
5362 $content .= $extraHTML;
5363 $content .= $termsHTML;
5364 $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5365 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5366 $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='button vipps-express-checkout' title='$title'><img alt='$title' border=0 src='$buttonimgurl'></a></div>";
5367 $content .= "<div id='vipps-status-message'></div>";
5368 $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5369 return;
5370 }
5371 }
5372
5373
5374
5375 public function vipps_wait_for_payment() {
5376 status_header(200,'OK');
5377 Vipps::nocache();
5378
5379 $orderid = WC()->session->get('_vipps_pending_order');
5380
5381 $order = null;
5382 $gw = $this->gateway();
5383
5384 // Failsafe for when the session disappears IOK 2018-11-19
5385 $no_session = $orderid ? false : true;
5386 $limited_session = sanitize_text_field(@$_GET['ls']);
5387
5388 // Now we *should* have a session at this point, but the session may have been deleted,
5389 // or the session may be in another browser, because we get here by the Vipps app opening the app.
5390 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5391 // simulating the session with that.
5392 // IOK 2019-11-19, changed to using GET 2023-01-23
5393 if ($no_session && $limited_session) {
5394 $orderid = intval(@$_GET['id']);
5395 }
5396 if ($orderid) {
5397 clean_post_cache($orderid);
5398 $order = wc_get_order($orderid);
5399 }
5400
5401 // if we came here with no session, check to see if we are allowed to do stuff with the order.
5402 if ($order && $no_session) {
5403 if (!$order->get_meta('_vipps_limited_session') || (!wp_check_password($limited_session, $order->get_meta('_vipps_limited_session')))) {
5404 $this->log("Wrong order session id on Vipps payment return url", 'error');
5405 $order = null; $orderid=0;
5406 } else {
5407 $session = WC()->session;
5408 if (!$session->has_session()) {
5409 $session->set_customer_session_cookie(true);
5410 }
5411 $session->set('_vipps_pending_order', $orderid);
5412 }
5413 }
5414
5415
5416 do_action('woo_vipps_wait_for_payment_page',$order);
5417
5418 $deleted_order=0;
5419 if ($orderid && !$order) {
5420 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5421 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5422 $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5423 $deleted_order=1;
5424 }
5425
5426 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
5427
5428 // If we are done, we are done, so go directly to the end. IOK 2018-05-16
5429 $status = $deleted_order ? 'cancelled' : $order->get_status();
5430
5431 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5432 $do_poll = true;
5433
5434 // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5435 if ($do_poll && $status == 'pending') {
5436 // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5437 $newstatus = $gw->callback_check_order_status($order);
5438 if ($status != $newstatus) {
5439 $status = $newstatus;
5440 clean_post_cache($orderid);
5441 $order = wc_get_order($orderid); // Reload order object
5442 }
5443 } else {
5444 // No need to do anyting here. IOK 2020-01-26
5445 }
5446
5447 $payment = 'notchecked';
5448 if ($do_poll) {
5449 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5450 }
5451
5452 // All these payment statuses are successes so go to the thankyou page.
5453 if ($payment == 'authorized' || $payment == 'complete') {
5454 // IOK 2023-07-17 this used to be called in the woocommerce_thankyou hook, now we do it here instead, since
5455 // we may need to be logged in to be able to get to that hook.
5456 $this->woocommerce_before_thankyou($order->get_id());
5457 wp_redirect($gw->get_return_url($order));
5458 exit();
5459 }
5460
5461 // We are done, but in failure. Don't poll.
5462 $content = "";
5463 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5464
5465 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5466 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
5467 if ('failed' == $status) {
5468 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5469 wp_redirect($failure_redirect);
5470 exit();
5471 }
5472 if ($status == 'cancelled' || $payment == 'cancelled') {
5473 $this->maybe_restore_cart($orderid,'failed');
5474 if ($failure_redirect){
5475 wp_redirect($failure_redirect);
5476 exit();
5477 }
5478 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5479 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5480 $content .= "</div>";
5481 $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5482
5483 return;
5484 }
5485
5486 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5487
5488 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5489 wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5490
5491 // Check that order exists and belongs to our session. Can use WC()->session->get() I guess - set the orderid or a hash value in the session
5492 // and check that the order matches (and is 'pending') (and exists)
5493 $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5494 $vippsstatus = $order->get_meta('_vipps_status');
5495 $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5496
5497 $signal = $this->callbackSignal($order);
5498 $content = "";
5499 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5500
5501 if ($signal && !is_file($signal)) $signal = '';
5502 $signalurl = $this->callbackSignalURL($signal);
5503
5504 $content .= "</p></div>";
5505
5506 // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5507 $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5508 $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5509 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5510 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5511 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5512 $content .= wp_nonce_field('vippsstatus','sec',1,false);
5513 $content .= "</form>";
5514
5515
5516 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5517 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5518 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5519 $content .= "</div>";
5520
5521 $content .= "<div id=success style='display:none'><p>". __('Order confirmed', 'woo-vipps') . '</p>';
5522 $content .= "<p><a class='btn button' id='continueToThankYou' href='" . $gw->get_return_url($order) . "'>".__('Continue','woo-vipps') ."</a></p>";
5523 $content .= '</div>';
5524
5525 $content .= "<div id=failure style='display:none'><p>". __('Order cancelled', 'woo-vipps') . '</p>';
5526 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5527 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5528 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5529 $content .= "</div>";
5530
5531
5532 $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5533 }
5534
5535
5536
5537 public function fakepage($title,$content) {
5538 global $wp, $wp_query;
5539 // We don't want this here.
5540 remove_filter ('the_content', 'wpautop');
5541
5542 $specialid = $this->gateway()->get_option('vippsspecialpageid');
5543 $wp_post = null;
5544 if ($specialid) {
5545 $wp_post = get_post($specialid);
5546 if ($wp_post) {
5547 $wp_post->post_title = $title;
5548 $wp_post->post_content = $content;
5549 // Normalize a bit
5550 $wp_post->filter = 'raw'; // important
5551 $wp_post->post_status = 'publish';
5552 $wp_post->comment_status= 'closed';
5553 $wp_post->ping_status= 'closed';
5554 } else {
5555 $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5556 }
5557 }
5558 if (!$wp_post || is_wp_error($wp_post)) {
5559 $post = new stdClass();
5560 $post->ID = -99;
5561 $post->post_author = 1;
5562 $post->post_date = current_time( 'mysql' );
5563 $post->post_date_gmt = current_time( 'mysql', 1 );
5564 $post->post_title = $title;
5565 $post->post_content = $content;
5566 $post->post_status = 'publish';
5567 $post->comment_status = 'closed';
5568 $post->ping_status = 'closed';
5569 $post->post_name = 'vippsconfirm-fake-page-name';
5570 $post->post_type = 'page';
5571 $post->filter = 'raw'; // important
5572 $wp_post = new WP_Post($post);
5573 wp_cache_add( -99, $wp_post, 'posts' );
5574 }
5575
5576 // Update the main query
5577 $wp_query->post = $wp_post;
5578 $wp_query->posts = array( $wp_post );
5579 $wp_query->queried_object = $wp_post;
5580 $wp_query->queried_object_id = $wp_post->ID;
5581 $wp_query->found_posts = 1;
5582 $wp_query->post_count = 1;
5583 $wp_query->max_num_pages = 1;
5584 $wp_query->is_page = true;
5585 $wp_query->is_singular = true;
5586 $wp_query->is_single = false;
5587 $wp_query->is_attachment = false;
5588 $wp_query->is_archive = false;
5589 $wp_query->is_category = false;
5590 $wp_query->is_tag = false;
5591 $wp_query->is_tax = false;
5592 $wp_query->is_author = false;
5593 $wp_query->is_date = false;
5594 $wp_query->is_year = false;
5595 $wp_query->is_month = false;
5596 $wp_query->is_day = false;
5597 $wp_query->is_time = false;
5598 $wp_query->is_search = false;
5599 $wp_query->is_feed = false;
5600 $wp_query->is_comment_feed = false;
5601 $wp_query->is_trackback = false;
5602 $wp_query->is_home = false;
5603 $wp_query->is_embed = false;
5604 $wp_query->is_404 = false;
5605 $wp_query->is_paged = false;
5606 $wp_query->is_admin = false;
5607 $wp_query->is_preview = false;
5608 $wp_query->is_robots = false;
5609 $wp_query->is_posts_page = false;
5610 $wp_query->is_post_type_archive = false;
5611 // Update globals
5612 $GLOBALS['wp_query'] = $wp_query;
5613 $wp->register_globals();
5614 return $wp_post;
5615 }
5616
5617 // Support the interactivity API with data about our cart IOK 2026-02-23
5618 public function woo_vipps_store_api_cart_data() {
5619 // 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
5620
5621 $checkout_page = $this->gateway()->vipps_checkout_available();
5622 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5623 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5624 $cart_data = array(
5625 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5626 'cart_supports_checkout' => (bool) $checkout_page,
5627 'checkout_url' => $checkout_url,
5628 );
5629 return $cart_data;
5630 }
5631
5632 public function woo_vipps_store_api_cart_schema() {
5633 return array(
5634 'cart_hide_express' => array(
5635 'description' => sprintf(__( 'Whether to hide the %1$s Express Checkout in the cart', 'woo-vipps' ), $this->get_payment_method_name()),
5636 'type' => array( 'boolean', 'null' ),
5637 'readonly' => true,
5638 ),
5639 'cart_supports_checkout' => array(
5640 'description' => sprintf(__( 'True if %1$s is active and the cart supports it', 'woo-vipps' ), $this->CheckoutName()),
5641 'type' => array( 'boolean', 'null' ),
5642 'readonly' => true,
5643 ),
5644 'checkout_url' => array(
5645 'description' => sprintf(__( 'Current checkout url based on cart state', 'woo-vipps' ), $this->get_payment_method_name()),
5646 'type' => array( 'string', 'null' ),
5647 'readonly' => true,
5648 ),
5649 );
5650 }
5651
5652 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5653 public static function order_is_vipps_retryable($order_id) {
5654 $order = wc_get_order($order_id);
5655 if (!$order) return false;
5656 $api = $order->get_meta('_vipps_api');
5657 $nonexpress_epayment = 'epayment' === $api && !$order->get_meta('_vipps_express_checkout');
5658 $shipping_set = $order->get_meta('_vipps_shipping_set');
5659
5660 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5661 return $nonexpress_epayment || $shipping_set;
5662 }
5663 }
5664