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

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