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

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

4,379 lines 241.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Delegate class for talking to Vipps MobilePay, encapsulating all the low-level behaviour and mapping error codes to exceptions
4
5 This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce
6 Copyright (c) 2019 WP-Hosting AS
7
8 MIT License
9
10 Copyright (c) 2019 WP-Hosting AS
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29
30 */
31 if ( ! defined('ABSPATH') ) {
32 exit; // Exit if accessed directly
33 }
34 require_once(dirname(__FILE__) . "/VippsApi.class.php");
35
36 class WC_Gateway_Vipps extends WC_Payment_Gateway {
37 public $form_fields = null;
38 public $dev_form_fields = null;
39 public $id = 'vipps';
40 public $icon = '';
41 public $has_fields = true;
42 public $method_title = 'Vipps MobilePay';
43 public $title = 'Vipps MobilePay';
44 public $method_description = "";
45 public $apiurl = null;
46 public $testapiurl = null;
47 public $api = null;
48 public $supports = null;
49 public $express_checkout_supported_product_types;
50
51 public $captured_statuses;
52
53 private static $instance = null; // This class uses the singleton pattern to make actions easier to handle
54
55 protected $keyset = null; // This will contain all api keys etc for the gateway, keyed on the merchant serial number.
56
57 // Just to avoid calculating these alot
58 private $page_templates = null;
59 private $page_list = null;
60
61
62
63 // This returns the singleton instance of this class
64 public static function instance() {
65 if (null === self::$instance) {
66 self::$instance = new self();
67 }
68 return self::$instance;
69 }
70
71 public function add_image_upload_setting_widget () {
72 add_action('admin_enqueue_scripts', function ($suff) {
73 if ($suff == 'woocommerce_page_wc-settings' && (($_REQUEST['section'] ?? false) == 'vipps')) {
74 if (!did_action('wp_enqueue_media')) {
75 wp_enqueue_media();
76 }
77 }
78 });
79 }
80
81
82 // Generates html for the woo_vipps_image settings widget type
83 public function generate_woo_vipps_image_html ($key, $field) {
84 $field_key = $this->get_field_key( $key );
85 $defaults = array(
86 'title' => '',
87 'disabled' => false,
88 'class' => '',
89 'css' => '',
90 'placeholder' => '',
91 'type' => 'woo_vipps_image',
92 'desc_tip' => false,
93 'description' => '',
94 'custom_attributes' => array(),
95 );
96 $data = wp_parse_args( $field, $defaults );
97
98 $imgid = intval($this->get_option($key));
99 $image = $imgid ? wp_get_attachment_image_src($imgid) : "";
100
101 ob_start();
102 ?>
103 <tr valign="top">
104 <th scope="row" class="titledesc">
105 <label for="<?php echo esc_attr( $field_key ); ?>"><?php echo wp_kses_post( $data['title'] ); ?> <?php echo $this->get_tooltip_html( $data ); // WPCS: XSS ok. ?></label>
106 </th>
107 <td class="forminp">
108 <fieldset>
109 <legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
110 <?php if ($image): ?>
111 <a href="#" class="woo-vipps-image-upload"><img style="max-width: 360px; max-height: 360px" src="<?php echo $image[0]; ?>" /><span style="display:none" class='uploadtext'><?php _e('Upload image', 'woo-vipps'); ?></span></a>
112 <a href="#" class="woo-vipps-image-remove"><?php _e('Remove image', 'woo-vipps');?></a>
113 <?php else: ?>
114 <a href="#" class="woo-vipps-image-upload"><img style="display:none; max-width:360px; max-height: 360px"/><span class='uploadtext'><?php _e('Upload image', 'woo-vipps'); ?></span></a>
115 <a href="#" class="woo-vipps-image-remove" style="display:none;"><?php _e('Remove image', 'woo-vipps');?></a>
116 <?php endif; ?>
117 <input type="hidden" class="woo-vipps-image-input <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" value="<?php echo esc_attr($imgid); ?>" <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?> />
118 <?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>
119 </fieldset>
120 </td>
121 </tr>
122 <?php
123
124 return ob_get_clean();
125 }
126
127 // Attempts to detect the current country based on the store's currency NT-2024-10-15
128 private function detect_country_from_currency() {
129 $currency = get_woocommerce_currency();
130 switch ($currency) {
131 case 'DKK':
132 return 'DK';
133 case 'NOK':
134 return 'NO';
135 case 'SEK':
136 return 'SE';
137 case 'EUR':
138 return 'FI';
139 default:
140 return null;
141 }
142 }
143 // Migrates the keysets to include the country setting if it's missing
144 // This is an initial migration step to ensure the country setting is explicitly set.
145 // We do this because we no longer want to automatically guess payment method name. NT-2024-10-15
146 private function migrate_keyset_with_country_detection() {
147 $settings = get_option('woocommerce_vipps_settings', array());
148 if ($settings['country'] ?? false) return; // Already set, do nothing IOK 2024-10-17
149
150 // Now we are only wanting to do this with people who have already configured the plugin. These will have at least this value set:
151 if ($settings['payment_method_name'] ?? false) {
152 // This assumes that EUR == FI which will be correct for all users reaching this branch IOK 2024-10-17
153 $detected_country = $this->detect_country_from_currency();
154 // If we can't detect the country, there's nothing to migrate. "this cannot happen" etc.
155 if (!$detected_country) return;
156
157 $settings['country'] = $detected_country;
158 update_option('woocommerce_vipps_settings', $settings);
159 delete_transient('_vipps_keyset');
160 return;
161 }
162 }
163
164 public function __construct() {
165 $this->testapiurl = 'https://apitest.vipps.no';
166 $this->apiurl = 'https://api.vipps.no';
167
168 $this->method_description = __('Offer Vipps or MobilePay as a payment method', 'woo-vipps');
169 $this->method_title = __('Vipps MobilePay','woo-vipps');
170 $this->title = __('Vipps MobilePay','woo-vipps');
171
172 $this->icon = plugins_url('img/vmp-logo.png',__FILE__);
173 $this->migrate_keyset_with_country_detection();
174 $this->init_form_fields();
175 $this->init_settings();
176
177
178 $this->api = new VippsApi($this);
179
180 $this->supports = array('products','refunds');
181
182 // we need to disallow certain types of refunds; mostly manual refunds done before capture. IOK 2026-02-24
183 add_action( 'woocommerce_create_refund', array($this, 'woocommerce_create_refund'), 10, 2);
184
185 // We can't guarantee any particular product type being supported, so we must enumerate those we are certain about
186 // IOK 2020-04-21 Add support for WooCommerce Product Bundles
187 $supported_types= array('simple','variable','variation','bundle', 'yith_bundle', 'gift-card');
188 $this->express_checkout_supported_product_types = apply_filters('woo_vipps_express_checkout_supported_product_types', $supported_types);
189
190 add_action('woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options') );
191 add_action('admin_init', array($this, 'add_image_upload_setting_widget'));
192
193 // Capturing, refunding and cancelling the order when transitioning states:
194 // This are the statuses for which the Vipps MobilePay plugin should try to ensure capture has been made.
195 // Normally, this is 'processing' and 'completed', but plugins may define other statuses. IOK 2018-10-05
196 // It is also possible to remove 'processing' from this list. If you do, you may use it as the end-state of the
197 // Vipps MobilePay transaction (see below in after_vipps_order_status) IOK 2018-12-05
198 $resultstatus = $this->get_option('result_status');
199 $captured_statuses = apply_filters('woo_vipps_captured_statuses', array('processing', 'completed'));
200 $captured_statuses = array_diff($captured_statuses, array($resultstatus));
201
202 $this->captured_statuses = $captured_statuses;
203
204 $non_completed_captured_statuses = array_diff($captured_statuses, array('completed'));
205
206 // This ensures that funds are captured when transitioning from 'on hold' to a status where the money
207 // should be captured, and refunded when moved from this status to cancelled or refunded
208 foreach($captured_statuses as $capstatus) {
209 add_action('woocommerce_order_status_' . $capstatus, array($this, 'maybe_capture_payment'));
210 }
211 // We will refund money on cancelled orders, but only if they are *relatively new*. This is to
212 // avoid accidents and issues where old orders are *somehow* cancelled even though they are complete. IOK 2024-08-12
213 add_action('woocommerce_order_status_cancelled', array($this, 'maybe_cancel_order'));
214 // Add a full refund if neccessary *before* woocommerce does, on priority 10 IOK 2026-01-28
215 add_action('woocommerce_order_status_refunded', array($this, 'maybe_refund_order'), 9, 1);
216
217 // Possibly delete orders that never went anywhere
218 add_action('woocommerce_order_status_pending_to_cancelled', array($this, 'maybe_delete_order'), 99999, 1);
219
220 // Disable emails for cancelled express orders that never went anywhere IOK 2026-09-09
221 add_filter('woocommerce_email_enabled_cancelled_order', function ( $enabled, $order, $email ) {
222 if ( ! $order instanceof WC_Order ) {
223 return $enabled;
224 }
225 $pm = $order->get_payment_method();
226 if (! Vipps::is_vipps_order($pm)){
227 return $enabled;
228 }
229 $is_vipps_express = (bool) $order->get_meta( '_vipps_express_checkout' );
230 $has_billing_email = (bool) $order->get_billing_email();
231 if ( $is_vipps_express && ! $has_billing_email ) {
232 return false;
233 }
234 return $enabled;
235 }, 10, 3);
236
237 // Handle orders when authorized
238 add_action('woocommerce_payment_complete', array($this, 'order_payment_complete'), 10, 1);
239
240 // when an order is complete, we need to check if there is reserved amount that is not captured
241 // if so, we need to cancel this amount PMB 2024-11-21
242 // nb: note very late priority - we must have captured before, please
243 // Also for orders that have been partially or completely refunded, or need to be set to cancelled IOK 2026-01-26
244 add_action('woocommerce_order_status_completed', array($this, 'maybe_cancel_reserved_amount'), 99);
245 add_action('woocommerce_order_status_refunded', array($this, 'maybe_cancel_reserved_amount'), 99, 1);
246 add_action('woocommerce_order_status_cancelled', array($this, 'maybe_cancel_reserved_amount'), 99, 1);
247
248 // New handling for callbacks in the action scheduler. LP 2026-03-27
249 add_action('woo_vipps_action_process_callback', [$this, 'action_process_callback'], 10, 4);
250
251 // Endpoint for setting shipping data for express checkout orders. LP 2026-03-30
252 add_action('rest_api_init', function() {
253 register_rest_route(Vipps::get_rest_namespace('v1'), '/order-set-shipping', [
254 'methods' => 'POST',
255 'callback' => [$this, 'rest_order_set_shipping'],
256 'permission_callback' => function($request) {
257 // Note: permission callbacks run twice, on purpose. LP 2026-04-01
258 // https://github.com/WP-API/WP-API/issues/2400
259 $input_token = $request->get_header('X-WooVipps-Token');
260
261 $order_id = $request->get_param('order_id');
262
263 $order = wc_get_order($order_id);
264 if (!is_a($order, 'WC_Order')) {
265 return new WP_Error('order_not_found', __('Order not found', 'woo-vipps'), ['status' => 404, 'order_id' => $order_id]);
266 }
267
268 // a small bit of security
269 $auth_token = $order->get_meta('_vipps_authtoken');
270 if (!$input_token || !$auth_token || !hash_equals($input_token, $auth_token)) {
271 /* translators: endpoint path, order id */
272 $this->log(sprintf(__('Wrong authtoken for rest endpoint %1$s for order %2$s', 'woo-vipps'), '/order-set-shipping', $order_id), 'warning');
273 return false;
274 }
275 return true;
276
277 },
278 'args' => [
279 'order_id' => [
280 'required' => true,
281 'validate_callback' => fn($param, $request, $key) => is_numeric($param),
282 'sanitize_callback' => fn($param, $request, $key) => intval($param),
283 ],
284 /* Data from Vipps callback or api poll. LP 2026-03-30 */
285 'vipps_order_data' => [
286 'required' => true,
287 'validate_callback' => fn($param, $request, $key) => is_array($param),
288 'sanitize_callback' => fn($param, $request, $key) => map_deep($param, 'sanitize_text_field'),
289 ],
290 ],
291 ]);
292 });
293 }
294
295 // this function is called after an order is changed to complete/refunded/cancelled. It checks if there is reserved money that is not captured
296 // if there still is money reserved, then this amount is cancelled PMB 2024-11-21
297 // Ensure we've updated the vipps status before calling this. IOK 2026-01-28
298 public function maybe_cancel_reserved_amount ($orderid) {
299 $order = wc_get_order($orderid);
300 if (!$order) return;
301 if (! Vipps::is_vipps_order($order)) return false;
302
303 // Check that the normal maybe_capture_order hook has actually ran *and* done something,
304 // it's only after this we know we have captured 'everything' so if there is anything left,
305 // it should be cancelled. IOK 2025-05-04
306 // Also set in maybe_cancel and maybe_refund now - in all "final status" hooks. IOK 2026-01-28
307 if (! $order->get_meta('_vipps_capture_complete')) {
308 return false;
309 }
310
311 $order_status = $order->get_status();
312
313 if ('completed' == $order_status) {
314 // For safety, on completed orders also only want to do this for orders that have had *something* captured. IOK 2025-02-04
315 $captured = intval($order->get_meta('_vipps_captured'));
316 if ($captured < 1) {
317 return false;
318 }
319 }
320 if ('cancelled' != $order_status) {
321 // If not in the 'cancelled' state, allow merchants that do not reserve large amounts to opt out for safety IOK 2025-02-04
322 if (apply_filters('woo_vipps_never_cancel_uncaptured_money', false, $order)) {
323 return false;
324 }
325 }
326
327 $ok = true;
328
329 $remaining = intval($order->get_meta('_vipps_capture_remaining'));
330
331 if ($remaining > 0) {
332 $this->log(sprintf(__("maybe_cancel_reserved_amount we have remaining reserved after capture of total %1\$s ",'woo-vipps'), $remaining),'debug');
333 } else {
334 // IOK 2026-06-15 Nothing left to cancel, just return
335 return false;
336 }
337
338 $currency = $order->get_currency();
339 try {
340 $ok = $this->cancel_payment($order);
341 if ($ok && $remaining && 'completed' == $order_status) {
342 $amount = number_format($remaining/100, 2) . " " . $currency;
343 $note = sprintf(__('Order %1$s: %2$s is cancelled to free up the reservation in the customers bank account.', 'woo-vipps'), $orderid, $amount);
344 $order->add_order_note($note);
345 }
346 } catch (Exception $e) {
347 $ok = false;
348 // if this happens, we just log it - we may not have an active admin
349 $msg = sprintf(__('Was not able to cancel remaining amount for the order %1$s: %2$s','woo-vipps'), $orderid, $e->getMessage());
350 $order->add_order_note($msg);
351 $this->log($msg,'error');
352 }
353
354 // We need to update the order details after the fact. We can't fix errors here though. IOK 2024-11-22
355 try {
356 $this->update_vipps_payment_details($order);
357 } catch (Exception $e) {
358 // noop
359 }
360
361 // we just return true from this function for now PMB 2024-11-21
362 // return false if we couldn't cancel reserved. IOK 2024-11-22
363 return $ok;
364 }
365
366
367 public function get_icon () {
368 $src = $this->icon;
369 if ($this->get_payment_method_name() == "Vipps") {
370 $src = plugins_url('img/vipps-mark.svg',__FILE__);
371 } else {
372 $src = plugins_url('img/mobilepay-mark.png',__FILE__);
373 }
374 return '<img src="' . esc_attr($src) . '" alt="' . $this->get_payment_method_name() . '">';
375 }
376
377
378 // True iff this gateway is currently in test mode. IOK 2019-08-30
379 public function is_test_mode() {
380 if (VIPPS_TEST_MODE) return true;
381 if ($this->get_option('developermode') == 'yes' && $this->get_option('testmode') == 'yes') return true;
382 return false;
383 }
384 // These abstraction gets the correct client id and so forth based on whether or not test mode is on
385 // "test mode" is now per MSN, so we accept that as an argument IOK 2023-12-19
386 public function apiurl ($msn="") {
387 $msn = $msn ?? $this->get_merchant_serial();
388 $keyset = $this->get_keyset();
389 $entry = $keyset ? ($keyset[$msn] ?? null) : null;
390 if (!$entry) {
391 $testmode = $this->is_test_mode();
392 } else {
393 $testmode = $entry['testmode'];
394 }
395 if ($testmode) return $this->testapiurl;
396 return $this->apiurl;
397 }
398
399 // This returns the *current* merchant serial number. There may be more than one, for instance if the test mode is on.
400 // IOK 2023-12-19
401 public function get_merchant_serial() {
402 $merch = $this->get_option('merchantSerialNumber');
403 $testmerch = @$this->get_option('merchantSerialNumber_test');
404 if (!empty($testmerch) && $this->is_test_mode()) return $testmerch;
405 return $merch;
406 }
407
408 // Returns a table of all the keydata of this instance, keyed on MSN. IOK 2023-12-19
409 public function get_keyset() {
410 if ($this->keyset) return $this->keyset;
411 $stored = get_transient('_vipps_keyset');
412 if ($stored) {
413 return $stored;
414 }
415
416 $keyset = [];
417 $main = $this->get_option('merchantSerialNumber');
418 if ($main) {
419 $data = ['client_id'=>$clientid=$this->get_option('clientId'),
420 'client_secret' => $this->get_option('secret'),
421 'sub_key'=>$this->get_option('Ocp_Apim_Key_eCommerce'),
422 'country' => $this->get_option('country'),
423 'gw' => 'vipps'
424 ];
425 if (! in_array(false, array_map('boolval', array_values($data)))) {
426 $data['testmode'] = 0; // Must add after
427 $keyset[$main] = $data;
428 }
429 }
430
431 $test = @$this->get_option('merchantSerialNumber_test');
432 $testmode = @$this->get_option('testmode');
433 if ($testmode === 'yes' && $test) {
434 $data = [
435 'client_id'=>$clientid=$this->get_option('clientId_test'),
436 'client_secret' => $this->get_option('secret_test'),
437 'sub_key'=>$this->get_option('Ocp_Apim_Key_eCommerce_test'),
438 'country' => $this->get_option('country'),
439 'gw' => 'vipps'
440 ];
441
442 if (! in_array(false, array_map('boolval', array_values($data)))) {
443 $data['testmode'] = 1;
444 $keyset[$test] = $data;
445 }
446 }
447
448 $this->keyset = $keyset;
449 set_transient('_vipps_keyset', $keyset, DAY_IN_SECONDS);
450 return $keyset;
451 }
452
453 // Get and show any keysets from the Vipps Recurring plugin too IOK 2024-11-29
454 private function get_recurring_keysets () {
455 $res = [];
456 $settings = get_option( 'woocommerce_vipps_recurring_settings' );
457 if (empty($settings)) return $res;
458
459 $country = $this->get_option('country');
460
461 $client_id = $settings["client_id"] ?? "";
462 $client_secret = $settings["secret_key"] ?? "";
463 $subscription_key = $settings["subscription_key"] ?? "";
464 $merchant_serial_number = $settings["merchant_serial_number"] ?? "";
465 if ($merchant_serial_number && $client_id && $client_secret && $subscription_key ) {
466 $res[$merchant_serial_number] = ['client_id' => $client_id, 'client_secret' => $client_secret, 'sub_key' => $subscription_key, 'country' => $country, 'testmode' => 0, 'gw'=>'vipps_recurring'];
467 }
468
469 $client_id = $settings["test_client_id"] ?? "";
470 $client_secret = $settings["test_secret_key"] ?? "";
471 $subscription_key = $settings["test_subscription_key"] ?? "";
472 $merchant_serial_number = $settings["test_merchant_serial_number"] ?? "";
473 if ($merchant_serial_number && $client_id && $client_secret && $subscription_key ) {
474 $res[$merchant_serial_number] = ['client_id' => $client_id, 'client_secret' => $client_secret, 'sub_key' => $subscription_key, 'country' => $country, 'testmode' => 1, 'gw'=>'vipps_recurring'];
475 }
476 return $res;
477 }
478
479 // Return all webhooks for our MSNs
480 public function get_webhooks_from_vipps () {
481 $keys = $this->get_keyset();
482 $hooks = [];
483 foreach($keys as $msn=>$data) {
484 try {
485 $hooks[$msn] = $this->api->get_webhooks($msn);
486 } catch (Exception $e) {
487 $this->log(sprintf(__('Could not get webhooks for Merchant Serial Number %1$s: %2$s', 'woo-vipps'), $msn, $e->getMessage()), 'error');
488 $hooks[$msn]=[];
489 }
490 }
491 return $hooks;
492 }
493
494
495 // The rest of the settings gets the correct client id, secret, sub key and order prefix based on the MSN.
496 public function get_clientid($msn="") {
497 if (!$msn) $msn = $this->get_merchant_serial();
498 $keyset = $this->get_keyset();
499 if (!isset($keyset[$msn])) return false;
500 return $keyset[$msn]['client_id'];
501 }
502 public function get_secret($msn="") {
503 if (!$msn) $msn = $this->get_merchant_serial();
504 $keyset = $this->get_keyset();
505 if (!isset($keyset[$msn])) return false;
506 return $keyset[$msn]['client_secret'];
507 }
508 public function get_key($msn="") {
509 if (!$msn) $msn = $this->get_merchant_serial();
510 $keyset = $this->get_keyset();
511 if (!isset($keyset[$msn])) return false;
512 return $keyset[$msn]['sub_key'];
513 }
514 public function get_country($msn="") {
515 if (!$msn) $msn = $this->get_merchant_serial();
516 $keyset = $this->get_keyset();
517 if (!isset($keyset[$msn])) return false;
518 return $keyset[$msn]['country'];
519 }
520
521 public function get_orderprefix() {
522 $prefix = $this->get_option('orderprefix');
523 return $prefix;
524 }
525
526 // We did shenanigans here earlier, we don't have to do that anymore. IOK 2022-12-09
527 public function get_return_url($order=null) {
528 $url = parent::get_return_url($order);
529 return $url;
530 }
531
532
533 // Delete express checkout orders with no customer information - these were abandonend before the app started.
534 // IOK 2019-08-26
535 public function maybe_delete_order ($orderid) {
536 $order = wc_get_order($orderid);
537 if (!$order) return;
538 if (! Vipps::is_vipps_order($order)) return false;
539 $express = $order->get_meta('_vipps_express_checkout');
540 if (!$express) return false;
541 $email = $order->get_billing_email();
542 if ($email) return false;
543
544 // Only delete if we have to
545 if ($this->get_option('deletefailedexpressorders') != 'yes') return false;
546 // Mark this order that an order that wasn't completed with any user info - it can be deleted. IOK 2019-11-13
547 $order->update_meta_data('_vipps_delendum',1);
548 $order->save();
549 return true;
550 }
551
552
553 // Return the status to use after return from Vipps MobilePay for orders that are not both "virtual" and "downloadable".
554 // These orders are *not* complete, and payment is *not* captured, which is why the default status is 'on-hold'.
555 // If you use custom order statuses, or if you don't capture on 'processing' - see filter 'woo_vipps_captured_statuses' -
556 // you can instead use 'processing' here - which is much nicer.
557 // If you do so, remember to capture *before* shipping is done on the order - if you send the package and then do 'complete',
558 // the capture may fail. IOK 2018-12-05
559 //
560 // IOK As of 2023-12-22, the default is now 'processing', since this is more in line with what other gateways are using,
561 // what other integrations and plugins expect, the most popular choice by users; and because of the fact that "on-hold" is
562 // normally used to indicate "a problem with the order". Not being able to capture a reserved order has to my knowledge at this
563 // point only happened once, in 2018, with a completely different api and backend.
564 public function after_vipps_order_status($order=null) {
565 // Revert to on-hold if the user tries to set a payment status that is a 'captured' status IOK 2024-01-25
566 $defaultstatus = 'on-hold';
567
568 $chosen = $this->get_option('result_status');
569 $newstatus = apply_filters('woo_vipps_after_vipps_order_status', $chosen, $order);
570
571 if (in_array($newstatus, $this->captured_statuses)){
572 $this->log(sprintf(__("Cannot use %1\$s as status for non-autocapturable orders: payment is captured on this status. See the woo_vipps_captured_statuses-filter.",'woo-vipps'), $newstatus),'debug');
573 return $defaultstatus;
574 }
575 return $newstatus;
576 }
577
578 // Create callback urls' using WC's callback API in a way that works with Vipps MobilePay callbacks and both pretty and not so pretty urls.
579 private function make_callback_urls($forwhat,$token='', $reference=0) {
580 // Passing the token as GET arguments, as the Authorize header is stripped. IOK 2018-06-13
581 // This applies to Ecom, Checkout and Express Checkout callbacks. For epayment, we instead need to use
582 // the webhook api, which is altogether different. IOK 2023-12-19
583 $url = home_url("/", 'https');
584 $queryargs = [];
585 if ($token) $queryargs['tk']=$token;
586 if ($reference) $queryargs['id']=$reference;
587
588 // HTTPS required. IOK 2018-05-18
589 // If the user for some reason hasn't enabled pretty links, fall back to ancient version. IOK 2018-04-24
590 if ( !get_option('permalink_structure')) {
591 $queryargs['wc-api'] = $forwhat;
592 } else {
593 $url = trailingslashit(home_url("wc-api/$forwhat", 'https'));
594 }
595 // And we need to add an empty "callback" query arg as the very last arg to receive the actual callback.
596 // We can't use add_query_arg for that, as an empty argument will remove the equals-sign.
597 $callbackurl = add_query_arg($queryargs, $url) . "&callback=";
598 return $callbackurl;
599 }
600
601 // Webhook callbacks do not pass GET arguments at all, but do provide an X-Vipps-Authorization header for verification. IOK 2023-12-19
602 public function webhook_callback_url () {
603 $url = home_url("/", 'https');
604 $queryargs = ['callback'=>'webhook'];
605 $forwhat = 'wc_gateway_vipps'; // Same callback as for epayment, checkout, express checkout
606 // HTTPS required. IOK 2018-05-18
607 // If the user for some reason hasn't enabled pretty links, fall back to ancient version. IOK 2018-04-24
608 if ( !get_option('permalink_structure')) {
609 $queryargs['wc-api'] = $forwhat;
610 } else {
611 $url = trailingslashit(home_url("wc-api/$forwhat", 'https'));
612 }
613 $callbackurl = add_query_arg($queryargs, $url);
614 return $callbackurl;
615 }
616
617
618 // The main payment callback
619 public function payment_callback_url ($token='', $reference=0) {
620 return $this->make_callback_urls('wc_gateway_vipps',$token, $reference);
621 }
622 public function shipping_details_callback_url($token='',$reference=0) {
623 return $this->make_callback_urls('vipps_shipping_details',$token,$reference);
624 }
625
626 // Allow user to select the template to be used for the special Vipps MobilePay pages. IOK 2020-02-17
627 public function get_theme_page_templates() {
628 if (!$this->page_templates) {
629 $choices = array('' => __('Use default template', 'woo-vipps'));
630 foreach(wp_get_theme()->get_page_templates() as $filename=>$name) {
631 $choices[$filename]=$name;
632 }
633 $this->page_templates = $choices;
634 }
635 return $this->page_templates;
636 }
637
638 // Check to see if the product in question can be bought with express checkout IOK 2018-12-04
639 public function product_supports_express_checkout($product) {
640 // IOK 2023-12-12 Can only support express checkout for Vipps - not MobilePay (yet!)
641 // IOK 2025-09-01 Now supports mobilepay
642 return apply_filters('woo_vipps_product_supports_express_checkout', $this->product_supports_checkout($product), $product);
643 }
644
645 // Checkout and Express Checkout are very similarily restricted because they both replace the standard
646 // Woo Checkout page, but express checkout is even more restricted, so we need to separate out the commonalities. IOK 2024-01-11
647 public function product_supports_checkout($product) {
648 $type = $product->get_type();
649 $ok = in_array($type, $this->express_checkout_supported_product_types);
650 $ok = apply_filters('woo_vipps_product_supports_checkout',$ok,$product);
651 return $ok;
652 }
653
654 // Almost the same as express checkout - unfortunately not *entirely* the same. IOK 2024-01-11
655 public function cart_supports_checkout($cart=null) {
656 if (!$cart) $cart = WC()->cart;
657 if (!$cart) return false;
658 # Not supported by Vipps MobilePay
659 if ($cart->cart_contents_total <= 0) return false;
660
661 $supports = true;
662 foreach($cart->get_cart() as $key=>$val) {
663 $prod = $val['data'];
664 if (!is_a($prod, 'WC_Product')) continue;
665 $product_supported = $this->product_supports_checkout($prod);
666 if (!$product_supported) {
667 $supports = false;
668 break;
669 }
670 }
671 $supports = apply_filters('woo_vipps_cart_supports_checkout', $supports, $cart);
672 return $supports;
673 }
674
675 // Check to see if the cart passed (or the global one) can be bought with express checkout IOK 2018-12-04
676 public function cart_supports_express_checkout($cart=null) {
677 if (!$cart) $cart = WC()->cart;
678 $supports = true;
679 if (!$cart) return $supports;
680 # Not supported by Vipps MobilePay
681 if ($cart->cart_contents_total <= 0) return false;
682
683 foreach($cart->get_cart() as $key=>$val) {
684 $prod = $val['data'];
685 if (!is_a($prod, 'WC_Product')) continue;
686 $product_supported = $this->product_supports_express_checkout($prod);
687 if (!$product_supported) {
688 $supports = false;
689 break;
690 }
691 }
692 $supports = apply_filters('woo_vipps_cart_supports_express_checkout', $supports, $cart);
693 return $supports;
694 }
695
696 // True if "Express checkout" should be displayed IOK 2018-06-18
697 public function show_express_checkout() {
698 if (!$this->express_checkout_available()) return false;
699 $show = 'yes' == $this->enabled && $this->cart_supports_express_checkout();
700
701 if (is_checkout()) {
702 $show = $show && $this->get_option('express_show_in_checkout') == 'yes';
703 } else { // for cart, and all other contexts, since this is how the method functioned before we checked checkout explicitly. LP 2026-07-02
704 $show = $show && $this->get_option('cartexpress') == 'yes';
705 }
706 // Earlier, we disabled this if Checkout was active; but we will now respect the setting in all
707 // cases. Also, there is a filter. IOK 2026-02-19
708 // Now there is also a separate setting for just checkout, 'express_show_in_checkout'. See above branch. LP 2026-07-01
709
710 return apply_filters('woo_vipps_show_express_checkout', $show);
711 }
712
713 public function show_login_with_vipps() {
714 return false;
715 }
716
717 // Called when orders reach the 'refunded' status. We'll add a complete refund and note that any rest is to be cancelled.
718 public function maybe_refund_order($order_id) {
719 $order = wc_get_order($order_id);
720 if (! Vipps::is_vipps_order($order)) return false;
721 try {
722 $order = $this->update_vipps_payment_details($order);
723 } catch (Exception $e) {
724 //Do nothing with this for now
725 $this->log(__("Error getting payment details before doing refund: ", 'woo-vipps') . $e->getMessage(), 'warning');
726 }
727 $payment = $this->check_payment_status($order);
728 if ($payment == 'initiated' || $payment == 'cancelled') {
729 return true; // Can't refund these
730 }
731
732 // This will create + process a refund for the captured amount (if any). IOK 2026-01-26
733 // We always run this since woo will create a manual refund on this status change: we want the refund to be through our gw instead. LP 2026-06-10
734 $this->wc_order_fully_refunded ($order_id);
735
736 // In any case, note that this order is ready for cancellation - we don't actually do this here anymore
737 $order->update_meta_data('_vipps_capture_complete',true);
738 $order->save();
739 }
740
741 // Called when orders reach the 'cancelled'-status. When this happens, orders will be *refunded*
742 // when they have been captured, but for added safety, this is only done when the orders are relatively new.
743 public function maybe_cancel_order($order_id) {
744 $order = wc_get_order($order_id);
745 if (! Vipps::is_vipps_order($order)) return false;
746
747 try {
748 $order = $this->update_vipps_payment_details($order);
749 } catch (Exception $e) {
750 //Do nothing with this for now
751 $this->log(__("Error getting payment details before doing cancel: ", 'woo-vipps') . $e->getMessage(), 'warning');
752 }
753
754 $payment = $this->check_payment_status($order);
755 if ($payment == 'initiated' || $payment == 'cancelled') {
756 return true; // Can't cancel these
757 }
758
759 $days_threshold = apply_filters('woo_vipps_cancel_refund_days_threshold', 30);
760 $order_date = $order->get_date_created();
761 $days_since_order = (time() - $order_date->getTimestamp()) / (60 * 60 * 24);
762
763 $captured = intval($order->get_meta('_vipps_captured'));
764 $vippsstatus = $order->get_meta('_vipps_status');
765
766 // If we have captured some funds, we must first create a refund for the amount we've captured.
767 // However, we will only do this if the order is relatively fresh, to avoid accidentally refunding
768 // old orders.
769 if ($captured > 0 || $vippsstatus == 'SALE') {
770 // If this is true then the order is *too old to refund* which would happen on maybe_cancel_payment.
771 // add a note instead.
772 if ($days_since_order > $days_threshold) {
773 $note = sprintf(__('Order with captured funds older than %d days cancelled - because the order is this old, it will not be automatically refunded at Vipps. Manual refund may be required.', 'woo-vipps'), $days_threshold);
774 $order->add_order_note($note);
775 // Add an admin notice in case this is interactive
776 $msg = sprintf(__("Could not cancel %1\$s payment", 'woo-vipps'), $this->get_payment_method_name());
777 $this->adminerr(__('Order', 'woo-vipps') . " " . $order->get_id() . ": " . $note);
778 $order->save();
779 Vipps::instance()->store_admin_notices();
780 return false;
781 }
782 // This will create + process a refund for the captured amount. IOK 2026-01-26
783 $this->wc_order_fully_refunded ($order_id);
784 }
785 // In any case, note that this order is ready for cancellation - we don't actually do this here anymore
786 $order->update_meta_data('_vipps_capture_complete',true);
787 $order->save();
788 }
789
790 // IOK 2024-09-01 In general, we can refund most Vipps Mobilepay orders through the api,
791 // however, this is not the case for the Bank Transfer method available through Checkout.
792 public function can_refund_order( $order ) {
793 $method = $order->get_meta('_vipps_api');
794 switch ($method) {
795 case 'banktransfer':
796 return false;
797 break;
798 case 'epayment':
799 return true;
800 break;
801 // Default is true; but the above are exhaustive IOK 2026-08-18
802 default:
803 return true;
804 break;
805 }
806 }
807
808 // This is ran in woocommerce_order_status_refunded *before* woos own wc_order_fully_refunded (priority 9)
809 // so that we can create a through-the-gateway refund for this if neccessary. That way, *our* logic for refunds occur
810 // instead of the normal woo logic. IOK 2026-04-16
811 public function wc_order_fully_refunded ($orderid) {
812 $order = wc_get_order($orderid);
813 if (! Vipps::is_vipps_order($order)) return false;
814
815 // First check to see if we actually need to refund anything now IOK 2026-02-16
816 $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() );
817 if ( ! $max_refund ) {
818 return;
819 }
820
821 // IOK 2019-10-03 it is now possible to do capture via other tools than Woo, so we must now first check to see if
822 // the order is capturable by getting full payment details.
823 try {
824 $order = $this->update_vipps_payment_details($order);
825 } catch (Exception $e) {
826 //Do nothing with this for now
827 $this->log(__("Error getting payment details before doing refund: ", 'woo-vipps') . $e->getMessage(), 'warning');
828 }
829
830 $data = [];
831 $data['amount'] = $max_refund;
832 $data['reason'] = __( 'Order fully refunded.', 'woocommerce' );
833 $data['order_id'] = $orderid;
834 $data['refund_payment'] = true; // This should call our "process_refund" instead of adding a manual refund
835
836 // IOK 2026-04-17 Should be all remaining un-refunded line items
837 $line_items = apply_filters('woo_vipps_order_fully_refunded_line_items', $this->get_remaining_refundable_line_items($order), $order);
838 $data['line_items'] = $line_items;
839
840 wc_switch_to_site_locale();
841 $the_refund = wc_create_refund($data);
842 wc_restore_locale();
843 if (is_wp_error($the_refund)) {
844 $msg = $the_refund->get_error_message();
845 $order->add_order_note(sprintf(__("Error when refunding payment through %1\$s:", 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $msg);
846 $order->save();
847 $this->adminerr($msg);
848 }
849 return true;
850 }
851
852
853 // IOK 2026-04-16 Build wc_create_refund() line_items for all remaining refundable order items - used to provide line-item info for the refund to be processed when
854 // "fully refunded" is done.
855 // [ $item_id => ['qty' => 1, 'refund_total' => '100.00', 'refund_tax' => [ 1 => '25.00' ], * ], ... ]
856 // Includes line items, fees and shipping. For fees/shipping, qty is set to 0.
857 private function get_remaining_refundable_line_items( WC_Order $order ) {
858 $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ));
859
860 $new_refund_line_items = [];
861 foreach($items as $item_id => $item) {
862 $new_refund_line = [];
863
864 // Calculate remaining tax for this line item; by tax id. The shape is [ [total] => [tax_id => value_for_this_tax_id] ].
865 // We want the tax id and the value.
866 $tax_data = wc_tax_enabled() ? $item->get_taxes() : false;
867 $remaining_tax = [];
868 if ($tax_data) {
869 foreach($tax_data['total'] as $tax_id => $value) {
870 if ('' === $value) continue; // don't add empty string as tax value, fatal crash. LP 2026-06-08
871 $remaining_tax[$tax_id] = $value;
872 }
873 }
874 // We can then subtract the tax already refunded for each of these items.
875 foreach($remaining_tax as $tax_id => $current) {
876 $refunded = $order->get_tax_refunded_for_item($item_id, $tax_id, $item->get_type());
877 $remaining = wc_format_decimal($current-$refunded);
878 if ($remaining > 0) {
879 $remaining_tax[$tax_id] = wc_format_decimal($current-$refunded);
880 } else {
881 unset($remaining_tax[$tax_id]);
882 }
883 }
884
885 // Then the quantity
886 $qty = (int) $item->get_quantity();
887 $refunded_quantity = abs((int) $order->get_qty_refunded_for_item($item_id, $item->get_type())); // Documented to be positive since 3.0, seems to be actually negative.
888 $remaining_quantity = $qty-$refunded_quantity;
889
890 $total = $item->get_total();
891 $refunded_total = $order->get_total_refunded_for_item($item_id, $item->get_type()); // A positive value
892 $remaining_total = wc_format_decimal($total-$refunded_total);
893
894
895 if ($remaining_quantity>0 || !empty($remaining_tax) || $remaining_total > 0) {
896 $new_refund_line['qty'] = max(0,$remaining_quantity);
897 $new_refund_line['refund_tax'] = $remaining_tax;
898 $new_refund_line['refund_total'] = $remaining_total;
899 $new_refund_line_items[$item_id] = $new_refund_line;
900 }
901
902 }
903
904 return $new_refund_line_items;
905
906 }
907
908
909 // This is for orders that are 'reserved' at Vipps but could actually be captured at once because
910 // they don't require payment. So we try to capture. IOK 2020-09-22
911 // do NOT call this unless the order is 'reserved' at Vipps!
912 protected function maybe_complete_payment($order) {
913 if (! Vipps::is_vipps_order($order)) return false;
914 if ($order->needs_processing()) return false; // No auto-capture for orders needing processing
915 // IOK 2018-10-03 when implementing partial capture, this must be modified.
916 $captured = intval($order->get_meta('_vipps_captured'));
917 $vippsstatus = $order->get_meta('_vipps_status');
918 if ($captured || $vippsstatus == 'SALE') {
919 return true;
920 }
921 $ok = 0;
922 try {
923 $ok = $this->capture_payment($order);
924 $order->add_order_note(sprintf(__('Payment automatically captured at %1$s for order not needing processing','woo_vipps'), $this->get_payment_method_name()));
925 } catch (Exception $e) {
926 $order->add_order_note(sprintf(__('Order does not need processing, but payment could not be captured at %1$s:','woo_vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage());
927 }
928 if (!$ok) return false;
929 $order->save();
930 return true;
931 }
932
933 // This filter runs on *all* refunds, including manual refunds. Its main job here is to
934 // disallow creating manual refunds on orders that have not been captured yet, since this makes the capture/cancel logic
935 // very confusing
936 public function woocommerce_create_refund ($refund, $args) {
937 $order_id = intval($args['order_id'] ?? 0);
938 $order = $order_id ? wc_get_order( $order_id ) : null;
939 if ( ! $order ) return;
940 if (! Vipps::is_vipps_order($order)) return;
941 // This is for manual refunds only IOK 2026-02-24
942 if (!($args['refund_payment'] ?? false)) {
943 try {
944 $order = $this->update_vipps_payment_details($order);
945 } catch (Exception $e) {
946 //Do nothing with this for now
947 $this->log(__("Error getting payment details before doing refund: ", 'woo-vipps') . $e->getMessage(), 'warning');
948 }
949 // This would be *per order line* in the fulfilment branch. IOK 2026-02-24 FIXME
950 $captured = intval($order->get_meta('_vipps_captured'));
951 if (!$captured) {
952 $msg = sprintf(__("Order %2\$d: It is not possible to create a manual refund for a %1\$s order before it has been captured - doing so makes it impossible to track how much to capture and how much to release. If you create the refund through %1\$s, a note will be made internally so that the amount to be refunded will *not* be captured - please do this instead if possible. Otherwise, capture the order then refund either manually or through %1\$s", 'woo-vipps'), $this->get_payment_method_name(), $order_id);
953 throw new Exception($msg);
954 }
955 }
956 }
957
958 // This is the Woocommerce refund api called by the "Refund" actions. IOK 2018-05-11
959 public function process_refund($orderid,$amount=null,$reason='') {
960 $order = wc_get_order($orderid);
961
962 $currency = $order->get_currency();
963
964 try {
965 $order = $this->update_vipps_payment_details($order);
966 } catch (Exception $e) {
967 //Do nothing with this for now
968 $this->log(__("Error getting payment details before doing refund: ", 'woo-vipps') . $e->getMessage(), 'warning');
969 }
970
971 $captured = intval($order->get_meta('_vipps_captured'));
972 $to_refund = intval($order->get_meta('_vipps_refund_remaining'));
973
974 // No funds captured, by epayment can do cancel of partial capture, so let's note that we are not to capture this.
975 if (!$captured) {
976 if ('epayment' != $order->get_meta('_vipps_api')) {
977 return new WP_Error('Vipps', sprintf(__("Cannot refund through %1\$s - the payment has not been captured yet.", 'woo-vipps'), $this->get_payment_method_name()));
978 }
979 if ($amount > $order->get_total()) {
980 return new WP_Error('Vipps', sprintf(__("Cannot refund through %1\$s - the refund amount is too large.", 'woo-vipps'), $this->get_payment_method_name()));
981 }
982 $msg = sprintf(__('The money for order %1$d has not been captured, only reserved. %2$s %3$s of the reserved funds will be released when the order is set to complete.', 'woo-vipps'), $orderid, $amount, $currency);
983 $this->log($msg, 'info');
984 $uncapturable = round($amount * 100) + intval($order->get_meta('_vipps_noncapturable'));
985
986 $order->update_meta_data('_vipps_noncapturable', $uncapturable);
987 $order->add_order_note($msg);
988 $order->save();
989 return true;
990 }
991
992 if ($amount*100 > $to_refund) {
993 return new WP_Error('Vipps', sprintf(__("Cannot refund through %1\$s - the refund amount is too large.", 'woo-vipps'), $this->get_payment_method_name()));
994 }
995 $ok = 0;
996
997 // Specialcase zero, because Vipps treats this as the entire amount IOK 2021-09-14
998 if (is_numeric($amount) && $amount == 0) {
999 $order->add_order_note($amount . ' ' . $currency . ' ' . sprintf(__(" refunded through %1\$s:",'woo-vipps'), Vipps::CompanyName()) . ' ' . $reason);
1000 return true;
1001 }
1002
1003 try {
1004 $ok = $this->refund_payment($order,$amount);
1005 } catch (TemporaryVippsApiException $e) {
1006 $this->log(sprintf(__('Could not refund %1$s payment for order id:', 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $orderid . "\n" .$e->getMessage(),'error');
1007 return new WP_Error('Vipps',sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), Vipps::CompanyName()) . ' ' . $e->getMessage());
1008 } catch (Exception $e) {
1009 $msg = sprintf(__('Could not refund %1$s payment','woo-vipps'), Vipps::CompanyName()) . ' ' . $e->getMessage();
1010 $order->add_order_note($msg);
1011 $this->log($msg,'error');
1012 return new WP_Error('Vipps',$msg);
1013 }
1014
1015 if ($ok) {
1016 $order->add_order_note($amount . ' ' . $currency . ' ' . sprintf(__(" refunded through %1\$s:",'woo-vipps'), Vipps::CompanyName()) . ' ' . $reason);
1017 }
1018 return $ok;
1019 }
1020
1021 // Detect default payment method based on country code NT-2024-10-15
1022 public function detect_default_payment_method_from_country($country_code) {
1023 // Default to MobilePay
1024 $payment_method_name = 'MobilePay';
1025 // Default to Vipps if country is Norway or Sweden
1026 if($country_code == 'NO' || $country_code == 'SE') {
1027 $payment_method_name = 'Vipps';
1028 }
1029 return $payment_method_name;
1030 }
1031
1032 // Returns true iff this is a store where Vipps will allow external payment methods.
1033 // Currently this is only Finland, and only Klarna is supported. We need to call this like so because
1034 // most of woocommerce will not be initialized when we need this info. IOK 2024-05-28
1035 public function allow_external_payments_in_checkout() {
1036 $store_location= wc_get_base_location();
1037 $store_country = $store_location['country'] ?? '';
1038 $finland = (get_woocommerce_currency() == "EUR" && $store_country == "FI");
1039 $norway = (get_woocommerce_currency() == "NOK" && $store_country == "NO");
1040 $sweden = (get_woocommerce_currency() == "SEK" && $store_country == "SE");
1041 return apply_filters('woo_vipps_allow_external_payment_methods', ($finland || $norway || $sweden));
1042 }
1043
1044 public function init_form_fields() {
1045 global $Vipps;
1046
1047 // Used for defaults in the admin interface; however this functions is called a loot more often than that.
1048 $page_templates = $this->get_theme_page_templates();
1049
1050 $orderprefix = $Vipps->generate_order_prefix();
1051
1052 // Default handling based on other parameters and earlier values.
1053 $expresscreateuserdefault = "no";
1054 $vippscreateuserdefault = "no";
1055
1056 // Express checkout uses verified email addresses,so we'll create users if the Login plugin is installed and WooCommerce is set to allow user registration.
1057 if (class_exists('VippsWooLogin')) {
1058 $woodefault = 'yes' === get_option('woocommerce_enable_signup_and_login_from_checkout');
1059 if ($woodefault) {
1060 $expresscreateuserdefault = "yes";
1061 // $vippscreateuserdefault = "yes"; // However, for Checkout the email address is freetext so we'll treat the default a bit different.
1062 }
1063 }
1064
1065 // We will only show the Checkout options if the user has activated the feature (thus creating the pages involved etc). IOK 2021-10-01
1066 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1067
1068 // This is used for new options,to set reasonable defaults based on older settings. We can't use WC_Settings->get_option for this unfortunately.
1069 $current = get_option('woocommerce_vipps_settings');
1070 // New defaults based on old defaults
1071 $default_static_shipping_for_checkout = 'no';
1072 $default_ask_address_for_express = 'no';
1073 $default_status_on_fail = 'failed';
1074 $default_express_show_in_checkout = 'yes';
1075 if ($current) {
1076 $default_static_shipping_for_checkout = (isset($current['enablestaticshipping'])) ? $current['enablestaticshipping'] : 'no';
1077 $default_ask_address_for_express = (isset($current['useExplicitCheckoutFlow']) && $current['useExplicitCheckoutFlow'] == "yes") ? "yes" : "no";
1078 // The old default used the same value as for Express Checkout. IOK 2023-07-27
1079 $vippscreateuserdefault = isset($current['expresscreateuser']) ? $current['expresscreateuser'] : $vippscreateuserdefault;
1080
1081 // For existing installs: set failed payments order status to cancelled to keep same default behaviour.
1082 // New installs will be set to failed instead of cancelled. LP 2026-03-26
1083 $default_status_on_fail = 'cancelled';
1084
1085 // New setting 'express_show_in_checkout', previously 'cartexpress' affected both cart and checkout.
1086 // Therefore, set new one equal to 'cartexpress' IF it isn't set yet, so that the functionality stays the same for users. LP 2026-07-02
1087 $default_express_show_in_checkout = 'yes';
1088 if (!isset($current['express_show_in_checkout']) && isset($current['cartexpress'])) {
1089 $default_express_show_in_checkout = $current['cartexpress'];
1090 }
1091 }
1092
1093 // Get the already-set country code. For existing sites, this will guess the country based on the currency; for new sites, use
1094 // the woo base country. IOK 2024-10-17 (previously used the currency here too).
1095 $countries = new WC_Countries(); // Can't use WC()->countries here - too early IOK 2024-10-17
1096 $country_code = $current['country'] ?? $countries->get_base_country();
1097
1098 // Same issue as above: We need the default payment method name before it is set to be able to provide defaults IOK 2023-12-01
1099 $payment_method_name = $current['payment_method_name'] ?? $this->detect_default_payment_method_from_country($country_code);
1100
1101 $checkoutfields = array(
1102 'checkout_options' => array(
1103 'title' => "Checkout", // Vipps::CheckoutName(), // Don't translate this, but save some space IOK 2024-12-06
1104 'type' => 'title',
1105 'class' => 'tab',
1106 'description' => sprintf(__("%1\$s is a new service from %2\$s which replaces the usual WooCommerce checkout page entirely, replacing it with a simplified checkout screen providing payment both with %2\$s and credit card. Additionally, your customers will get the option of providing their address information using their %2\$s app directly.", 'woo-vipps'), Vipps::CheckoutName(), Vipps::CompanyName()),
1107 ),
1108
1109
1110 'vipps_checkout_enabled' => array(
1111 'title' => sprintf(__('Activate Alternative %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1112 'label' => sprintf(__('Enable Alternative %1$s screen, replacing the standard checkout page', 'woo-vipps'), Vipps::CheckoutName()),
1113 'type' => 'checkbox',
1114 'description' => sprintf(__('If activated, this will <strong>replace</strong> the standard Woo checkout screen with %1$s, providing easy checkout using %1$s or credit card, with no need to type in addresses.', 'woo-vipps'), Vipps::CheckoutName()),
1115 'default' => 'no',
1116 ),
1117
1118 'checkoutcreateuser' => array (
1119 'title' => sprintf(__('Create new customers on %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1120 'label' => sprintf(__('Create new customers on %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1121 'type' => 'checkbox',
1122 'description' => sprintf(__('Enable this to create and login customers when using %1$s. Otherwise these will all be guest checkouts. If using, you may want to install Login with Vipps too.', 'woo-vipps'), Vipps::CheckoutName()),
1123 'default' => $vippscreateuserdefault,
1124 ),
1125
1126 'enablestaticshipping_checkout' => array(
1127 'title' => sprintf(__('Enable static shipping for %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1128 'label' => __('Enable static shipping', 'woo-vipps'),
1129 'type' => 'checkbox',
1130 'description' => sprintf(__('If your shipping options do not depend on the customers address, you can enable \'Static shipping\', which will precompute the shipping options when using %1$s so that this will be much faster. If you do this and the customer isn\'t logged in, the base location of the store will be used to compute the shipping options for the order. You should only use this if your shipping is actually \'static\', that is, does not vary based on the customers address. So fixed price/free shipping will work. If the customer is logged in, their address as registered in the store will be used, so if your customers are always logged in, you may be able to use this too.', 'woo-vipps'), Vipps::CheckoutName()),
1131 'default' => $default_static_shipping_for_checkout
1132 ),
1133
1134
1135 'requireUserInfo_checkout' => array(
1136 'title' => __('Ask the user to consent to share user information', 'woo-vipps'),
1137 'label' => __('Ask the user to consent to share user information', 'woo-vipps'),
1138 'type' => 'checkbox',
1139 'description' => sprintf(__('If using %1$s, ask for the users consent to share user information with the store. This will allow better integration between Login With %1$s but will add another step to first-time buyers.', 'woo-vipps'), Vipps::CompanyName()),
1140 'default' => 'no'
1141 ),
1142
1143 'noAddressFields' => array(
1144 'title' => __('Drop the address fields on the Checkout screen', 'woo-vipps'),
1145 'label' => __('Don\'t require the address fields', 'woo-vipps'),
1146 'type' => 'checkbox',
1147 'description' => __('If your products <i>don\'t require shipping</i>, either because they are digital downloads, immaterial products or delivering the products directly on purchase, you can check this box. The user will then not be required to provide an address, which should speed things up a bit. If your products require shipping, this will have no effect. NB: If you have plugins that require shipping information, then this is not going to work very well.','woo-vipps'),
1148 'default' => 'no'
1149 ),
1150
1151 'noContactFields' => array(
1152 'title' => __('Drop the contact fields on the Checkout screen', 'woo-vipps'),
1153 'label' => __('Don\'t require the contact fields', 'woo-vipps'),
1154 'type' => 'checkbox',
1155 'description' => __('If your products <i>don\'t require shipping</i> as above, and you also don\'t care about the customers name or contact information, you can drop this too! The customer fields will then be filled with a placeholder. NB: If you have plugins that require contact information, then this is not going to work very well. Also, for this to work you have to check the \'no addresses\' box as well.','woo-vipps'),
1156 'default' => 'no'
1157 ),
1158
1159
1160 );
1161
1162 $vipps_checkout_shipping_fields = array(
1163
1164 'checkout_shipping' => array(
1165 'title' => sprintf(__('%1$s Shipping Methods', 'woo-vipps'), Vipps::CheckoutName()),
1166 'type' => 'title',
1167 'description' => sprintf(__("When using %1\$s, you have the option to use %1\$s specific shipping methods with extended features for certain carriers. These will add an apropriate logo as well as extended delivery options for certain methods. For some of these, you need to add integration data from the carriers below. You can then add these shipping methods to your shipping zones the normal way, but they will only appear in the %1\$s screen.", 'woo-vipps'), Vipps::CheckoutName())
1168 ),
1169
1170 'vcs_posten' => array(
1171 'title' => __('Posten Norge', 'woo-vipps'),
1172 'class' => 'vcs_posten vcs_main',
1173 'custom_attributes' => array('data-vcs-show'=>'.vcs_depend.vcs_posten'),
1174 'label' => sprintf(__('Support Posten Norge as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1175 'type' => 'checkbox',
1176 'description' => sprintf(__('Activate this for Posten Norge as a %1$s Shipping method.', 'woo-vipps'), Vipps::CheckoutName()),
1177 'default' => 'yes'
1178 ),
1179
1180 'vcs_posti' => array(
1181 'title' => __('Posti', 'woo-vipps'),
1182 'class' => 'vcs_posti vcs_main',
1183 'custom_attributes' => array('data-vcs-show'=>'.vcs_depend.vcs_posti'),
1184 'label' => sprintf(__('Support Posti as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1185 'type' => 'checkbox',
1186 'description' => sprintf(__('Activate this for Posti as a %1$s Shipping method.', 'woo-vipps'), Vipps::CheckoutName()),
1187 'default' => 'yes'
1188 ),
1189
1190 'vcs_postnord' => array(
1191 'title' => __('PostNord', 'woo-vipps'),
1192 'class' => 'vcs_postnord vcs_main',
1193 'custom_attributes' => array('data-vcs-show'=>'.vcs_depend.vcs_postnord'),
1194 'label' => sprintf(__('Support PostenNord as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1195 'type' => 'checkbox',
1196 'description' => sprintf(__('Activate this for PostNord as a %1$s Shipping method.', 'woo-vipps'), Vipps::CheckoutName()),
1197 'default' => 'yes'
1198 ),
1199
1200 'vcs_porterbuddy' => array(
1201 'title' => __('Porterbuddy', 'woo-vipps'),
1202 'class' => 'vcs_porterbuddy vcs_main',
1203 'custom_attributes' => array('data-vcs-show'=>'.vcs_depend.vcs_porterbuddy'),
1204 'label' => sprintf(__('Support Porterbuddy as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1205 'type' => 'checkbox',
1206 'description' => sprintf(__('Activate this for Porterbuddy as a %1$s Shipping method. Your store address will be used as the pick-up point and your admin email will be used for booking information from Porterbuddy.' ,'woo-vipps'), Vipps::CheckoutName()),
1207 'default' => 'no'
1208 ),
1209
1210 'vcs_porterbuddy_publicToken' => array(
1211 'title' => __('Porterbuddy public token', 'woo-vipps'),
1212 'class' => 'vippspw vcs_porterbuddy vcs_depend',
1213 'type' => 'password',
1214 'description' => __('The public key provided to you by Porterbuddy','woo-vipps'),
1215 'default' => '',
1216 ),
1217 'vcs_porterbuddy_apiKey' => array(
1218 'title' => __('Porterbuddy API key', 'woo-vipps'),
1219 'class' => 'vippspw vcs_porterbuddy vcs_depend',
1220 'type' => 'password',
1221 'description' => __('The API key provided to you by Porterbuddy','woo-vipps'),
1222 'default' => '',
1223 ),
1224 'vcs_porterbuddy_phoneNumber' => array(
1225 'title' => __('Porterbuddy Phone Number', 'woo-vipps'),
1226 'class' => 'vcs_porterbuddy vcs_depend',
1227 'type' => 'text',
1228 'description' => __('Your phone number where Porterbuddy may send you important messages. Format must be MSISDN (including country code). Example: "4791234567"','woo-vipps'),
1229 'default' => '',
1230 ),
1231
1232 // Vipps checkout *shipping options* - extra shipping options that only work with Checkout
1233 'vcs_helthjem' => array(
1234 'title' => __('Helthjem', 'woo-vipps'),
1235 'label' => sprintf(__('Support Helthjem as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1236 'type' => 'checkbox',
1237 'class' => 'vcs_helthjem vcs_main',
1238 'custom_attributes' => array('data-vcs-show'=>'.vcs_depend.vcs_helthjem'),
1239 'description' => sprintf(__('Activate this for Helthjem as a %1$s Shipping method.' ,'woo-vipps'), Vipps::CheckoutName()),
1240 'default' => 'no'
1241 ),
1242
1243 'vcs_helthjem_shopId' => array(
1244 'title' => __('Helthjem Shop Id', 'woo-vipps'),
1245 'class' => 'vcs_helthjem vcs_depend',
1246 'type' => 'number',
1247 'custom_attributes' => array('pattern'=>'[0-9]'),
1248 'description' => __('The ShopId provided to you by Helthjem','woo-vipps'),
1249 'default' => '',
1250 ),
1251
1252 'vcs_helthjem_username' => array(
1253 'title' => __('Helthjem Username', 'woo-vipps'),
1254 'class' => 'vcs_helthjem vcs_depend',
1255 'type' => 'text',
1256 'description' => __('The Username provided to you by Helthjem','woo-vipps'),
1257 'default' => '',
1258 ),
1259 'vcs_helthjem_password' => array(
1260 'title' => __('Helthjem Password', 'woo-vipps'),
1261 'class' => 'vippspw vcs_helthjem vcs_depend',
1262 'type' => 'password',
1263 'description' => __('Password provided to you by Helthjem','woo-vipps'),
1264 'default' => '',
1265 ),
1266
1267 );
1268
1269 /* Support for *certain* external payment methods in Checkout. IOK 2024-05-27 */
1270 $externals = [];
1271 $external_payment_fields = [];
1272 $allow_external_payments = $this->allow_external_payments_in_checkout();
1273 if ($allow_external_payments) {
1274 if (in_array('KCO_Gateway', Vipps::$installed_gateways) || in_array('WC_Gateway_Klarna_Payments', Vipps::$installed_gateways)) {
1275 $externals['checkout_external_payments_klarna'] = array(
1276 'title' => __('Klarna', 'woo-vipps'),
1277 'label' => __('Klarna', 'woo-vipps'),
1278 'type' => 'checkbox',
1279 'class' => 'external_payments klarna',
1280 'description' => sprintf(__("Allow Klarna as an external payment method in %1\$s",'woo-vipps'), Vipps::CheckoutName()),
1281 'default' => 'no',
1282 );
1283 }
1284 if (!empty($externals)) {
1285 $external_payment_fields = [
1286 'checkout_external_payment_title' => array(
1287 'title' => sprintf(__('External Payment Methods', 'woo-vipps'), Vipps::CheckoutName()),
1288 'type' => 'title',
1289 'description' => sprintf(__("Allow certain external payment methods in %1\$s, returning control to WooCommerce for the order", 'woo-vipps'), Vipps::CheckoutName())
1290 ),
1291 ];
1292 foreach($externals as $k => $def) $external_payment_fields[$k] = $def;
1293 }
1294 }
1295
1296 $vipps_checkout_widgets_fields = [
1297 'checkout_widgets' => [
1298 'title' => sprintf(__('%1$s widgets', 'woo-vipps'), Vipps::CheckoutName()),
1299 'type' => 'title',
1300 'description' => sprintf(__('Widgets are elements shown above the %1$s frame with extra functionality.', 'woo-vipps'), Vipps::CheckoutName()),
1301 ],
1302 'checkout_widget_ordernotes' => [
1303 'title' => __('Order notes', 'woo-vipps'),
1304 'label' => __('Enable the order notes widget', 'woo-vipps'),
1305 'type' => 'checkbox',
1306 'description' => __('A widget to add customer notes with their order.', 'woo-vipps'),
1307 'default' => 'yes'
1308 ],
1309 ];
1310 /* This seems to have broken some sites with OOM during plugin activation. 2025-05-27 */
1311 // if (wc_coupons_enabled()) {
1312 $vipps_checkout_widgets_fields['checkout_widget_coupon'] = [
1313 'title' => __('Coupon code', 'woo-vipps'),
1314 'label' => __('Enable the coupon code widget', 'woo-vipps'),
1315 'type' => 'checkbox',
1316 'description' => __('A widget to activate coupon codes.', 'woo-vipps'),
1317 'default' => 'yes'
1318 ];
1319 // }
1320
1321 $mainfields = array(
1322 'main_options' => array(
1323 'title' => __('Main options', 'woo-vipps'),
1324 'type' => 'title',
1325 'class' => 'tab',
1326 ),
1327 'enabled' => array(
1328 'title' => __('Enable/Disable', 'woocommerce'),
1329 'label' => sprintf(__('Enable %1$s', 'woo-vipps'), Vipps::CompanyName()),
1330 'type' => 'checkbox',
1331 'description' => '',
1332 'default' => 'no',
1333 ),
1334 'country' => array(
1335 'title' => __('Country', 'woo-vipps'),
1336 'label' => __('Country', 'woo-vipps'),
1337 'type' => 'select',
1338 'options' => array(
1339 'NO' => __('Norway', 'woo-vipps'),
1340 'SE' => __('Sweden', 'woo-vipps'),
1341 'FI' => __('Finland', 'woo-vipps'),
1342 'DK' => __('Denmark', 'woo-vipps'),
1343 ),
1344 'description' => __('Select the country for this merchant serial number. This will determine the appropriate payment method (Vipps or MobilePay).', 'woo-vipps'),
1345 'default' => $country_code,
1346 ),
1347
1348 'payment_method_name' => array(
1349 'title' => __('Payment method', 'woo-vipps'),
1350 'label' => __('Choose which payment method should be displayed to users at checkout', 'woo-vipps'),
1351 'type' => 'select',
1352 'options' => array(
1353 'Vipps' => __('Vipps','woo-vipps'),
1354 'MobilePay' => __('MobilePay', 'woo-vipps'),
1355 ),
1356 'description' => __('Choose which payment method should be displayed to users at checkout', 'woo-vipps'),
1357 'default' => $payment_method_name,
1358 ),
1359
1360 'orderprefix' => array(
1361 'title' => __('Order-id Prefix', 'woo-vipps'),
1362 'label' => __('Order-id Prefix', 'woo-vipps'),
1363 'type' => 'string',
1364 'description' => __('An alphanumeric textstring to use as a prefix on orders from your shop, to avoid duplicate order-ids','woo-vipps'),
1365 'default' => $orderprefix
1366 ),
1367 'merchantSerialNumber' => array(
1368 'title' => __('Merchant Serial Number', 'woo-vipps'),
1369 'label' => __('Merchant Serial Number', 'woo-vipps'),
1370 'type' => 'number',
1371 'description' => __('Your "Merchant Serial Number" from the Developer tab on https://portal.vippsmobilepay.com','woo-vipps'),
1372 'default' => '',
1373 ),
1374 'clientId' => array(
1375 'title' => __('Client Id', 'woo-vipps'),
1376 'class' => 'vippspw',
1377 'label' => __('Client Id', 'woo-vipps'),
1378 'type' => 'password',
1379 'description' => __('Find your account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "Show keys". Copy the value of "client_id"','woo-vipps'),
1380 'default' => '',
1381 ),
1382 'secret' => array(
1383 'title' => __('Client Secret', 'woo-vipps'),
1384 'label' => __('Client Secret', 'woo-vipps'),
1385 'class' => 'vippspw',
1386 'type' => 'password',
1387 'description' => __('Find your account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "show keys". Copy the value of "client_secret"','woo-vipps'),
1388 'default' => '',
1389 ),
1390 'Ocp_Apim_Key_eCommerce' => array(
1391 'title' => __('Subscription Key', 'woo-vipps'),
1392 'label' => __('Subscription Key', 'woo-vipps'),
1393 'class' => 'vippspw',
1394 'type' => 'password',
1395 'description' => __('Find your account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "show keys". Copy the value of "Vipps-Subscription-Key"','woo-vipps'),
1396 'default' => '',
1397 ),
1398
1399 'result_status' => array(
1400 'title' => sprintf(__('Order status on return from %1$s', 'woo-vipps'), Vipps::CompanyName()),
1401 'label' => __('Choose default order status for reserved (not captured) orders', 'woo-vipps'),
1402 'type' => 'select',
1403 'options' => array(
1404 'processing' => __('Processing', 'woo-vipps'),
1405 'on-hold' => __('On hold','woo-vipps'),
1406 ),
1407 'description' => __('By default, orders that are <b>reserved</b> but not <b>yet captured</b> will now have the order status \'Processing\'. You can capture the sum manually, or by changing the status to \'Complete\'. You should ensure that your workflow is such that the order is not shipped until after this capture.<br><br> The status \'On hold\' can be chosen instead for stores using a workflow where orders are shipped when the status is \'Processing\'. In this case, \'On hold\' will mean "order is reserved but not yet captured". This is a slightly safer solution, and ensures that the order status will reflect the payment status. <br><br>However, in many stores \'On hold\' has the additional meaning "there is a problem with the order"; and an email is often sent to the customer about this problem. The default is \'Processing\' because of this, and because many plugins and integrations expect orders to be \'Processing\' when the customer has completed payment.', 'woo-vipps'),
1408 'default' => 'processing',
1409 ),
1410
1411 'status_on_fail' => array(
1412 'title' => sprintf(__('Order status on failed payment (for restartable orders)', 'woo-vipps'), Vipps::CompanyName()),
1413 'label' => __('Choose default order status for failed payments', 'woo-vipps'),
1414 'type' => 'select',
1415 'options' => array(
1416 /* translators: woocommerce status name */
1417 'failed' => __('Failed', 'woo-vipps'),
1418 /* translators: woocommerce status name */
1419 'cancelled' => __('Cancelled','woo-vipps'),
1420 ),
1421 /* translators: company name. cancelled and failed are woocommerce status names! */
1422 'description' => sprintf(__('By default, orders where payment is started but not completed at %1$s will be set to failed if they can be restarted. This setting changes this behaviour, but does <strong>not</strong> affect orders that cannot be restarted as these will always be set to cancelled.<br><br>Cancelled orders will keep the customer\'s shopping cart intact.<br>Failed orders can be restarted, possibly with another payment method.', 'woo-vipps'), Vipps::CompanyName()),
1423 'default' => $default_status_on_fail,
1424 ),
1425
1426 /*
1427 'title' => array(
1428 'title' => __('Title', 'woocommerce'),
1429 'type' => 'text',
1430 'description' => __('This controls the title which the user sees during checkout.', 'woocommerce'),
1431 'default' => sprintf(__('%1$s','woo-vipps'), $payment_method_name),
1432 ),
1433 */
1434
1435 'description' => array(
1436 'title' => __('Description', 'woocommerce'),
1437 'type' => 'textarea',
1438 'description' => __('This controls the description which the user sees during checkout.', 'woocommerce'),
1439 'default' => __("Pay safely and easily. No fees, no matter the amount.", 'woo-vipps'),
1440 ),
1441
1442 'vippsdefault' => array(
1443 'title' => sprintf(__('Use %1$s as default payment method on checkout page', 'woo-vipps'), $payment_method_name),
1444 'label' => sprintf(__('%1$s is default payment method', 'woo-vipps'), $payment_method_name),
1445 'type' => 'checkbox',
1446 'description' => sprintf(__('Enable this to use %1$s as the default payment method on the checkout page, regardless of order.', 'woo-vipps'), $payment_method_name),
1447 'default' => 'yes',
1448 ),
1449
1450 'checkout_phone_transformation' => array(
1451 'title' => sprintf(__('Phone number transformation for %1$s and Express Checkout', 'woo-vipps'), 'Checkout'),
1452 'label' => sprintf(__('Choose a transformation to apply to phone numbers for %1$s and Express Checkout', 'woo-vipps'), 'Checkout'),
1453 'type' => 'select',
1454 'options' => array(
1455 'none' => __('None', 'woo-vipps'),
1456 'ensure_plus' => __('Prepend \'+\'','woo-vipps'),
1457 'strip_country_code' => __('Strip country code','woo-vipps'),
1458 ),
1459 'description' => __('Phone numbers from Express or Checkout are in the format 47xxxxxx without plus-sign in front. If you prefer, or if it is neccessary for your integrations, you can transform these numbers either by adding the plus sign or by stripping the country-code (45, 46, 47, 358).<br>NB: stripping country codes is at the moment only supported for Norwegian, Danish, Finnish and Swedish numbers.<br>Remember to explicitly test your use case before committing to any transformation on your live store.', 'woo-vipps'),
1460 'default' => 'none',
1461 ),
1462 );
1463
1464 $expressfields = array(
1465 'express_options' => array(
1466 'title' => sprintf(__('Express Checkout', 'woo-vipps')),
1467 'type' => 'title',
1468 'class' => 'tab',
1469 'description' => sprintf(__("%1\$s allows you to buy products by a single click from the cart, checkout, or directly from product or catalog pages. Product will get a 'buy now' button which will start the purchase process immediately.", 'woo-vipps'), Vipps::ExpressCheckoutName())
1470 ),
1471
1472 'cartexpress' => array(
1473 'title' => __('Enable Express Checkout in cart', 'woo-vipps'),
1474 'label' => __('Enable Express Checkout in cart', 'woo-vipps'),
1475 'type' => 'checkbox',
1476 'description' => sprintf(__('Enable this to allow customers to shop using %1$s directly from the cart with no login or address input needed', 'woo-vipps'), Vipps::ExpressCheckoutName()) . '.<br>' .
1477 sprintf(__('Please note that for Express Checkout, shipping must be calculated in a callback from the %1$s app, without any knowledge of the customer. This means that Express Checkout may not be compatible with all Shipping plugins or setup. You should test that your setup works if you intend to provide this feature.', 'woo-vipps'), Vipps::CompanyName()),
1478 'default' => 'yes',
1479 ),
1480
1481 'express_show_in_checkout' => array(
1482 'title' => __('Enable Express Checkout in checkout', 'woo-vipps'),
1483 'label' => __('Enable Express Checkout in checkout', 'woo-vipps'),
1484 'type' => 'checkbox',
1485 'description' => sprintf(__('Enable this to allow customers to shop using %1$s directly from the checkout page with no login or address input needed', 'woo-vipps'), Vipps::ExpressCheckoutName()) . '.<br>' .
1486 sprintf(__('Please note that for Express Checkout, shipping must be calculated in a callback from the %1$s app, without any knowledge of the customer. This means that Express Checkout may not be compatible with all Shipping plugins or setup. You should test that your setup works if you intend to provide this feature.', 'woo-vipps'), Vipps::CompanyName()),
1487 'default' => $default_express_show_in_checkout,
1488 ),
1489
1490 'singleproductexpress' => array(
1491 'title' => __('Enable Express Checkout for single products', 'woo-vipps'),
1492 'label' => __('Enable Express Checkout for single products', 'woo-vipps'),
1493 'type' => 'select',
1494 'options' => array(
1495 'none' => __('No products','woo-vipps'),
1496 'some' => __('Some products', 'woo-vipps'),
1497 'all' => __('All products','woo-vipps')
1498 ),
1499 'description' => sprintf(__('Enable this to allow customers to buy a product using %1$s directly from the product page. If you choose \'some\', you must enable this on the relevant products', 'woo-vipps'), Vipps::ExpressCheckoutName()),
1500 'default' => 'none',
1501 ),
1502 'singleproductexpressarchives' => array(
1503 'title' => __('Add \'Buy now\' button on catalog pages too', 'woo-vipps'),
1504 'label' => __('Add the button for all relevant products on catalog pages', 'woo-vipps'),
1505 'type' => 'checkbox',
1506 'description' => sprintf(__('If %1$s is enabled for a product, add the \'Buy now\' button to catalog pages too', 'woo-vipps'), Vipps::ExpressCheckoutName()),
1507 'default' => 'no',
1508 ),
1509 'expresscheckout_termscheckbox' => array(
1510 'title' => sprintf(__('Add terms and conditions checkbox on %1$s', 'woo-vipps'), Vipps::ExpressCheckoutName()),
1511 'label' => sprintf(__('Always ask for confirmation on %1$s', 'woo-vipps'), Vipps::ExpressCheckoutName()),
1512 'type' => 'checkbox',
1513 'description' => sprintf(__('When using %1$s, ask the user to confirm that they have read and accepted the stores terms and conditons before proceeding', 'woo-vipps'), Vipps::ExpressCheckoutName()),
1514 'default' => 'no',
1515 ),
1516
1517 'expresscheckout_always_address' => array(
1518 'title' => __('Always ask for address, even if products don\'t need shipping', 'woo-vipps'),
1519 'label' => __('Always ask the user for their address, even if you don\'t need it for shipping', 'woo-vipps'),
1520 'type' => 'checkbox',
1521 'description' => __('If the order contains only "virtual" products that do not need shipping, we do not normally ask the user for their address - but check this box to do so anyway.', 'woo-vipps'),
1522 'default' => $default_ask_address_for_express,
1523 ),
1524
1525 'enablestaticshipping' => array(
1526 'title' => __('Enable static shipping for Express Checkout', 'woo-vipps'),
1527 'label' => __('Enable static shipping', 'woo-vipps'),
1528 'type' => 'checkbox',
1529 'description' => __('If your shipping options do not depend on the customers address, you can enable \'Static shipping\', which will precompute the shipping options when using Express Checkout so that this will be much faster. If you do this and the customer isn\'t logged in, the base location of the store will be used to compute the shipping options for the order. You should only use this if your shipping is actually \'static\', that is, does not vary based on the customers address. So fixed price/free shipping will work. If the customer is logged in, their address as registered in the store will be used, so if your customers are always logged in, you may be able to use this too.', 'woo-vipps'),
1530 'default' => 'no',
1531 ),
1532
1533
1534 'expresscreateuser' => array (
1535 'title' => __('Create new customers on Express Checkout', 'woo-vipps'),
1536 'label' => __('Create new customers on Express Checkout', 'woo-vipps'),
1537 'type' => 'checkbox',
1538 'description' => sprintf(__('Enable this to create and login new customers when using express checkout. Otherwise these will all be guest checkouts. If you have "Login with Vipps" installed, this will be the default (unless you have turned off user creation in WooCommerce itself)', 'woo-vipps'), Vipps::CompanyName()),
1539 'default' => $expresscreateuserdefault,
1540 ),
1541 'singleproductbuynowcompatmode' => array(
1542 'title' => __('"Buy now" compatibility mode', 'woo-vipps'),
1543 'label' => __('Activate compatibility mode for all "Buy now" buttons', 'woo-vipps'),
1544 'type' => 'checkbox',
1545 'description' => __('Choosing this will use a different method of handling the "Buy now" button on a single product, which will work for more product types and more plugins - while being <i>slightly</i> less smooth. Use this if your product needs more configuration than simple or standard variable products', 'woo-vipps'),
1546 'default' => 'no',
1547 ),
1548
1549
1550 'deletefailedexpressorders' => array(
1551 'title' => __('Delete failed Express Checkout Orders', 'woo-vipps'),
1552 'label' => __('Delete failed Express Checkout Orders', 'woo-vipps'),
1553 'type' => 'checkbox',
1554 'description' => __('As Express Checkout orders are anonymous, failed orders will end up as "cancelled" orders with no information in them. Enable this to delete these automatically when cancelled - but test to make sure no other plugin needs them for anything.', 'woo-vipps'),
1555 'default' => 'no',
1556 )
1557 );
1558 // New shipping in express checkout is available, but merchant has overridden the old shipping callback. Ask what to do! IOK 2020-02-12
1559 if (has_action('woo_vipps_shipping_methods')) {
1560 $shippingoptions = array(
1561 'newshippingcallback' => array(
1562 'title' => __('Use old-style shipping callback for express checkout', 'woo-vipps'),
1563 'label' => __('Use your current shipping filters', 'woo-vipps'),
1564 'type' => 'select',
1565 'options' => array(
1566 'none' => __('Select one','woo-vipps'),
1567 'old' => __('Keep using old shipping callback with my custom filter', 'woo-vipps'),
1568 'new' => __('Use new shipping callback','woo-vipps')
1569 ),
1570 'description' => sprintf(__('Since version 1.4 this plugin uses a new method of providing shipping methods to %1$s when using Express Checkout. The new method supports metadata in the shipping options, which is neccessary for integration with Bring, Postnord etc. However, the new method is not compatible with the old <code>\'woo_vipps_shipping_methods\'</code> filter, which your site has overridden in a theme or plugin. If you want to, you can continue using this filter and the old method. If you want to disable your filters and use the new method, you can choose this here. ', 'woo-vipps'), Vipps::CompanyName()),
1571 'default' => 'none',
1572 )
1573 );
1574 $expressfields = array_merge(array_slice($expressfields ,0,1), $shippingoptions, array_slice($expressfields,1));
1575 }
1576
1577 $advancedfields = array(
1578 'advanced_options' => array(
1579 'title' => __('Advanced', 'woo-vipps'),
1580 'type' => 'title',
1581 'class' => 'tab',
1582 'description' => __("If you have issues with your theme, you might find a setting here that will help. Normally you do not need to change these.", 'woo-vipps')
1583 ),
1584
1585 'vippsorderattribution' => array(
1586 'title' => __( 'Support WooCommerces Order Attribution API for Checkout and Express Checkout', 'woo-vipps' ),
1587 'label' => __( 'Add support for Order Attribution', 'woo-vipps' ),
1588 'type' => 'checkbox',
1589 'default'=> 'no',
1590 'description' => __('Turn this on to add support for Woos Order Attribution API for Checkout and Express Checkout. Some stores have reported problems when using this API together with Vipps, so be sure to test this if you turn it on.', 'woo-vipps'),
1591 ),
1592
1593 'vippsspecialpagetemplate' => array(
1594 'title' => sprintf(__('Legacy: Override page template used for the special %1$s page', 'woo-vipps'), Vipps::CompanyName()),
1595 'label' => sprintf(__('Use specific template for %1$s', 'woo-vipps'), Vipps::CompanyName()),
1596 'type' => 'select',
1597 'options' => $page_templates,
1598 'description' => sprintf(__('Use this template from your theme or child-theme for the special %1$s page.<br>Legacy: This is not necessary anymore - you should instead choose a template by editing the page like any other page.','woo-vipps'), Vipps::CompanyName()),
1599 'default' => ''),
1600
1601 // Deprecated, not shown anymore: TODO: remove this option in future. LP 2026-09-01
1602 'vippsspecialpageid' => array(
1603 'title' => sprintf(__('Use a real page ID for the special %1$s pages - neccessary for some themes', 'woo-vipps'), Vipps::CompanyName()),
1604 'label' => __('Use a real page ID', 'woo-vipps'),
1605 'type' => 'select',
1606 'options' => [],
1607 'description' => sprintf(__('Some very few themes do not work with the simulated pages used by this plugin, and needs a real page ID for this. Choose a blank page for this; the content will be replaced, but the template and other metadata will be present. You only need to use this if the plugin seems to break on the special %1$s pages.', 'woo-vipps'), Vipps::CompanyName()),
1608 'default' => ''
1609 ),
1610
1611 'sendreceipts' => array(
1612 'title' => __("Send receipts and order confirmation info to the customers' app on completed purchases.", 'woo-vipps'),
1613 'label' => sprintf(__("Send receipts to the customers %1\$s app", 'woo-vipps'), Vipps::CompanyName()),
1614 'type' => 'checkbox',
1615 'description' => sprintf(__("If this is checked, a receipt will be sent to %1\$s which will be viewable in the users' app, specifying the order items, shipping et cetera", 'woo-vipps'), Vipps::CompanyName()),
1616 'default' => 'yes'
1617 ),
1618
1619 'receiptimage' => array (
1620 'title' => sprintf(__('Use this image for the order confirmation link uploaded to the customers\' %1$s app', 'woo-vipps'), Vipps::CompanyName()),
1621 'label' => sprintf(__('Profile image used in the %1$s App', 'woo-vipps'), Vipps::CompanyName()),
1622 'type' => 'woo_vipps_image',
1623 'description' => sprintf(__('If set, this image will be uploaded to %1$s and used to profile your store in the %1$s app for links to the order confirmation etc', 'woo-vipps'), Vipps::CompanyName()),
1624 'default' => 0,
1625 ),
1626
1627
1628 'use_flock' => array (
1629 'title' => __('Use flock() to lock orders for Express Checkout', 'woo-vipps'),
1630 'label' => __('Use flock() to lock orders for Express Checkout', 'woo-vipps'),
1631 'type' => 'checkbox',
1632 'description' => __('Use the flock() system call to ensure orders are only finalized once. You can use this for normal setups, but probably not on Windows with IIS, and possibly not on distributed filesystems like NFS. If you don\t know what it is, probably do not use it. If you get duplicated shipping lines on some express orders, you may try using this', 'woo-vipps'),
1633 'default' => 'no',
1634 ),
1635
1636 'delete_settings_on_deactivation' => array (
1637 'title' => __('Delete plugin settings on deactivation', 'woo-vipps'),
1638 'label' => __('Delete plugin settings on deactivation', 'woo-vipps'),
1639 'type' => 'checkbox',
1640 'description' => __('If set, all plugin settings will be deleted upon plugin deactivation. Warning: there is no recovery after deletion.', 'woo-vipps'),
1641 'default' => 'no',
1642 ),
1643
1644 'developermode' => array ( // DEVELOPERS! DEVELOPERS! DEVELOPERS! DEVE
1645 'title' => __('Enable developer mode', 'woo-vipps'),
1646 'label' => __('Enable developer mode', 'woo-vipps'),
1647 'type' => 'checkbox',
1648 'description' => __('Enable this to enter developer mode. This gives you access to the test-api and sometimes other tools not yet ready for general consumption', 'woo-vipps'),
1649 'default' => VIPPS_TEST_MODE ? 'yes' : 'no',
1650 )
1651
1652
1653 );
1654
1655 $developerfields = array(
1656 'developertitle' => array(
1657 'title' => __('Developer mode', 'woo-vipps'),
1658 'type' => 'title',
1659 'class' => 'tab',
1660 'description' => __('These are settings for developers that contain extra features that are normally not useful for regular users, or are not yet ready for primetime', 'woo-vipps'),
1661 ),
1662
1663 'testmode' => array(
1664 'title' => __('Test mode', 'woo-vipps'),
1665 'label' => __('Enable test mode', 'woo-vipps'),
1666 'type' => 'checkbox',
1667 'description' => sprintf(__('If you enable this, transactions will be made towards the %1$s Test API instead of the live one. No real transactions will be performed. You will need to fill out your test
1668 accounts keys below, and you will need to install a special test-mode app from Testflight on a device (which cannot run the regular %1$s app). Contact %1$s\'s technical support if you need this. If you turn this mode off, normal operation will resume. If you have the VIPPS_TEST_MODE defined in your wp-config file, this will override this value. ', 'woo-vipps'), Vipps::CompanyName()),
1669 'default' => VIPPS_TEST_MODE ? 'yes' : 'no',
1670 ),
1671 'merchantSerialNumber_test' => array(
1672 'title' => __('Merchant Serial Number', 'woo-vipps'),
1673 'class' => 'vippspw',
1674 'label' => __('Merchant Serial Number', 'woo-vipps'),
1675 'type' => 'number',
1676 'description' => __('Your test account "Merchant Serial Number" from the Developer tab on https://portal.vippsmobilepay.com','woo-vipps'),
1677 'default' => '',
1678 ),
1679 'clientId_test' => array(
1680 'title' => __('Client Id', 'woo-vipps'),
1681 'label' => __('Client Id', 'woo-vipps'),
1682 'type' => 'password',
1683 'class' => 'vippspw',
1684 'description' => __('Find your test account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "Show keys". Copy the value of "client_id"','woo-vipps'),
1685 'default' => '',
1686 ),
1687 'secret_test' => array(
1688 'title' => __('Client Secret', 'woo-vipps'),
1689 'label' => __('Client Secret', 'woo-vipps'),
1690 'type' => 'password',
1691 'class' => 'vippspw',
1692 'description' => __('Find your test account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "show keys". Copy the value of "client_secret"','woo-vipps'),
1693 'default' => '',
1694 ),
1695 'Ocp_Apim_Key_eCommerce_test' => array(
1696 'title' => __('Subscription Key', 'woo-vipps'),
1697 'label' => __('Subscription Key', 'woo-vipps'),
1698 'type' => 'password',
1699 'class' => 'vippspw',
1700 'description' => __('Find your test account under the "Developer" tab on https://portal.vippsmobilepay.com/ and choose "show keys". Copy the value of "Vipps-Subscription-Key"','woo-vipps'),
1701 'default' => '',
1702 ),
1703 );
1704
1705 // Add all the standard fields
1706 foreach($mainfields as $key=>$field) {
1707 $this->form_fields[$key] = $field;
1708 }
1709 foreach($expressfields as $key=>$field) {
1710 $this->form_fields[$key] = $field;
1711 }
1712 foreach($checkoutfields as $key=>$field) {
1713 $this->form_fields[$key] = $field;
1714 }
1715
1716
1717 foreach($external_payment_fields as $key=>$field) {
1718 $this->form_fields[$key] = $field;
1719 }
1720
1721 foreach($vipps_checkout_widgets_fields as $key=>$field) {
1722 $this->form_fields[$key] = $field;
1723 }
1724
1725 foreach($vipps_checkout_shipping_fields as $key=>$field) {
1726 $this->form_fields[$key] = $field;
1727 }
1728 foreach($advancedfields as $key=>$field) {
1729 $this->form_fields[$key] = $field;
1730 }
1731
1732 // The react UI decides whether or not to show the developer fields, however we always have to send this data to the client
1733 // because otherwise the react UI will not be able to show the correct translations, since they would be missing.
1734 foreach($developerfields as $key=>$field) {
1735 $this->form_fields[$key] = $field;
1736 }
1737 // Developer mode settings: Only shown when active. IOK 2019-08-30
1738 if ($this->get_option('developermode') == 'yes' || VIPPS_TEST_MODE) {
1739 if (VIPPS_TEST_MODE) {
1740 $this->form_fields['developermode']['description'] .= '<br><b>' . __('VIPPS_TEST_MODE is set to true in your configuration - dev mode is forced', 'woo-vipps') . "</b>";
1741 $this->form_fields['testmode']['description'] .= '<br><b>' . __('VIPPS_TEST_MODE is set to true in your configuration - test mode is forced', 'woo-vipps') . "</b>";
1742 }
1743 }
1744
1745
1746 }
1747
1748
1749 // IOK 2018-04-18 utilities for the 'admin notices' interface.
1750 private function adminwarn($what) {
1751 add_action('vipps_admin_notices',function() use ($what) {
1752 echo "<div class='notice notice-warning is-dismissible'><p>$what</p></div>";
1753 });
1754 }
1755 private function adminerr($what) {
1756 add_action('vipps_admin_notices',function() use ($what) {
1757 echo "<div class='notice notice-error is-dismissible'><p>$what</p></div>";
1758 });
1759 }
1760 private function adminnotify($what) {
1761 add_action('vipps_admin_notices',function() use ($what) {
1762 echo "<div class='notice notice-info is-dismissible'><p>$what</p></div>";
1763 });
1764 }
1765
1766 // Only be available if current currency is NOK IOK 2018-09-19
1767 // Only be available if current currency is supported NT 2023-12-04
1768 public function is_available() {
1769 // This is split into two functions to avoid triggering infinite recursion in filters that override this value below. IOK 2021-10-30
1770 $ok = $this->standard_is_available();
1771 $ok = apply_filters('woo_vipps_is_available', $ok, $this);
1772 return $ok;
1773 }
1774
1775 // True if the alternative Checkout screen is both available and activated. Returns the page id of the checkout
1776 // page for convenience. IOK 2021-10-01
1777 public function vipps_checkout_available () {
1778
1779 if ($this->get_option('vipps_checkout_enabled') != 'yes') return false;
1780 if (!$this->standard_is_available()) return false;
1781
1782 $checkoutid = wc_get_page_id('vipps_checkout');
1783 if (!$checkoutid) return false;
1784
1785 // Page doesn't exist anymore
1786 if (! get_post_status($checkoutid)) {
1787 delete_option('woocommerce_vipps_checkout_page_id');
1788 return false;
1789 }
1790
1791 // Restrictions on cart are similar to express checkout, but not exactly the same. IOK 2024-01-11
1792 if (!$this->cart_supports_checkout()) return false;
1793
1794 // Filter to false if you want to use the standard checkout for whatever reason
1795 return apply_filters('woo_vipps_checkout_available', $checkoutid, $this);
1796 }
1797
1798 // Get supported currencies for payment method NT 2023-12-04
1799 public function get_supported_currencies($payment_method) {
1800 switch ($payment_method) {
1801 case 'Vipps': return array('NOK', 'SEK');
1802 case 'MobilePay': return array('DKK', 'EUR');
1803 default: return array();
1804 }
1805 }
1806
1807 // Check if payment method supports currency. NT 2023-12-04
1808 public function payment_method_supports_currency($payment_method, $currency) {
1809 $ok = in_array($currency, $this->get_supported_currencies($payment_method));
1810 return $ok;
1811 }
1812
1813 // Basic unfiltered version of "can I use vipps" ? IOK 2021-10-01
1814 protected function standard_is_available () {
1815 if (!$this->can_be_activated()) return false;
1816 if (!parent::is_available()) return false;
1817 if (!$this->payment_method_supports_currency($this->get_payment_method_name(), get_woocommerce_currency())) return false;
1818 return true;
1819 }
1820
1821 // True if the express checkout feature should be available
1822 public function express_checkout_available() {
1823 if (! $this->is_available()) return false;
1824 $ok = true;
1825 $ok = apply_filters('woo_vipps_express_checkout_available', $ok, $this);
1826 return $ok;
1827 }
1828
1829 // IOK 2018-04-20 Initiate payment at Vipps and redirect to the Vipps payment terminal.
1830 public function process_payment ($order_id) {
1831 global $woocommerce, $Vipps;
1832 if (!$order_id) return [];
1833
1834 do_action('woo_vipps_before_process_payment',$order_id);
1835
1836 // Get current merchant serial
1837 $msn = $this->get_merchant_serial();
1838
1839 // Do a quick check for correct setup first - this is the most critical point IOK 2018-05-11
1840 try {
1841 $at = $this->api->get_access_token($msn);
1842 } catch (Exception $e) {
1843 $this->log(sprintf(__('Could not get access token when initiating %1$s payment for order id:','woo-vipps'), $this->get_payment_method_name()) . $order_id .":\n" . $e->getMessage(), 'error');
1844 wc_add_notice(sprintf(__('Unfortunately, the %1$s payment method is currently unavailable. Please choose another method.','woo-vipps'), $this->get_payment_method_name()),'error');
1845 return [];
1846 }
1847
1848 // From the request, get either [billing_phone] => or [vipps phone]
1849 $phone = '';
1850 if (isset($_POST['vippsphone'])) {
1851 $phone = trim(sanitize_text_field($_POST['vippsphone']));
1852 }
1853 if (!$phone && isset($_POST['billing_phone'])) {
1854 $phone = trim(sanitize_text_field($_POST['billing_phone']));
1855 }
1856
1857 // This is for express checkout if we know the customers' phone.
1858 // thanks to sOndre @ github for reporting , https://github.com/vippsas/vipps-woocommerce/issues/22
1859 if (!$phone && WC()->customer) {
1860 $phone = WC()->customer->get_billing_phone();
1861 }
1862
1863 // No longer the case for V2 of the API
1864 if (false && !$phone) {
1865 wc_add_notice(sprintf(__('You need to enter your phone number to pay with %1$s','woo-vipps'), $this->get_payment_method_name()) ,'error');
1866 return [];
1867 }
1868
1869 $order = wc_get_order($order_id);
1870 $content = null;
1871
1872 // Should be impossible, but there we go IOK 2022-04-21
1873 if (! $order->has_status(['pending', 'failed'])) {
1874 $this->log(sprintf(__("Trying to start order %1\$s with status %2\$s - only 'pending' and 'failed' are allowed, so this will fail", 'woo-vipps'), $order_id, $order->get_status()));
1875 wc_add_notice(sprintf(__('This order cannot be paid with %1$s - please try another payment method or try again later', 'woo-vipps'), $this->get_payment_method_name()), 'error');
1876 return [];
1877 }
1878
1879 // If the Vipps order already has an init-timestamp, we should *not* call init_payment again,
1880 // in the *normal* case, this is a user who have lost their vipps session, so it suffices to
1881 // just return the stored vipps session URL (eg. the user used the Back button.) If abandoned, the
1882 // order will eventually be cancelled. Changes in the cart will result in a new order anyway.
1883 // Note: we now also support restarting the payment with a new retry session if there is no stored vipps session. LP 2026-03-12
1884 if ($order->get_meta('_vipps_init_timestamp')) {
1885 $oldurl = $order->get_meta('_vipps_orderurl');
1886
1887 // Poll status at VMP here to verify session is still open. LP 2026-02-27
1888 $vipps_status = 'unknown';
1889 $vipps_session_open = false;
1890 try {
1891 $vipps_status = $this->get_payment_details($order)['status'] ?? 'unknown';
1892 if ('unknown' !== $vipps_status) {
1893 $vipps_status = $this->interpret_vipps_order_status($vipps_status);
1894 $vipps_session_open = 'initiated' === $vipps_status;
1895 }
1896 } catch (Exception $e) {
1897 /* translators: company name, order id */
1898 $this->log(sprintf(__('Retry-branch: error getting payment details from %1$s for order id %2$s:','woo-vipps'), $this->get_payment_method_name(), $order->get_id()) . "\n" . $e->getMessage(), 'error');
1899 }
1900
1901 // Do we have an active session we can redirect to? LP 2026-02-27
1902 if ($vipps_session_open && $oldurl) {
1903 $order->add_order_note(sprintf(__('%1$s payment restarted','woo-vipps'), $this->get_payment_method_name()));
1904 return array('result'=>'success','redirect'=>$oldurl);
1905 }
1906
1907 // If not, then we need to create a new retry session with incrementing index. LP 2026-03-12
1908 $retry_count = intval($order->get_meta('_vipps_retry_count')) + 1;
1909 $enable_payment_retry = 'unknown' !== $vipps_status; // dont retry unknown statuses. LP 2026-03-18
1910 if (apply_filters('woo_vipps_enable_payment_retry', $enable_payment_retry, $order, $vipps_status, $retry_count)) {
1911 /* translators: number of attempts, order id. */
1912 $this->log(sprintf(__('Order %2$d session could not be restored, creating a new retry session (retry #%1$d).', 'woo-vipps'), $retry_count, $order_id), 'info');
1913 $order->update_meta_data('_vipps_retry_count', $retry_count);
1914 $order->save_meta_data();
1915 } else {
1916 // If restarting is disabled, then we have to cancel the order. LP 2026-03-12
1917 /* translators: order id. */
1918 $this->log(sprintf(__('Order %1$d session could not be restored, and payment retry is disabled. Cancelling the order!', 'woo-vipps'), $order_id), 'info');
1919 $order->update_status('cancelled', sprintf(__('Cannot restart order at %1$s', 'woo-vipps'), Vipps::CompanyName()));
1920 return [];
1921 }
1922 }
1923
1924 // This is needed to ensure that the callbacks from Vipps have access to the customers' session which is important for some plugins. IOK 2019-11-22
1925 $this->save_session_in_order($order);
1926
1927 // Vipps-terminal-page return url to poll/await return
1928 $returnurl= $Vipps->payment_return_url();
1929 // If we are using express checkout, use this to handle the address stuff
1930 // IOK 2018-11-19 also when *not* using express checkout. This allows us to pass the order-id in the return URL and use this as a password in case the sesson has been lost.
1931 $authtoken = $this->generate_authtoken();
1932
1933 // IOK 2019-11-19 We have to do this because even though we actually store the order ID in the session, we can a) be redirected to another browser than the one with
1934 // the session, and b) some plugins wipe the session for guest purchases.
1935 // So we might need to restore (enough of the) session to get to the than you page, even if the session is gone
1936 // or in another castle.
1937 // IOK 2023-01-23 store the limited session in the order instead and separeted it from the authtoken
1938 $limited_session = $this->generate_authtoken();
1939 $returnurl = add_query_arg('ls',$limited_session,$returnurl);
1940 $returnurl = add_query_arg('id', $order_id, $returnurl);
1941
1942 try {
1943 // If the order was 'failed', it isnt any more! yet!
1944 if ($order->get_status() == 'failed') {
1945 $order->set_status('pending', __('Setting order status to pending to start payment', 'woo-vipps'));
1946 }
1947 // The requestid is actually for replaying the request, but I get 402 if I retry with the same Orderid.
1948 // Still, if we want to handle transient error conditions, then that needs to be extended here (timeouts, etc)
1949 // Update: replaced requestid with new idempotency key, moved this into epayment_initiate_payment. LP 2026-03-13
1950 $content = $this->api->epayment_initiate_payment($phone,$order,$returnurl,$authtoken);
1951 } catch (TemporaryVippsApiException $e) {
1952 $this->log(sprintf(__('Could not initiate %1$s payment','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage(), 'error');
1953 wc_add_notice(sprintf(__('Unfortunately, the %1$s payment method is temporarily unavailable. Please wait or choose another method.','woo-vipps'), $this->get_payment_method_name()),'error');
1954 return [];
1955 } catch (Exception $e) {
1956
1957 // Special case the "duplicate order id" thing to ensure it doesn't happen again, and if it does, at least
1958 // log some more info IOK 2022-11-02
1959 if (preg_match("/Duplicate Order Id/i", $e->getMessage())) {
1960 do_action('woo_vipps_duplicate_order_id', $order);
1961 $this->log(sprintf(__("Duplicate Order ID! Please report this to support@wp-hosting.no together with as much info about the order as possible. Express: %1\$s Status: %2\$s User agent: %3\$s", 'woo-vipps'), $order->get_meta('_vipps_express_checkout'), $order->get_status(), $order->get_customer_user_agent()), 'error');
1962 $order->update_status('cancelled', __('Cannot restart order with same order ID: Must cancel', 'woo-vipps'));
1963 }
1964
1965 $this->log(sprintf(__('Could not initiate %1$s payment','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage(), 'error');
1966 wc_add_notice(sprintf(__('Unfortunately, the %1$s payment method is currently unavailable. Please choose another method.','woo-vipps'), $this->get_payment_method_name()),'error');
1967 return [];
1968 }
1969
1970 $url = $content['url'];
1971 $vippstamp = time();
1972
1973 // Ensure we only check the status by ajax of our own orders. IOK 2018-05-03
1974 $sessionorders= WC()->session->get('_vipps_session_orders');
1975 $sessionorders[$order_id] = 1;
1976 WC()->session->set('_vipps_session_orders',$sessionorders);
1977 WC()->session->set('_vipps_pending_order',$order_id); // Send information to the 'please confirm' screen IOK 2018-04-24
1978
1979 $order = wc_get_order($order_id);
1980 if ($authtoken) {
1981 $order->update_meta_data('_vipps_authtoken',wp_hash_password($authtoken));
1982 }
1983 if ($limited_session) {
1984 $order->update_meta_data('_vipps_limited_session',wp_hash_password($limited_session));
1985 }
1986 // Store the "session URL" for restarts of the order in the same session context. IOK 2022-11-02
1987 $order->update_meta_data('_vipps_init_timestamp',$vippstamp);
1988 $order->update_meta_data('_vipps_orderurl', $url);
1989
1990 $order->update_meta_data('_vipps_status','INITIATE'); // INITIATE right now
1991
1992 $name = $this->get_payment_method_name();
1993 $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), $name));
1994 $order->add_order_note(sprintf(__('Awaiting %1$s payment confirmation','woo-vipps'),$name));
1995
1996 $order->save();
1997 // Create a signal file that we can check without calling wordpress to see if our result is in IOK 2018-05-04
1998 try {
1999 $Vipps->createCallbackSignal($order);
2000 } catch (Exception $e) {
2001 // Could not create a signal file, but that's ok.
2002 }
2003 do_action('woo_vipps_before_redirect_to_vipps',$order_id);
2004
2005 // This will send us to a receipt page where we will do the actual work. IOK 2018-04-20
2006 return array('result'=>'success','redirect'=>$url);
2007 }
2008
2009
2010 // This tries to capture a Vipps payment, and resets the status to 'on-hold' if it fails. IOK 2018-05-07
2011 public function maybe_capture_payment($orderid) {
2012 $order = wc_get_order($orderid);
2013 if (! Vipps::is_vipps_order($order)) return false;
2014 $ok = 0;
2015
2016 # Shortcut orders that have been directly captured
2017 $vippsstatus = $order->get_meta('_vipps_status');
2018 if ($vippsstatus == 'SALE') {
2019 return true;
2020 }
2021
2022 $remaining = intval($order->get_meta('_vipps_capture_remaining'));
2023
2024 // Somehow the order status in payment_complete has been set to the 'after order status' or 'complete' by a filter. If so, do not capture.
2025 // Capture will be done *before* payment_complete if appropriate IOK 2020-09-22
2026 if (did_action('woocommerce_pre_payment_complete')) {
2027 if (!$order->needs_processing()) return; // This is fine, we've captured.
2028 if ($remaining>0) {
2029 // Not everything has been captured, but we have reached a capturable status. Complain, do not capture. IOK 2020-09-22
2030 $this->log(sprintf(__("Filters are setting the payment_complete order status to '%1\$s' - will not capture", 'woo-vipps'), $order->get_status()),'debug');
2031 $order->add_order_note(sprintf(__('Payment complete set status to "%1$s" - will not capture payments automatically','woo-vipps'), $order->get_status()));
2032 return false;
2033 }
2034 }
2035
2036 // IOK 2019-10-03 it is now possible to do capture via other tools than Woo, so we must now first check to see if
2037 // the order is capturable by getting full payment details.
2038 try {
2039 $order = $this->update_vipps_payment_details($order);
2040 } catch (Exception $e) {
2041 //Do nothing with this for now
2042 $this->log(__("Error getting payment details before doing capture: ", 'woo-vipps') . $e->getMessage(), 'warning');
2043 }
2044
2045 try {
2046 $ok = $this->capture_payment($order);
2047 } catch (Exception $e) {
2048 // This is handled in sub-methods so we shouldn't actually hit this IOK 2018-05-07
2049 }
2050 if ($ok) {
2051 // Signal other hooked actions that this one actually did something. IOK 2025-02-04
2052 $order->update_meta_data('_vipps_capture_complete',true);
2053 $order->save();
2054 } else {
2055 $order->update_meta_data('_vipps_capture_complete',false);
2056 $msg = sprintf(__("Could not capture %1\$s payment for this order!", 'woo-vipps'), $this->get_payment_method_name());
2057 $order->add_order_note($msg);
2058 $order->save();
2059 if (apply_filters('woo_vipps_on_hold_on_failed_capture', true, $order)) {
2060 $msg = sprintf(__("Could not capture %1\$s payment - status set to", 'woo-vipps'), $this->get_payment_method_name()) . ' ' . __('on-hold','woocommerce');
2061 $order->set_status('on-hold',$msg);
2062 $order->save();
2063 global $Vipps;
2064 $this->adminerr($msg);
2065 $Vipps->store_admin_notices();
2066 return false;
2067 }
2068 }
2069 }
2070
2071
2072 // Capture (possibly partially) the order. Only full capture really supported by plugin at this point. IOK 2018-05-07
2073 // Except that we *do* note that money "refunded" through vipps before capture should be "uncapturable". IOK 2024-11-25
2074 public function capture_payment($order) {
2075 $pm = $order->get_payment_method();
2076 if (! Vipps::is_vipps_order($pm)) {
2077 $this->log(sprintf(__('Trying to capture payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()). ' ' . $order->get_id(), 'error');
2078 $this->adminerr(sprintf(__('Cannot capture payment on orders not made by %1$s','woo-vipps'), $this->get_payment_method_name()));
2079 return false;
2080 }
2081
2082 // Partial capture can happen if the order is edited IOK 2017-12-19
2083 $captured = intval($order->get_meta('_vipps_captured'));
2084 $vippsstatus = $order->get_meta('_vipps_status');
2085 $noncapturable = intval($order->get_meta('_vipps_noncapturable')); // This money has been marked as not-to-be-captured. It will be cancelled on complete.
2086
2087 // Ensure 'SALE' direct captured orders work
2088 if (!$captured && $vippsstatus == 'SALE') {
2089 $order = $this->update_vipps_payment_details($order);
2090 $captured = intval($order->get_meta('_vipps_captured'));
2091 }
2092
2093 $total = round(wc_format_decimal($order->get_total(),'')*100);
2094 $amount = $total-$captured-$noncapturable; // IOK subtract any amount not to be captured here
2095
2096 if ($amount<=0) {
2097 $order->add_order_note(__('Payment already captured','woo-vipps'));
2098 return true;
2099 }
2100
2101 // If we already have captured everything, then we are ok! IOK 2017-05-07
2102 if ($captured) {
2103 $remaining = intval($order->get_meta('_vipps_capture_remaining'));
2104 if (!$remaining) {
2105 $order->add_order_note(__('Payment already captured','woo-vipps'));
2106 return true;
2107 }
2108 }
2109
2110 // Each time we succeed, we'll increase the 'capture' transaction id so we don't just capture the same amount again and again. IOK 2018-05-07
2111 // (but on failre, we don't increase it - and also, we don't really support partial capture yet.) IOK 2018-05-07
2112 $requestidnr = intval($order->get_meta('_vipps_capture_transid'));
2113 // IOK 2023-03-13 keep track of failed captures; because some stores automate capturing without paying attention to the result.
2114 // some reservations are for only 7 days (or 30 days or 180 days) so some orders will be uncapturable. This will be reset by the
2115 // 'Show full transaction details' metabox.
2116 $failures = intval($order->get_meta('_vipps_capture_failures'));
2117 $failurelimit = 10;
2118 try {
2119
2120 // Assume we cannot capture if we have gotten errors 10 times from the API
2121 if ($failures >= $failurelimit) {
2122 throw new Exception(sprintf(__("More than %1\$d API exceptions trying to capture order - this order cannot be captured.", 'woo-vipps'), $failures));
2123 }
2124
2125 $requestid = $requestidnr . ":" . $order->get_order_key();
2126 $api = $order->get_meta('_vipps_api');
2127
2128
2129 if ($api == 'banktransfer') {
2130 // This is an error - we should not ever get to the 'capture' branch if we are a banktransfer payment.
2131 // IOK 2024-01-09
2132 $content = [];
2133 } else {
2134 // Now the only other api is 'epayment' IOK 2026-08-18
2135 $content = $this->api->epayment_capture_payment($order,$amount,$requestid);
2136 }
2137 } catch (TemporaryVippsApiException $e) {
2138 $this->log(sprintf(__('Could not capture %1$s payment for order id:', 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id() . "\n" .$e->getMessage(),'error');
2139 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . "\n" . $e->getMessage());
2140 return false;
2141 } catch (Exception $e) {
2142 // Keep track of API failures up to a point. IOK 2024-03-13
2143 if ($failures < $failurelimit) {
2144 $order->update_meta_data('_vipps_capture_failures', $failures + 1);
2145 $order->save();
2146 }
2147
2148 $msg = sprintf(__('Could not capture %1$s payment for order_id:','woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id() . "\n" . $e->getMessage();
2149 $this->log($msg,'error');
2150 $this->adminerr($msg);
2151 return false;
2152 }
2153
2154 $currency = $order->get_currency();
2155
2156 // Previously, we got this from the transactionInfo field of the Vipps data - this is no longer provided. IOK 2025-08-12
2157 // We simply have to keep track: There is no way of knowing what the correct values are here yet, as we only get these values async, after
2158 // the fact.
2159 $captured = $amount + intval($order->get_meta('_vipps_captured'));
2160 $remaining = intval($order->get_meta('_vipps_amount')) - $captured - intval($order->get_meta('_vipps_cancelled'));
2161 $refundable = $captured - intval($order->get_meta('_vipps_refunded'));
2162
2163 $order->update_meta_data('_vipps_captured', $captured);
2164 $order->update_meta_data('_vipps_capture_remaining', $remaining);
2165 $order->update_meta_data('_vipps_refund_remaining', $refundable);
2166 $order->update_meta_data('_vipps_capture_timestamp', time());
2167 $order->add_order_note(sprintf(__('%1$s Payment captured:','woo-vipps'), $this->get_payment_method_name()) . ' ' . sprintf("%0.2f",$captured/100) . ' ' . $currency);
2168
2169
2170 // Since we succeeded, the next time we'll start a new transaction.
2171 $order->update_meta_data('_vipps_capture_transid', $requestidnr+1);
2172 $order->save();
2173
2174 return true;
2175 }
2176
2177
2178 // Cancel (only completely) a reserved but not yet captured order IOK 2018-05-07
2179 public function cancel_payment($order) {
2180 $pm = $order->get_payment_method();
2181 if (! Vipps::is_vipps_order($pm)) {
2182 $this->log(sprintf(__('Trying to cancel payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()). ' ' .$order->get_id(), 'error');
2183 $this->adminerr(sprintf(__('Cannot cancel payment on orders not made by %1$s','woo-vipps'), $this->get_payment_method_name()));
2184 return false;
2185 }
2186 // We'll use the same transaction id for all cancel jobs, as we can only do it completely. IOK 2018-05-07
2187 // For epayment, partial cancellations will be possible. IOK 2022-11-12
2188 // IOK 2026-08-18 actually, epayment does *not* support partial cancellation - all remaining funds are cancelled.
2189 $api = $order->get_meta('_vipps_api');
2190 try {
2191 $requestid = "";
2192 if ($api == 'banktransfer') {
2193 // If we are here, and the order is somehow not captured, just do nothing. IOK 2024-01-09
2194 $content = [];
2195 } else {
2196 // api is here 'epayment'. IOK 2026-07-18
2197 $requestid = 1;
2198 // This will cancel any remaining, not-captured amount IOK 2026-01-28
2199 $content = $this->api->epayment_cancel_payment($order,$requestid);
2200 }
2201 } catch (TemporaryVippsApiException $e) {
2202 $this->log(sprintf(__('Could not cancel %1$s payment for order_id:', 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id() . "\n" .$e->getMessage(),'error');
2203 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage());
2204 return false;
2205 } catch (Exception $e) {
2206 $msg = sprintf(__('Could not cancel %1$s payment for order id:','woo-vipps'), $this->get_payment_method_name()) . $order->get_id() . "\n" . $e->getMessage();
2207 $this->log($msg,'error');
2208 $this->adminerr($msg);
2209 return false;
2210 }
2211
2212 // the epay v2 API would return transactionInfo and Summary with the result, the new epayment api returns nothing.
2213 // Removed epay branch 2025-08-12 IOK
2214 $total = intval($order->get_meta('_vipps_amount'));
2215 $captured = intval($order->get_meta('_vipps_captured'));
2216 $cancelled = $total;
2217 $remaining = $total - $captured - $cancelled;
2218
2219 // We need to assume it worked. Also, we can't do partial cancels yet, so just cancel everything.
2220 $order->update_meta_data('_vipps_cancel_timestamp',time());
2221 $order->update_meta_data('_vipps_cancelled', $cancelled);
2222 $order->update_meta_data('_vipps_cancel_remaining', $remaining);
2223
2224
2225 // Set status from Vipps, ignore errors, use statusdata if we have it.
2226 try {
2227 $status = $this->get_vipps_order_status($order);
2228 if ($status) $order->update_meta_data('_vipps_status',$status);
2229 $order->add_order_note(sprintf(__('%1$s Payment cancelled:','woo-vipps'), $this->get_payment_method_name()));
2230 $order->save();
2231 } catch (Exception $e) {
2232 }
2233 return true;
2234 }
2235
2236 // Refund (possibly partially) the captured order. IOK 2018-05-07
2237 // The caller must handle the errors.
2238 public function refund_payment($order,$amount=0,$cents=false) {
2239 $pm = $order->get_payment_method();
2240 if (! Vipps::is_vipps_order($pm)) {
2241 $msg = sprintf(__('Trying to refund payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id();
2242 $this->log($msg,'error');
2243 throw new VippsAPIException($msg);
2244 }
2245
2246
2247 // If we haven't captured anything, we can't refund IOK 2017-05-07
2248 $captured = intval($order->get_meta('_vipps_captured'));
2249 if (!$captured) {
2250 $msg = sprintf(__('Trying to refund payment on %1$s payment not captured:','woo-vipps'), $this->get_payment_method_name()). ' ' .$order->get_id();
2251 $this->log($msg,'error');
2252 throw new VippsAPIException($msg);
2253 }
2254
2255 // Each time we succeed, we'll increase the 'refund' transaction id so we don't just refund the same amount again and again. IOK 2018-05-07
2256 // (but on failre, we don't increase it.) IOK 2018-05-07
2257 $requestidnr = intval($order->get_meta('_vipps_refund_transid'));
2258 $requestid = $requestidnr . ":" . $order->get_order_key();
2259
2260 $api = $order->get_meta('_vipps_api');
2261 if ($api == 'banktransfer') {
2262 $msg = sprintf(__("Cannot refund bank transfer order %1\$d", 'woo-vipps'), $order->get_id());
2263 $this->log($msg, 'error');
2264 throw new Exception($msg);
2265 } else {
2266 // api is now 'epayment' IOK 2026-08-18
2267 $content = $this->api->epayment_refund_payment($order,$requestid,$amount,$cents);
2268 }
2269
2270 $currency = $order->get_currency();
2271
2272 // Previously, we got updated transaction info in a transactionInfo field. this is no longer provided,
2273 // so we have to do dead reckoning. IOK 2025-08-12
2274 // We simply have to keep track: There is no way of knowing what the correct values are here yet, as we only get these values async, after
2275 // the fact.
2276 $captured = intval($order->get_meta('_vipps_captured'));
2277
2278 if (!$amount) {
2279 $amount = wc_format_decimal($order->get_total(),'');
2280 $cents = false;
2281 }
2282 if ($amount && !$cents) {
2283 $amount = round($amount * 100);
2284 }
2285
2286 $refunded_now = $amount;
2287 $refunded = intval($order->get_meta('_vipps_refunded')) + $amount;
2288 $remaining = $captured - $refunded;
2289
2290 $order->update_meta_data('_vipps_refunded', $refunded);
2291 $order->update_meta_data('_vipps_refund_remaining', $remaining);
2292 $order->update_meta_data('_vipps_refund_timestamp', time());
2293 $order->add_order_note(sprintf(__('%1$s Payment Refunded:','woo-vipps'), $this->get_payment_method_name()) . ' ' . sprintf("%0.2f",$refunded/100) . ' ' . $currency );
2294
2295 // Since we succeeded, the next time we'll start a new transaction.
2296 $order->update_meta_data('_vipps_refund_transid', $requestidnr+1);
2297 $order->save();
2298 return true;
2299 }
2300
2301 // Generate a one-time password for certain callbacks, with some backwards compatibility for PHP 5.6
2302 public function generate_authtoken($length=32) {
2303 $token="";
2304 if (function_exists('random_bytes')) {
2305 $token = bin2hex(random_bytes($length));
2306 } elseif (function_exists('openssl_random_pseudo_bytes')) {
2307 $token = bin2hex(openssl_random_pseudo_bytes($length));
2308 } elseif (function_exists('mcrypt_create_iv')) {
2309 // These aren't "secure" but they are probably ok for this purpose. IOK 2018-05-18
2310 $indirect = 'mcrypt_create_iv'; // grep-based 7.2 compatibility checkers need to be worked around IOK 2018-10-24
2311 $token = bin2hex($indirect($length));
2312 } else {
2313 // Final fallback
2314 $token = bin2hex(md5(microtime() . ":" . mt_rand()));
2315 }
2316
2317 return $token;
2318 }
2319
2320 // Collapse several statuses to a known list IOK 2019-01-23
2321 // Statuses still in use are annotated. IOK 2025-08-12
2322 public function interpret_vipps_order_status($status) {
2323 switch ($status) {
2324 case 'INITIATE': // legacy, used by us to indicate a fresh order
2325 case 'REGISTER':
2326 case 'REGISTERED':
2327 case 'CREATED': // Checkout, Epayment
2328 return 'initiated';
2329 break;
2330 case 'RESERVE':
2331 case 'RESERVED':
2332 case 'AUTHORISED':
2333 case 'AUTHORIZED': // Checkout, Epayment
2334 case 'CAPTURED': // Epayment
2335 case 'REFUNDED': // epayment - this is probably authorized, because it will have had that state *before* it was refuned. IOK 2025-08-12
2336 return 'authorized';
2337 break;
2338 case 'SALE':
2339 return 'complete';
2340 break;
2341 case 'CANCEL':
2342 case 'CANCELLED': // Epayment
2343 case 'VOID':
2344 case 'AUTOREVERSAL':
2345 case 'AUTOCANCEL':
2346 case 'AUTO_CANCEL':
2347 case 'RESERVE_FAILED':
2348 case 'FAILED':
2349 case 'REJECTED':
2350 case 'TERMINATED': // Checkout, Epayment
2351 case 'ABORTED': // Epayment
2352 case 'EXPIRED': // Epayment
2353 return 'cancelled';
2354 break;
2355 }
2356 // Default should never happen, but just to ensure we are in our enumeration
2357 return "initiated";
2358 }
2359
2360 // This does not normally call Vipps, so if you need to refresh status, please use callback_check_order_status first. IOK 2019-01-23
2361 public function check_payment_status($order) {
2362 if (!$order) return 'cancelled';
2363 $status = $this->interpret_vipps_order_status($order->get_meta('_vipps_status'));
2364 // This can happen if the vipps status is set from the back end for instance. IOK 2020-08-14
2365 if ($order->get_status() == 'pending' && $status != 'initiated') {
2366 $this->callback_check_order_status($order);
2367 $order = wc_get_order($order->get_id()); // refresh to get the new status IOK 2021-01-20
2368 $status = $this->interpret_vipps_order_status($order->get_meta('_vipps_status'));
2369 }
2370 return $status;
2371 }
2372
2373 // Called by callback_check_order_status and handle_callback to handle the situation where
2374 // the payment method has been set to something else *after* Vipps has gotten the order.
2375 // This happens very rarely for people who use Vipps as an external payment method in Klarna, so
2376 // we only do it for orders that match this. IOK 2023-02-03
2377 public function reset_erroneous_payment_method($order) {
2378 // This is only called by methods that are Vipps-specific, but still lets be careful not to touch other orders
2379
2380 // New 2026-01-05: we now check all other payment methods that aren't vipps, and reset it back to vipps.
2381 // The issue was using Klarna Payments and pressing 'back' in the browser, then completing the payment in vipps checkout
2382 // the order still had the payment method klarna_payments, since we previously only checked 'kco' = klarna/kustom checkout. LP 2026-01-05
2383 if (! Vipps::is_vipps_order($order) && $order->get_meta("_vipps_orderid")) {
2384 $order->set_payment_method('vipps');
2385 $express = $order->get_meta('_vipps_express_checkout');
2386 $checkout = $order->get_meta('_vipps_checkout');
2387 $order->set_payment_method_title('Vipps');
2388 if ($express) $order->set_payment_method_title('Vipps Express Checkout');
2389 if ($checkout) $order->set_payment_method_title('Vipps Checkout');
2390 // paypal gw resets payment gateway on order save because it has this meta, so delete it before save. LP 2026-06-23
2391 $order->delete_meta_data('_ppcp_paypal_order_id');
2392 $order->save();
2393
2394 $msg = sprintf(__("Payment method reset to %1\$s - it had been set to another payment method while completing the order for %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id());
2395 $this->log($msg, 'debug');
2396 $order->add_order_note($msg);
2397 }
2398 }
2399
2400 // Check status of order at Vipps, in case the callback has been delayed or failed.
2401 // Should only be called if in status 'pending'; it will modify the order when status changes.
2402 public function callback_check_order_status($order, $allow_retry = true) {
2403 global $Vipps;
2404 $orderid = $order->get_id();
2405
2406 clean_post_cache($order->get_id());
2407 $order = wc_get_order($orderid); // Ensure a fresh copy is read.
2408
2409 $oldstatus = $order->get_status();
2410 $newstatus = $oldstatus;
2411
2412 // Only do work when the orders woo status is pending
2413 if ($oldstatus != 'pending') return $oldstatus;
2414
2415 $oldvippsstatus = $this->interpret_vipps_order_status($order->get_meta('_vipps_status'));
2416 $vippsstatus = "";
2417
2418 /* Now read the payment details and update the order with the relevant values, finding the new Vipps status IOK 2021-01-20 */
2419 $paymentdetails = array();
2420 try {
2421 $paymentdetails = $this->get_payment_details($order);
2422 $newvippsstatus = $paymentdetails['status'];
2423 $vippsstatus = $this->interpret_vipps_order_status($newvippsstatus);
2424
2425 $ready = false; // True if money is authorized or complete
2426 if (in_array($vippsstatus, ['authorized', 'complete'])) {
2427 $ready = true;
2428 }
2429
2430 // No change == nothing to do. Ensure we don't modify the order at this point. IOK 2025-10-24
2431 if ($vippsstatus == $oldvippsstatus) {
2432 return $oldstatus;
2433 }
2434 // Something changed, so we are now going to sideeffect the order. IOK 2025-10-15
2435 $this->log(sprintf(__("%1\$s poll: Handling order: ", 'woo-vipps'), Vipps::CompanyName()) . " " . $orderid, 'debug');
2436
2437 // If we are in the process of getting a callback from vipps, don't update anything. Currently, Woo/WP has no locking mechanism,
2438 // and it isn't feasible to implement one portably. So this reduces somewhat the likelihood of races when this method is called
2439 // and callbacks happen at the same time.
2440 if (!$Vipps->lockOrder($order)) {
2441 return $oldstatus;
2442 }
2443
2444 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
2445 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
2446 if ($ready) {
2447 $this->reset_erroneous_payment_method($order);
2448 }
2449
2450 $order->update_meta_data('_vipps_status',$newvippsstatus);
2451
2452 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
2453 if (!empty($paymentdetails)) {
2454 // checkout has a string, epayment has an array with upper case "type" and apparently, cardBin IOK 2025-08-12
2455 $paymentMethod = $paymentdetails['paymentMethod'] ?? "epayment";
2456 // After normalization, all APIs will have data here.
2457 $details = $paymentdetails['paymentDetails'];
2458 if (isset($details['paymentMethod'])) {
2459 $paymentMethod = $details['paymentMethod'];
2460 }
2461 if (!is_string($paymentMethod)) {
2462 // should be WALLET
2463 $paymentMethod = $paymentMethod['type'] ?? "epayment";
2464 }
2465 $transaction = array();
2466 $transaction['timeStamp'] = date('Y-m-d H:i:s', time());
2467 $transaction['amount'] = $details['amount']['value'];
2468 $transaction['currency'] = $details['amount']['currency'];
2469 $transaction['status'] = $details['state'];
2470 $transaction['paymentmethod'] = $paymentMethod;
2471 $this->order_set_transaction_metadata($order, $transaction);
2472 }
2473
2474 } catch (Exception $e) {
2475 $this->log(sprintf(__("Error getting payment details from %1\$s for order_id:",'woo-vipps'), $this->get_payment_method_name()) . $orderid . "\n" . $e->getMessage(), 'error');
2476 clean_post_cache($order->get_id());
2477 $Vipps->unlockOrder($order);
2478 return $oldstatus;
2479 }
2480 $order->save();
2481
2482 // We have a completed order, but the callback haven't given us the payment details yet - so handle it.
2483 if (($vippsstatus == 'authorized' || $vippsstatus=='complete') && $order->get_meta('_vipps_express_checkout')) {
2484
2485 do_action('woo_vipps_express_checkout_get_order_status', $paymentdetails);
2486 $address_set = $order->get_meta('_vipps_shipping_set');
2487
2488 if ($address_set) {
2489 // Callback has handled the situation, do nothing
2490 } elseif ($paymentdetails['shippingDetails'] ?? "") {
2491 // We need to set shipping details here
2492 $billing = isset($paymentdetails['billingDetails']) ? $paymentdetails['billingDetails'] : false;
2493 $this->set_order_shipping_details($order,$paymentdetails['shippingDetails'], $paymentdetails['userDetails'], $billing, $paymentdetails);
2494 } else {
2495 // IN THIS CASE we actually need to cancel the order as we have no way of determining whose order this is.
2496 // But first check to see if it has customer info!
2497 // Cancel any orders where the Checkout session is dead and there is no address info available
2498 if (!$order->has_shipping_address() && !$order->has_billing_address()) {
2499 $this->log(sprintf(__("No shipping details from %1\$s for express checkout for order id:",'woo-vipps'), $this->get_payment_method_name()) . ' ' . $orderid, 'error');
2500 $sessiontimeout = time() - (60*90);
2501 $then = intval($order->get_meta('_vipps_init_timestamp'));
2502 if ($then < $sessiontimeout) {
2503 $this->log(sprintf(__("Order %2\$d has no address info and any %1\$s session is dead - have to cancel.", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()));
2504 $order->update_status('cancelled', sprintf(__('Could not get address info for order from %1$s', 'woo-vipps'), Vipps::CompanyName()));
2505 $order->save();
2506 } else {
2507 // NOOP - the vipps checkout session can still be active, so we need to let it be
2508 $this->log(sprintf(__("No address information for order %2\$d, but there still might be an active %1\$s session for it, so do not cancel it.", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()));
2509 }
2510 }
2511 clean_post_cache($order->get_id());
2512 $Vipps->unlockOrder($order);
2513 return $oldstatus;
2514 }
2515 }
2516
2517 # Since the order is pending, and the vipps status has changed, switch to the correct vipps status in woo too IOK 2025-10-24
2518 switch ($vippsstatus) {
2519 case 'authorized':
2520 $this->payment_complete($order);
2521 break;
2522 case 'complete':
2523 $msg = sprintf(__('Payment captured directly at %1$s', 'woo-vipps'), $this->get_payment_method_name());
2524 $msg = $msg . __(" - order does not need processing", 'woo-vipps');
2525 $order->add_order_note($msg);
2526 $order = $this->update_vipps_payment_details($order, $paymentdetails);
2527 $order->payment_complete();
2528 break;
2529 case 'cancelled':
2530 $order_is_retryable = $allow_retry && Vipps::order_is_vipps_retryable($order->get_id());
2531 $status_on_fail = $this->get_option('status_on_fail');
2532 $cancel_on_fail = apply_filters('woo_vipps_cancel_failed_orders', false, $order, $vippsstatus);
2533 if ($cancel_on_fail || !$order_is_retryable) {
2534 $status_on_fail = 'cancelled';
2535 }
2536 if (!in_array($status_on_fail, ['cancelled', 'failed'])) {
2537 /* translators: order status name. Cancelled is woocommerce status name */
2538 $this->log(__('Unsupported status for payment failure of \'%1$s\', falling back to cancelled.', 'woo-vipps'), 'warning');
2539 $status_on_fail = 'cancelled';
2540 }
2541
2542 /* translators: company name */
2543 $order->update_status($status_on_fail, sprintf(__('Order failed or rejected at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
2544 break;
2545 }
2546
2547 $order->save();
2548 clean_post_cache($order->get_id());
2549 $newstatus = $order->get_status();
2550 $Vipps->unlockOrder($order);
2551 return $newstatus;
2552 }
2553
2554 // IOK 2020-01-20 Previously was just a debugging tool, then was used to update postmeta values. Now is used as the main source of info
2555 // about the order from Vipps; the previous side-effecting is now done by update_vipps_payment_details.
2556 // IOK 2021-11-24 Because of this, we need to handle 402 and 404 errors differently here now - these *are* results, meaning there is
2557 // no payment details because the order doesn't exist.
2558 // IOK 2022-01-19 And now, with the epayment API in use by checkout, we also need to use the poll api because the epayment API does not return user- and shipping data. We do get an order status though.
2559 // IOK 2025-08-12 And now, with epayment used for Express Checkout also, there *is* user- and shipping-data when using Express Checkout, so we can simplify. We still need to transform the data
2560 // so old hooks and filters can get the input they expect.
2561 public function get_payment_details($order) {
2562 $result = array();
2563 $checkout_session = $order->get_meta('_vipps_checkout_session');
2564 $express = $order->get_meta('_vipps_express_checkout');
2565
2566 // IOK 2025-08-12: Three cases; either this is Checkout, in which case we need to get user/shipping-data from the checkout session,
2567 // or it is Express Checkout or normal payment, in which cases we just retrieve the payment from the epayment API - possibly containing user data
2568 if ($checkout_session) {
2569 try {
2570 $result = $this->api->checkout_get_session_info($order);
2571
2572 if ($result == 'EXPIRED') {
2573 $result = array('status'=>'CANCEL', 'state'=>'CANCEL');
2574 return $result;
2575 }
2576
2577 // For checkout, we are really handling *session states* which we map to *order states*, but sometimes
2578 // these indicate a failed order.
2579 // 'state' can be missing for Checkout, first case is if this is because the session is invalid.
2580 // The sesssion states are # "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
2581 if (isset($result['sessionState']) && ($result['sessionState'] == 'SessionTerminated' || $result['sessionState'] == 'SessionExpired')) {
2582 $result['state'] = 'CANCEL';
2583 $result['status'] = 'CANCEL';
2584 $order->add_order_note(sprintf(__('%1$s Order with no order status, so session was never completed; setting status to cancelled', 'woo-vipps'), Vipps::CheckoutName()));
2585 return $result;
2586 }
2587
2588 // IOK 2023-12-14 at some point, apparently SessionStarted became SessionCreated
2589 if (isset($result['sessionState']) && ($result['sessionState'] == 'SessionStarted' || $result['sessionState'] == 'SessionCreated')) {
2590 // We have no order info, only the session data (this is a checkout order). Therefore, assume it has been started at least.
2591 $result['status'] = "INITIATE";
2592 $result['state'] = "INITIATE";
2593 $created = $order->get_date_created();
2594 $timestamp = 0;
2595 $now = time();
2596 try {
2597 $timestamp = $created->getTimestamp();
2598 } catch (Exception $e) {
2599 // PHP 8 gives ValueError for certain older versions of WooCommerce here.
2600 $timestamp = intval($created->format('U'));
2601 }
2602 $passed = $now - $timestamp;
2603 $minutes = ($passed / 60);
2604
2605 // Give up after 120 minutes. Actually, 60 minutes is probably enough: We expire live sessions after 50 mins.
2606 if ($minutes > 120) {
2607 $this->log(sprintf(__('Checkout order older than 120 minutes with no order status - cancelled as abandoned: %1$s', 'woo-vipps'), $order->get_id()), 'debug');
2608 $order->add_order_note(sprintf(__('%1$s Order with no order status, so session was never completed; setting status to cancelled', 'woo-vipps'), Vipps::CheckoutName()));
2609 $result['status'] = 'CANCEL';
2610 $result['state'] = 'CANCEL';
2611 }
2612 return $result;
2613 }
2614
2615
2616 } catch (VippsAPIException $e) {
2617 $resp = intval($e->responsecode);
2618 if ($resp == 402 || $resp == 404) {
2619 $result = array('status'=>'CANCEL', 'state'=>'CANCEL');
2620 return $result;
2621 } else {
2622 throw $e;
2623 }
2624 }
2625 } else {
2626 try {
2627 $result = $this->api->epayment_get_payment($order);
2628 } catch (VippsAPIException $e) {
2629 $resp = intval($e->responsecode);
2630 if ($resp == 402 || $resp == 404) {
2631 $result = array('status'=>'CANCEL', 'state'=>'CANCEL');
2632 return $result;
2633 } else {
2634 $this->log(sprintf(__("Could not get order status from %1\$s using epayment api: ", 'woo-vipps'), Vipps::CompanyName()) . $e->getMessage(), "error");
2635 throw $e;
2636 }
2637 }
2638 }
2639
2640 if (!$result || $result == "ERROR") {
2641 $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
2642 wp_die(sprintf(__("Could not get payment results for order %1\$s - you may have the wrong MSN for the order. Please check logs for more information", 'woo-vipps'), $order->get_id()));
2643 }
2644
2645 // We now have a result which maybe will contain user and shipping data, which we will need to normalize because it is slightly different in the different
2646 // APIs, and we have provided filters/hooks that receive this information. IOK 2025-08-12
2647 // If we now have epayment data, we want to translate this to the ecom 'view' for now. Later, we will do the opposite.
2648 // We now only need to map Checkout and epayment to the same format, but we will try to keep the normalized result compatible with
2649 // ecom, in case consumers have created filters/hooks. IOK 2025-08-12
2650
2651 // if Checkout, the result will have a sessionState, reference etc, userInfo, shippingDetails, billingDetails and the payment details in a paymentDetails member.
2652 // if not, the result will *be* a paymentDetails field, but with user*Details* and shippingDetails added. No billingDetails.
2653 // The end result should have state/status (not neccessarily present with Checkout), userDetails, paymentDetails, shippingDetails and billingDetails, and a transactionSummary.
2654 // Because that's what the code depending on this has been expecting. IOK 2025-08-12
2655 if (!$checkout_session) {
2656 // This should be an ecom result, so move all data (for simplicitys sake) into paymentDetails
2657 $result['paymentDetails'] = $result;
2658 }
2659 // Ensure we get payment details with state + aggregate, which we do not if the payment method is bank transfer for checkout. IOK 2024-01-09
2660 // Also add keys and transform for backwards compatibility.
2661 $result = $this->normalizePaymentDetails($result);
2662 $newstatus = $this->interpret_vipps_order_status($result['status']);
2663 $ready = false;
2664 if (in_array($newstatus, ['authorized', 'complete'])) {
2665 $ready = true;
2666 }
2667
2668 if ($ready && ($express || $checkout_session)) {
2669 // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
2670 // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
2671 $result = $this->ensure_userDetails($result, $order);
2672
2673 // After, we need to normalize shipping details or even add them if e.g. using Checkout without address or contact info IOK 2025-08-13
2674 // Epayment Express Checkout is of course also significantly different from both the old Express and from Checkout in the formatting here. IOK 2025-08-12
2675 $result = $this->normalizeShippingDetails($result, $order);
2676 }
2677
2678 return $result;
2679 }
2680
2681 // IOK 2025-08-12 Normalize the shippingDetails field for backwards compatibility, since this is different from old express, new express and checkout.
2682 function normalizeShippingDetails($result, $order) {
2683
2684 // We may actually get no shipping details eg. for Checkout when not asking for addresses etc.
2685 if (!isset($result['shippingDetails'])) {
2686 $result['shippingDetails'] = ['address' => [] ];
2687 }
2688 $details = $result['shippingDetails'];
2689
2690 $user = $result['userDetails']; // Normalized earlier
2691 $address = $details['address'] ?? [];
2692
2693 // No address, but we have a list of addresses in the userDetails field. This is ecom with the address scope added.
2694 // We will use the first one.
2695 if (empty($address) && isset($result['userDetails']) && isset($result['userDetails']['addresses']) && !empty($result['userDetails']['addresses'])) {
2696 $address = $result['userDetails']['addresses'][0];
2697 }
2698
2699 // Checkout has address info inside shippingDetails, whereas Express Checkout now has it in an address field IOK 2025-08-12
2700 if (empty($address)) {
2701 $address['firstName'] = $details['firstName'] ?? "";
2702 $address['lastName'] = $details['lastName'] ?? "";
2703 $address['email'] = $details['email'] ?? "";
2704 $address['mobileNumber'] = $details['phoneNumber'] ?? "";
2705 $address['addressLine1'] = $details['streetAddress'] ?? "";
2706 $address['addressLine2'] = "";
2707 $address['city'] = $details['city'] ?? "";
2708 $address['postCode'] = $details['postalCode'] ?? "";
2709 $address['country'] = $details['country'] ?? "";
2710 }
2711
2712 // Need at least a country for VAT so add our own.
2713 if (!$address['country']) {
2714 $countries=new WC_Countries();
2715 $address['country'] = $countries->get_base_country();
2716 }
2717
2718 // Normalize from user details when neccessary (mostly for new express)
2719 $address['firstName'] = ($address['firstName'] ?? "") ?: $user['firstName'];
2720 $address['lastName'] = ($address['lastName'] ?? "") ?: $user['lastName'];
2721 $address['email'] = ($address['email'] ?? "") ?: $user['email'];
2722 $address['mobileNumber'] = ($address['mobileNumber'] ?? "") ?: $user['mobileNumber'];
2723
2724 // phoneNumber is checkout, mobileNumber is epayment
2725 $address['phoneNumber'] = $address['mobileNumber'];
2726 // addressline1 and 2 are epayment, streetAddress is checkout
2727 $address['streetAddress'] = $address['addressLine1'];
2728 // postCode is epayment, postalCode is checkout
2729 $address['postalCode'] = $address['postCode'];
2730
2731
2732 // Ensure we have 'address' as in Express/epayment
2733 $details['address'] = $address;
2734
2735 // Name change from Checkout/Express to new Express from MethodId to OptionId IOK 2025-08-12
2736 $details['shippingMethodId'] = $details['shippingMethodId'] ?? ($details['shippingOptionId'] ?? "");
2737 $details['shippingOptionId'] = $details['shippingMethodId'];
2738
2739 $result['shippingDetails'] = $details;
2740 return $result;
2741 }
2742
2743
2744 // IOK 2024-01-09 If using Checkout with the BankTransfer method, which is eg. used in Finland,
2745 // we are (currently) not receiving any 'state' or 'aggregate', so add this iff the payment is successful.
2746 // The reason for this is that this payment type does not actually use the epayment API at all (!)
2747 // Also moved some other compatibility code here -
2748 // --- reference used to be orderId
2749 // --- state used to be status
2750 // --- there used to be a transactionSummary field
2751 // --- at one point there was a 'transactionAggregate' instead of an 'aggregate'
2752 private function normalizePaymentDetails($result) {
2753 // the 'reference' used to be an 'orderId', keep for compatibility.
2754 $result['orderId'] = $result['reference'];
2755 if (isset($result['sessionState']) && $result['sessionState'] == 'PaymentSuccessful' && $result['paymentMethod'] == 'BankTransfer') {
2756 $details = $result['paymentDetails'];
2757 $details['state'] = 'SALE'; // Payment is actually complete at this point
2758 $aggregate=[];
2759 $aggregate['capturedAmount'] = $details['amount'];
2760 $aggregate['authorizedAmount'] = $details['amount'];
2761 $aggregate['refundedAmount'] = ['value'=>0, 'currency'=>$details['amount']['currency']];
2762 $aggregate['cancelledAmount'] = ['value'=>0, 'currency'=>$details['amount']['currency']];
2763 $details['aggregate'] = $aggregate;
2764 $result['paymentDetails'] = $details;
2765 }
2766
2767 // Sometimes we get the state at top level, sometimes in paymentDetails.
2768 $state = $result['state'] ?? false;
2769
2770 // Now, if we don't have payment details, we should have a dead session or something like it. If we do, we can cretae
2771 // a normalized result. IOK 2023-12-13
2772 if (isset($result['paymentDetails'])) {
2773 $details = $result['paymentDetails'];
2774 $state = $state ?: $details['state'];
2775 $result['state'] = $state;
2776 $details['state'] = $state;
2777
2778 # IOK 2022-01-19 for this, the docs and experience does not agree, so check both
2779 $aggregate = (isset($details['transactionAggregate'])) ? $details['transactionAggregate'] : $details['aggregate'];
2780
2781 // if 'AUTHORISED' and directCapture is set and true, set to SALE which will set the order to complete
2782 // IOK 2025-08-12: This is never true anymore - directCapture is never set and the SALE state does not seem to exist.
2783 // this was an extra feature for merchants with special products however, so keep the logic just in case.
2784 if (($result['state'] == 'AUTHORISED' || $result['state'] == "AUTHORIZED") && isset($result['directCapture']) && $result['directCapture']) {
2785 $result['state'] = "SALE";
2786 }
2787
2788 # the transactionSummary used to contain the information now present in 'aggregate', so map it back for compatibility.
2789 $transactionSummary = array();
2790 // Always NOK at this point, but we also don't care because the order has the currency
2791 // IOK 2024-03-22 Now supports other currencies, but we still don't care.
2792 $transactionSummary['capturedAmount'] = isset($aggregate['capturedAmount']) ? $aggregate['capturedAmount']['value'] : 0;
2793 $transactionSummary['refundedAmount'] = isset($aggregate['refundedAmount']) ? $aggregate['refundedAmount']['value'] : 0;
2794 $transactionSummary['cancelledAmount'] =isset($aggregate['cancelledAmount']) ? $aggregate['cancelledAmount']['value'] : 0;
2795 $transactionSummary['authorizedAmount'] =isset($aggregate['authorizedAmount']) ? $aggregate['authorizedAmount']['value'] : 0;
2796 $transactionSummary['remainingAmountToCapture'] = $transactionSummary['authorizedAmount'] - $transactionSummary['cancelledAmount'] - $transactionSummary['capturedAmount'];
2797 $transactionSummary['remainingAmountToRefund'] = $transactionSummary['capturedAmount'] - $transactionSummary['refundedAmount'];
2798 // now also reducing remainingAmmountToCancel with cancelledAmount PMB 2024-11-21
2799 $transactionSummary['remainingAmountToCancel'] = $transactionSummary['authorizedAmount'] - $transactionSummary['capturedAmount'] - $transactionSummary['cancelledAmount'];
2800
2801 $result['transactionSummary'] = $transactionSummary;
2802 }
2803 // After this, the result will contain a 'state' which used to be a 'status' - map back for compatibility.
2804 // The reason for the method is that in ecom v2 we needed to calculate this from the transaction history. IOK 2025-08-12
2805 $result['status'] = $this->get_payment_status_from_payment_details($result);
2806
2807 // No longer used; was used in later versions of ecom to deduce order status. Added for typewise compatibility.
2808 $result['transactionLogHistory'] = array();
2809 // The corresponding epayment log. Filled on-demand by debugging code.
2810 $result['epaymentLog'] = null;
2811
2812 return $result;
2813 }
2814
2815 // Checkout v3 does *not* provide userDetails. Checkout v2 and epayment *does*. But Checkout additionally allows
2816 // for anonymous purchases, in which case there is *no* user details. In this case we provide an anonymous user so we can actually create an order.
2817 // To handle this, we provide this utility that ensures we have userDetails no matter the input. For this we use the anonymous filters and "billingDetails" if present
2818 // if not, we use shippingDetails. IOK 2023-01-10
2819 // Also, epayment uses mobileNumber and checkout uses phoneNumber, so normalize.
2820 public function ensure_userDetails($vippsdata, $order) {
2821 $userDetails = [];
2822
2823 // If we have userDetails, use it (ecom API with user data requested - Express Checkout
2824 if (isset($vippsdata['userDetails'])) {
2825 $userDetails = $vippsdata['userDetails'];
2826 // This is the verified user information from the app - this is always the customer for Express Checkout, but not for Checkout IOK 2025-08-12
2827 // Also, it may not always be available - it depends on consent and whether scope was added (probably) in epayment_initate_payment. IOK 2026-08-18
2828 $sub = "";
2829 if (isset($vippsdata['profile']) && isset($vippsdata['profile']['sub'])) {
2830 $sub = $vippsdata['profile']['sub'];
2831 }
2832 $userDetails['sub'] = $sub;
2833
2834 } else if (isset($vippsdata['billingDetails'])) {
2835 // Otherwise this is now Checkout, and we want to get it from billingDetails preferrably
2836 $addr = $vippsdata['billingDetails'];
2837 $phone = $addr['phoneNumber'];
2838 $userDetails = array(
2839 'firstName' => $addr['firstName'],
2840 'lastName' => $addr['lastName'],
2841 'email' => $addr['email'],
2842 'phoneNumber' => $phone,
2843 // This is the verified user information from the app - this is always the customer for Express Checkout, but not for Checkout IOK 2025-08-12
2844 'sub' => ""
2845 );
2846
2847 // Or use shippingDetails - this is still Checkout
2848 } else if (isset($vippsdata['shippingDetails'])) {
2849 $addr = $vippsdata['shippingDetails'];
2850 $phone = $addr['phoneNumber'];
2851 $userDetails = array(
2852 'firstName' => $addr['firstName'],
2853 'lastName' => $addr['lastName'],
2854 'email' => $addr['email'],
2855 'phoneNumber' => $phone,
2856 // This is the verified user information from the app - this is always the customer for Express Checkout, but not for Checkout IOK 2025-08-12
2857 'sub' => ""
2858 );
2859
2860 // And it is possible to not require user details in Checkout at all (or if not using express)
2861 } else {
2862 $userDetails = array(
2863
2864 'firstName' => apply_filters('woo_vipps_anon_customer_first_name', __('Anonymous customer', 'woo-vipps'), $order),
2865 'lastName' => apply_filters('woo_vipps_anon_customer_last_name', "", $order),
2866 'email' => apply_filters('woo_vipps_anon_customer_email', '', $order),
2867 'phoneNumber' => apply_filters('woo_vipps_anon_customer_phone_number', '', $order),
2868 'sub' => ""
2869 );
2870 }
2871
2872 //Normalize the result
2873 $phone = $userDetails['phoneNumber'] ?? ($userDetails['mobileNumber'] ?? "");
2874 $userDetails['phoneNumber'] = $phone;
2875 $userDetails['mobileNumber'] = $phone;
2876
2877
2878 // No longer used, but try to provide it for backwards compatibility
2879 $userDetails['userId'] = $userDetails['phoneNumber'];
2880 // This is possible to get iff we have the 'sub', unfortunately it is not passed directly in several of the apis. IOK 2025-08-12
2881 $userDetails['email_verified'] = ($userDetails['email_verified'] ?? false);
2882
2883 $vippsdata['userDetails'] = $userDetails;
2884
2885 return $vippsdata;
2886 }
2887
2888 // Update the order with Vipps payment details, either passed or called using the API.
2889 public function update_vipps_payment_details ($order, $details = null) {
2890 if (!$details) {
2891 try {
2892 $details = $this->get_payment_details($order);
2893 } catch (Exception $e) {
2894 // We'll try but if we fail, that's too bad. IOK 2026-03-18. No recovery is really possible here.
2895 $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
2896 $this->log($e->getMessage());
2897 }
2898 }
2899
2900 if ($details) {
2901 if (isset($details['transactionSummary'])) {
2902 $transactionSummary= $details['transactionSummary'];
2903 $order->update_meta_data('_vipps_status',$details['status']);
2904 $order->update_meta_data('_vipps_captured',$transactionSummary['capturedAmount']);
2905 $order->update_meta_data('_vipps_refunded',$transactionSummary['refundedAmount']);
2906 $order->update_meta_data('_vipps_capture_remaining',$transactionSummary['remainingAmountToCapture']);
2907 $order->update_meta_data('_vipps_refund_remaining',$transactionSummary['remainingAmountToRefund']);
2908 if (isset($details['transactionSummary']['cancelledAmount'])) {
2909 $order->update_meta_data('_vipps_cancelled',$transactionSummary['cancelledAmount']);
2910 $order->update_meta_data('_vipps_cancel_remaining',$transactionSummary['remainingAmountToCancel']);
2911 }
2912 }
2913 // This is the epayment API - IOK 2022-01-20
2914 if (isset($details['paymentDetails'])) {
2915 $d = $details['paymentDetails'];
2916 if (isset($d['amount'])) {
2917 $order->update_meta_data('_vipps_amount', $d['amount']['value']);
2918 }
2919 $aggregate = (isset($d['transactionAggregate'])) ? $d['transactionAggregate'] : $d['aggregate'];
2920 if ($aggregate) {
2921 // capturedAmount, refundedAmount, authorizedAmount, cancelledAmount
2922 if (isset($aggregate['authorizedAmount'])) {
2923 $order->update_meta_data('_vipps_amount', $aggregate['authorizedAmount']['value']);
2924 }
2925 }
2926 }
2927 // Modify payment method name if neccessary
2928 if (isset($details['paymentMethod']) && $details['paymentMethod'] == 'Card') {
2929 if ($order->get_meta('_vipps_checkout')) {
2930 $order->set_payment_method_title(sprintf(__('Credit Card / %1$s', 'woo-vipps'), Vipps::CheckoutName()));
2931 }
2932 }
2933 if (isset($details['paymentMethod']) && $details['paymentMethod'] == 'BankTransfer') {
2934 if ($order->get_meta('_vipps_checkout')) {
2935 $order->set_payment_method_title(sprintf(__('Bank Transfer/ %1$s', 'woo-vipps'), Vipps::CheckoutName()));
2936 $order->update_meta_data('_vipps_api', 'banktransfer');
2937 }
2938 }
2939 $order->save();
2940 }
2941 return $order;
2942 }
2943
2944 // IOK 2021-01-20 from March 2021 the order_status interface is removed; so we now need to interpret the payment history to find
2945 // out the order status.
2946 // IOK 2022-01-19 With the epayment API on the other hand, the payment status is back as 'state'.
2947 // IOK 2025-08-12 Except for certain payment methods in Checkout. So.
2948 // IOK 2025-08-12 Removing the ecom support now, so all code will use 'state', as normalized by normalizePaymentDetails
2949 public function get_payment_status_from_payment_details($details) {
2950 $status = $details['state'];
2951 return $status;
2952 }
2953
2954 // Get the order status as defined by Vipps; if you have the payment details already, pass them. Will modify the order. IOK 2021-01-20
2955 public function get_vipps_order_status($order, $statusdata=null) {
2956 $vippsorderid = $order->get_meta('_vipps_orderid');
2957 if (!$vippsorderid) {
2958 $msg = sprintf(__('Could not get %1$s order status - it has no %1$s Order Id. Must cancel.','woo-vipps'), $this->get_payment_method_name());
2959 $this->log($msg,'error');
2960 return 'CANCEL';
2961 }
2962 if (!$statusdata) {
2963 try {
2964 $statusdata = $this->get_payment_details($order);
2965 } catch (TemporaryVippsApiException $e) {
2966 $this->log(sprintf(__('Could not get %1$s order status for order id:', 'woo-vipps'), Vipps::CompanyName()) . ' ' . $order->get_id() . "\n" .$e->getMessage(),'error');
2967 return null;
2968 } catch (VippsAPIException $e) {
2969 $msg = sprintf(__('Could not get %1$s order status','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage();
2970 $this->log($msg,'error');
2971 if (intval($e->responsecode) == 402 || intval($e->responsecode) == 404) {
2972 $this->log(sprintf(__('Order does not exist at %1$s - cancelling','woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id(), 'warning');
2973 return 'CANCEL';
2974 }
2975 } catch (Exception $e) {
2976 $msg = sprintf(__('Could not get %1$s order status for order id:','woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id() . "\n" . $e->getMessage();
2977 $this->log($msg,'error');
2978 return null;
2979 }
2980 }
2981 if (!$statusdata) return null;
2982
2983 $vippsstatus = isset($statusdata['status']) ? $statusdata['status'] : "";
2984
2985 if (!$vippsstatus) {
2986 $this->log("Unknown Vipps Status: " . print_r($statusdata, true), 'debug');
2987 }
2988 return $vippsstatus;
2989 }
2990
2991 // The various Vipps APIs return address info with various keys and formats, so we need to translate all of them
2992 // to a canonical format.
2993 public function canonicalize_vipps_address($address, $user) {
2994 // eCom has user info only in the user struct
2995 $firstname = $user['firstName'];
2996 $lastname = $user['lastName'];
2997 $email = $user['email'];
2998
2999 // Get the passed phone number from checkout or express, which could be in any number of slots IOK 2025-
3000 $phone = isset($user['mobileNumber']) ? $user['mobileNumber'] : "";
3001 if (isset($user['phoneNumber'])) $phone = $user['phoneNumber'];
3002 if (!$phone && ($address['phoneNumber'] ?? "")) $phone = $address['phoneNumber'];
3003 if (!$phone && ($address['mobileNumber'] ?? "")) $phone = $address['mobileNumber'];
3004
3005 // Phone number transformations - the format Vipps Mobilepay uses is often not what merchants expect or need
3006 // NOTE: as of writing this, the checkout+expresscheckout expected format is '{countrycode}{phonenr}', so we assume this. LP 2025-12-29
3007 $phone_transformation = $this->get_option('checkout_phone_transformation');
3008 switch($phone_transformation) {
3009 case 'ensure_plus':
3010 $phone = "+$phone";
3011 break;
3012 case 'strip_country_code':
3013 // We only support norway,denmark,swedish,finnish country codes as of now. LP 2025-12-29
3014 // We can't do these replaces in sequence, because e.g. +47 46 12 34 56 would become 0 12 34 56 instead of the correct 46 12 34 56. LP 2026-02-19
3015 if (preg_match('!^(45|47)!', $phone)) { // NO, DK
3016 $phone = preg_replace('!^(45|47)!', '', $phone);
3017 } else if (preg_match('!^(46|358)!', $phone)) { // SE, FI: leading zero for area codes is not used with country code, so add it back here. LP 2026-02-09
3018 $phone = preg_replace('!^(46|358)!', '0', $phone);
3019 }
3020 break;
3021 }
3022 $phone = apply_filters('woo_vipps_canonicalize_checkout_phone', $phone, $address, $user);
3023
3024 if (!isset($address['firstName']) or !$address['firstName']) {
3025 $address['firstName'] = $firstname;
3026 }
3027 if (!isset($address['lastName']) or !$address['lastName']) {
3028 $address['lastName'] = $lastname;
3029 }
3030 // checkout uses phonenumber
3031 if (!isset($address['phoneNumber']) or !$address['phoneNumber']) {
3032 $address['phoneNumber'] = $phone;
3033 }
3034 // epayment uses mobileNumber
3035 if (!isset($address['mobileNumber']) or !$address['mobileNumber']) {
3036 $address['mobileNumber'] = $phone;
3037 }
3038 if (!isset($address['email']) or !$address['email']) {
3039 $address['email'] = $email;
3040 }
3041
3042 // epayment
3043 $addressline1 = isset($address['addressLine1']) ? $address['addressLine1'] : "";
3044 $addressline2 = isset($address['addressLine2']) ? $address['addressLine2'] : "";
3045 // checkout
3046 if (isset($address['streetAddress'])) {
3047 $addressline1 = $address['streetAddress'];
3048 }
3049 if (isset($address['street_address'])) { // From the userinfo api
3050 $addressline1 = $address['street_address'];
3051 }
3052 if ($addressline1 == $addressline2) $addressline2 = '';
3053 $address['addressLine1'] = $addressline1; // epayment
3054 $address['addressline2'] = $addressline2;
3055 $address['streetAddress'] = $addressline1; // Checkout
3056
3057 $city = "";
3058 if (isset($address['city'])) { // checkout and epayment
3059 $city = $address['city'];
3060 } elseif (isset($address['region'])) { // ecom
3061 $city = $address['region'];
3062 }
3063 $address['city'] = $city;
3064 $address['region'] = $city;
3065
3066 $postcode= "";
3067 if (isset($address['postCode'])) {
3068 $postcode= $address['postCode']; // epayment
3069 } elseif (isset($address['postalCode'])){
3070 $postcode= $address['postalCode']; // checkout
3071 } elseif (isset($address['postal_code'])) {
3072 $postcode= $address['postal_code']; // userinfo
3073 }
3074 $address['postCode'] = $postcode; // epayment
3075 $address['zipCode'] = $postcode; // ecom
3076 $address['postalCode'] = $postcode; // checkout
3077
3078 // Allow users to modify the address to e.g. handle phone numbers differently IOK 2025-01-20
3079 // note: added separate filter for phone number 'woo_vipps_canonicalize_checkout_phone' because of the different uses, keys etc. LP 2025-12-29
3080 return apply_filters('woo_vipps_canonicalize_checkout_address', $address, $user);
3081 }
3082
3083 public function set_order_shipping_details($order,$shipping, $user, $billing=false, $alldata=null, $assigncustomer=true) {
3084 global $Vipps;
3085 $done = $order->get_meta('_vipps_shipping_set');
3086 if ($done) return true;
3087 $order->update_meta_data('_vipps_shipping_set', true);
3088 $order->save(); // Limit the window for double shipping as much as possible.
3089
3090 // This is for handling a custom consent checkbox for mailing lists etc. IOK 2023-02-09
3091 if ($alldata && isset($alldata['customConsentProvided'])) {
3092 $order->update_meta_data('_vipps_custom_consent_provided', intval($alldata['customConsentProvided']));
3093 }
3094
3095
3096 // We get different values from the normal callback and the Checkout callback, so be prepared for several results. IOK 2021-09-02
3097 // IOK VERIFY this should be normalized now or soon at any rate
3098 $address = isset($shipping['address']) ? $shipping['address'] : $shipping;;
3099
3100 // Sometimes we get an empty shipping address! In this case, fill the details with the billing address.
3101 // This only happens with ecommerce though, so we need to check before canonicalizing. IOK 2022-03-21
3102 // billing is only present in Checkout, which uses phoneNumber, streetAddress, postalCode etc.
3103 $shipping_empty = true;
3104 if ($billing && array_key_exists('streetAddress', $address)) {
3105 $keys = ['firstName', 'lastName', 'email', 'phoneNumber', 'streetAddress', 'postalCode', 'city', 'country'];
3106 foreach ($keys as $key) {
3107 if (isset($address[$key]) && $address[$key]) {
3108 $shipping_empty = false; break;
3109 }
3110 }
3111 if ($shipping_empty) {
3112 foreach ($keys as $key) $address[$key] = $billing[$key];
3113 }
3114 }
3115
3116 if (!$billing) $billing = $address;
3117 $address = $this->canonicalize_vipps_address($address, $user);
3118 $billing = $this->canonicalize_vipps_address($billing, $user);
3119
3120 # Billing.
3121 $order->set_billing_email($billing['email']);
3122 $order->set_billing_phone($billing['mobileNumber']);
3123 $order->set_billing_first_name($billing['firstName']);
3124 $order->set_billing_last_name($billing['lastName']);
3125 $order->set_billing_address_1($billing['addressLine1']);
3126 if ($billing['addressLine2'] ?? false) $order->set_billing_address_2($billing['addressLine2']);
3127 $order->set_billing_city($billing['city'] ?? "");
3128 $order->set_billing_postcode($billing['postCode'] ?? "");
3129 $order->set_billing_country($billing['country'] ?? "");
3130
3131 # Shipping.
3132 $order->set_shipping_first_name($address['firstName']);
3133 $order->set_shipping_last_name($address['lastName']);
3134 if (version_compare(WC_VERSION, '5.6.0', '>=')) {
3135 $order->set_shipping_phone($address['mobileNumber']);
3136 }
3137 $order->set_shipping_address_1($address['addressLine1']);
3138 if ($address['addressLine2'] ?? "") $order->set_shipping_address_2($address['addressLine2']);
3139 $order->set_shipping_city($address['city']);
3140 $order->set_shipping_postcode($address['postCode']);
3141 $order->set_shipping_country($address['country']);
3142
3143 $order->save();
3144
3145 // This is *essential* to get VAT calculated correctly. That calculation uses the customer, which uses the session, which we will have restored at this point.IOK 2019-10-25
3146 if (WC()->customer) {
3147 WC()->customer->set_billing_email($billing['email']);
3148 WC()->customer->set_email($billing['email']);
3149
3150 $country = $billing['country'] ?? ($address['country'] ?? "");
3151 WC()->customer->set_billing_location($country,'',$billing['postalCode'],$billing['region']);
3152 WC()->customer->set_shipping_location($address['country'],'',$address['postalCode'],$address['region']);
3153 }
3154
3155 // We may need the order total early on, so start with that
3156 $ordertotal = $order->get_total() ?: 0;
3157
3158 $vipps_reserved = $alldata['paymentDetails']['amount']['value'] ?? null;
3159 // i dont expect order total to ever be greater than vipps reserved total here, so i dont use abs(). LP 2026-01-28
3160 $diff_reserved_ordertotal = is_numeric($vipps_reserved) ? ($vipps_reserved / 100 - $ordertotal) : 0;
3161
3162 // Now do shipping, if it exists IOK 2021-09-02
3163 $method = isset($shipping['shippingMethodId']) ? $shipping['shippingMethodId'] : false;
3164
3165 // We will add a shipping rate either if there has been passed a shipping method; or if this
3166 // is an order that needs shipping and the amount reserved at vipps is greater than the order total
3167 // (with a bit of tolerance for rounding errors). If we *don't* have a shipping method but we know
3168 // we need a shipping rate; this is an error condition that the merchant needs to resolve. We'll add a fake
3169 // 'Unknown' rate to ensure we capture the right amount and add this fact to the order log. Code by LP, comment by IOK 2026-01-28
3170 $needs_shipping = $method || $order->get_meta('_vipps_needs_shipping');
3171 $tol = 0.01;
3172 $has_shipping = $method || $diff_reserved_ordertotal > $tol;
3173
3174 if ($needs_shipping && $has_shipping) {
3175 $shipping_rate=null;
3176 $option_table = [];
3177
3178 // Try to find the shipping rate. LP 2026-01-28
3179 if ($method) {
3180 if (substr($method,0,1) != '$') {
3181 // This is the old way of adding shipping. We should probably deprecate this sometime soon. IOK 2026-01-28
3182 $shipping_rate = $this->get_legacy_express_checkout_shipping_rate($shipping);
3183 } else {
3184 // Strip suffixes if we have several Express rates mapping to the same Woo rate (eg. for Posten). IOK 2025-05-04
3185 $matches = [];
3186 preg_match("!^(?P<key>[^:]+):?(?P<option_index>.+)?$!", $method, $matches);
3187 $key = $matches['key'] ?? "";
3188 $option_index = intval(trim($matches['option_index'] ?? "")); // 0 is never an index
3189 $shipping_table = $order->get_meta('_vipps_express_checkout_shipping_method_table');
3190 $is_base64 = $shipping_table ? ( $shipping_table['_is_base64'] ?? false) : false;
3191
3192 if (is_array($shipping_table) && isset($shipping_table[$key])) {
3193 $decoded = $is_base64 ? @base64_decode($shipping_table[$key]) : $shipping_table[$key];
3194
3195 // Ensure no shop manager has injected an evil object (that they would have had to add as a plugin) here. IOK 2026-09-18
3196 $shipping_rate = $decoded ? @unserialize($decoded, ['allowed_classes' => [WC_Shipping_Rate::class]]) : null;
3197 $shipping_rate = is_a($shipping_rate,'WC_Shipping_Rate') ? $shipping_rate : null;
3198
3199 if (!$shipping_rate) {
3200 $this->log(sprintf(__("%1\$s: Could not deserialize the chosen shipping method %2\$s for order %3\$d", 'woo-vipps'), Vipps::ExpressCheckoutName(), $method, $order->get_id()), 'error');
3201 $this->log(sprintf(__("Serialized data was %1\$s", 'woo-vipps'), $decoded), 'error');
3202 } else {
3203 // Special-case the Woo pickup location methods. IOK 2026-01-28
3204 if ($option_index) {
3205 $meta = $shipping_rate->get_meta_data();
3206 $option_table = $meta['_vipps_pickupPoints'] ?? [];
3207 // force string table IOK 2025-08-15
3208 $point = $option_table["i".$option_index] ?? "";
3209 if ($point) {
3210 $shipping['pickupPoint'] = $point;
3211 }
3212 $shipping_rate->add_meta_data('_vipps_pickupPoints', null);
3213 }
3214 // Empty this when done, but not if there was an error - let the merchant be able to debug. IOK 2020-02-14
3215 $order->update_meta_data('_vipps_express_checkout_shipping_method_table', null);
3216 }
3217 }
3218 }
3219
3220 // Possible extra metadata from Checkout IOK 2023-01-17
3221 // Store in the order, but also in the shipping rate so it will be visible in the order screen
3222 // along with the shipping ragte
3223 if (isset($shipping['pickupPoint'])) {
3224 $order->update_meta_data('vipps_checkout_pickupPoint', $shipping['pickupPoint']);
3225 if ($shipping_rate) {
3226 $pp = $shipping['pickupPoint'];
3227 $addr = [];
3228 foreach(['address', 'postalCode', 'city', 'country'] as $key) {
3229 $v = trim($pp[$key]);
3230 if (!empty($v)) $addr[] = trim($pp[$key]);
3231 }
3232
3233 $shipping_rate->add_meta_data('pickup_location', $pp['name']);
3234 $shipping_rate->add_meta_data('pickup_address', join(", ", $addr));
3235 $shipping_rate->add_meta_data('pickup_details', ""); // Not supported by the API unfortunately. IOK 2025-06-07
3236 }
3237 }
3238 if (isset($shipping['timeslot'])) {
3239 $order->update_meta_data('vipps_checkout_timeslot', $shipping['timeslot']);
3240 if ($shipping_rate) {
3241 $pp = $shipping['timeslot'];
3242 $slot = "";
3243 $slot .= sprintf(__("Date: %s", 'woo-vipps'), ($pp['date'] ?? ""));
3244 $slot .= " " . sprintf(__("Start: %s", 'woo-vipps'), ($pp['start'] ?? ""));
3245 $slot .= " " . sprintf(__("End: %s", 'woo-vipps'), ($pp['end'] ?? ""));
3246 $shipping_rate->add_meta_data('vipps_delivery_timeslot', $slot);
3247 $shipping_rate->add_meta_data('vipps_delivery_timeslot_id', $pp['id']);
3248 }
3249 }
3250 }
3251
3252 // We know we need a shipping rate, and what its price has to be, so ensure we have one by making one up if missing. LP 2026-01-28
3253 if (!$shipping_rate) {
3254 $shipping_rate = new WC_Shipping_Rate(
3255 'UNKNOWN',
3256 sprintf(__('Unknown shipping: please check the shipping details at %1$s', 'woo-vipps'), Vipps::CompanyName()),
3257 $diff_reserved_ordertotal,
3258 [],
3259 'UNKNOWN',
3260 0,
3261 );
3262 $shipping_rate = apply_filters('woo_vipps_unknown_shipping_rate_dummy', $shipping_rate, $order);
3263
3264 // Add an error log for the merchants to quote to us.
3265 $msg = sprintf(__("%1\$s Could not retrieve any shipping rate for this order, with method %2\$s",'woo-vipps'), $this->get_payment_method_name(), $method) . " " . $order->get_id();
3266 $order->add_order_note($msg);
3267 $this->log($msg, 'warning');
3268 }
3269
3270 $shipping_rate = apply_filters('woo_vipps_express_checkout_final_shipping_rate', $shipping_rate, $order, $shipping);
3271
3272 $total_shipping = 0;
3273 $total_shipping_tax = 0;
3274 $it = null;
3275
3276 // Recover the Shipping Method class
3277 $methods_classes = WC()->shipping->get_shipping_method_class_names();
3278 $methodclass = $methods_classes[$shipping_rate->get_method_id()] ?? null;
3279 $shipping_method = $methodclass ? new $methodclass($shipping_rate->get_instance_id()) : null;
3280 $is_vipps_checkout_shipping = $shipping_method && is_a($shipping_method, 'VippsCheckout_Shipping_Method');
3281
3282 // Some Checkout-specific shipping methods calculate the cost in the Vipps window.
3283 if ($is_vipps_checkout_shipping && $shipping_method->dynamic_cost) {
3284 $vippsamount = intval($order->get_meta('_vipps_amount'));
3285 $shipping_tax_rate = floatval($order->get_meta('_vipps_shipping_tax_rates'));
3286 $compareamount = $ordertotal * 100;
3287 $amountdiff = $vippsamount-$compareamount; // this is the *actual* shipping cost at this point
3288 $diffnotax = ($amountdiff / (100 + $shipping_tax_rate)); // Adjusted to correct values actually
3289 $difftax = WC_Tax::round($amountdiff/100 - $diffnotax);
3290 $actual = $amountdiff/100 - $difftax;
3291
3292 $shipping_rate->set_cost($actual);
3293 $shipping_rate->set_taxes( [ 1 => $difftax] );
3294 }
3295
3296 $it = new WC_Order_Item_Shipping();
3297 $it->set_shipping_rate($shipping_rate);
3298 $it->set_order_id( $order->get_id() );
3299 // This should actually have been done by the "set_shipping_rate" call above, but as of 3.9.2 at least, this does not work.
3300 // Therefore, do it manually/forcefully IOK 2020-02-17
3301 foreach($shipping_rate->get_meta_data() as $key => $value) {
3302 $it->add_meta_data($key,$value,true);
3303 }
3304 $it->save();
3305
3306 $order->add_item($it);
3307
3308 $total_shipping = $it->get_total() ?: 0;
3309 $total_shipping_tax = $it->get_total_tax() ?: 0;
3310
3311 // Try to avoid calculate_totals, because this will recalculate shipping _without checking if the rate
3312 // in question actually should use tax_. Therefore we will just add the pre-calculated values, so that the
3313 // value reserved at Vipps and the order total is the same. IOK 2022-10-03
3314 $order->set_shipping_total($total_shipping);
3315 $order->set_shipping_tax($total_shipping_tax);
3316
3317 $order->set_total($ordertotal + $total_shipping + $total_shipping_tax);
3318 $order->update_taxes(); // Necessary for the admin view only; does not recalculate order.
3319
3320 // Add an early hook for Checkout orders with special shipping methods
3321 $metadata = $shipping_rate->get_meta_data();
3322 if (isset($metadata['type'])) {
3323 do_action('woo_vipps_checkout_special_shipping_method', $order, $shipping_rate, $metadata['type']);
3324 }
3325
3326 $order->save();
3327 // NB: WE DO NOT CALL CALCULATE TOTALS!
3328 // THIS WILL CALCULATE SHIPPING FOR TAX IF THERE IS A TAX RATE FOR THE GIVEN AREA WITH THE 'shipping' PROPERTY CHECKED - EVEN IF THE SHIPPING RATE IT SELF IS NOT THUS CONFIGURED.
3329 // Same thing happens when using the "recalculate" button in the backend
3330 // This will *only* affect users that choose "No tax" for the shipping rates themselves, which is mostly just wrong, but we want to ensure consistency here since this is hard
3331 // to debug.
3332 // $order->calculate_totals(true);
3333 }
3334
3335
3336 // If we have the 'expresscreateuser' thing set to true, we will create or assign the order here, as it is the first-ish place where we can.
3337 // If possible and safe, user will be logged in before being sent to the thankyou screen. IOK 2020-10-09
3338 // Same thing for Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3339 $customer = false;
3340 if ($assigncustomer) {
3341 $customer = Vipps::instance()->express_checkout_get_vipps_customer($order);
3342 }
3343 if ($customer) {
3344 // This would have been used to ensure that we 'enroll' the users the same way as in the Login plugin. Unfortunately, the userId from express checkout isn't
3345 // the same as the 'sub' we get in Login so that must be a future feature. IOK 2020-10-09
3346 // IOK 2025-08-13 we do get the 'sub' now, at least for express checkout. For Checkout, we would have to compare the email of the user with the verified email
3347 // after calling get_userinfo, so we'll leave that be.
3348 // We *maybe* get the sub - it depends on consent, and *maybe* that a scope has been added in epayment_initate_payment. IOK 2026-08-18
3349 if (class_exists('VippsWooLogin') && $customer && !is_wp_error($customer) && !get_user_meta($customer->get_id(), '_vipps_phone',true)) {
3350 update_user_meta($customer->get_id(), '_vipps_phone', $billing['phoneNumber']);
3351 if (isset($user['sub'])) {
3352 $userid = $customer->get_id();
3353 update_user_meta($userid, '_vipps_id', $user['sub']);
3354 update_user_meta($userid, '_vipps_just_connected', 1);
3355 }
3356 }
3357
3358 // Ensure we get any changes made to the order, as it will be re-saved later
3359 $order = wc_get_order($order->get_id());
3360 }
3361
3362 do_action('woo_vipps_set_order_shipping_details', $order, $shipping, $user);
3363 $order->save(); // I'm not sure why this is neccessary - but be sure.
3364
3365 }
3366
3367 // Previously, shipping rates were added by creating them here with metadata packed into the shippingMethodId. This is from 1.4.0 only
3368 // used when the woo_vipps_shipping_methods filter has been overriden by the merchant. IOK 2020-02-14
3369 private function get_legacy_express_checkout_shipping_rate($shipping) {
3370 $method = $shipping['shippingMethodId'];
3371 list ($rate,$tax) = explode(";",$method);
3372 // The method ID is encoded in the rate ID but we apparently must still send it to the WC_Shipping_Rate constructor. IOK 2018-06-01
3373 // Unfortunately, Vipps won't accept long enought 'shipingMethodId' for us to actually stash all the information we need. IOK 2018-06-01
3374 list ($method,$product) = explode(":",$rate);
3375 $tax = wc_format_decimal($tax,'');
3376 $label = $shipping['shippingMethod'];
3377 $cost = wc_format_decimal($shipping['shippingCost'],''); // This is inclusive of tax
3378 $costExTax= wc_format_decimal($cost-$tax,'');
3379 $shipping_rate = new WC_Shipping_Rate($rate,$label,$costExTax,array(array('total'=>$tax)), $method, $product);
3380 $shipping_rate = apply_filters('woo_vipps_express_checkout_shipping_rate',$shipping_rate,$costExTax,$tax,$method,$product);
3381 return $shipping_rate;
3382 }
3383
3384 // Used by both callback_check_order_status and handle_callback - sets the neccessary order metadata after a successful (or not vipps transaction). IOK 2025-08-13
3385 public function order_set_transaction_metadata($order, $transaction) {
3386 // Set Vipps metadata as early as possible
3387 $vippsstamp = strtotime($transaction['timeStamp']);
3388 $vippsamount = $transaction['amount'] ?? '';
3389 $vippscurrency= $transaction['currency'] ?? '';
3390 $vippsstatus = $transaction['status'];
3391
3392 $order->update_meta_data('_vipps_callback_timestamp',$vippsstamp);
3393 $order->update_meta_data('_vipps_amount',$vippsamount);
3394 $order->update_meta_data('_vipps_currency',$vippscurrency);
3395 $order->update_meta_data('_vipps_status',$vippsstatus);
3396
3397 // Checkout only, modify payment method name if neccessary
3398 if ($transaction['paymentmethod'] == 'Card') {
3399 if ($order->get_meta('_vipps_checkout')) {
3400 $order->set_payment_method_title(sprintf(__('Credit Card / %1$s', 'woo-vipps'), Vipps::CheckoutName()));
3401 }
3402 }
3403 // Checkout only, banktransfers are handled specially so note that
3404 if ($transaction['paymentmethod'] == 'BankTransfer') {
3405 $order->set_payment_method_title(sprintf(__('Bank Transfer/ %1$s', 'woo-vipps'), Vipps::CheckoutName()));
3406 $order->update_meta_data('_vipps_api', 'banktransfer');
3407 }
3408 }
3409
3410 // Handle the callback from Vipps ePayment
3411 public function handle_callback($result, $order, $ischeckout=false, $iswebhook=false) {
3412 global $Vipps;
3413
3414 $vippsorderid = $result['orderId'];
3415 $merchant= $result['merchantSerialNumber'];
3416
3417 $keyset = $this->get_keyset();
3418 $me = array_keys($keyset);
3419
3420 // Validate the callback first
3421 if (!in_array($merchant, $me)) {
3422 $this->log(sprintf(__("%1\$s callback with wrong merchantSerialNumber - might be forged",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3423 return false;
3424 }
3425 if (!$order) {
3426 $this->log(sprintf(__("%1\$s callback for unknown order",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3427 return false;
3428 }
3429 $order_id = $order->get_id();
3430 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3431 $this->log(sprintf(__('Wrong %1$s Orderid - possibly an attempt to fake a callback ', 'woo-vipps'), Vipps::CompanyName()), 'warning');
3432 clean_post_cache($order_id);
3433 exit();
3434 }
3435
3436 // Note any errors in the callback early
3437 $errorInfo = $result['errorInfo'] ?? '';
3438 if ($errorInfo) {
3439 /* translators: payment method name, order id */
3440 $this->log(sprintf(__('Message in callback from %1$s for order %2$s: ','woo-vipps'), $this->get_payment_method_name(), $order_id), $errorInfo['errorMessage'], 'error');
3441 /* translators: payment method name, message */
3442 $order->add_order_note(sprintf(__('Message from %1$s: %2$s','woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3443 }
3444
3445 // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3446 try {
3447 $Vipps->createCallbackSignal($order,'ok');
3448 } catch (Exception $e) {
3449 // Could not create a signal file, but that's ok.
3450 }
3451
3452 // New callback handling: schedule an Action Scheduler job to process it. The purpose of processing callback
3453 // is to set finalize the order (set order status, set shipping for express) in the case when customer does
3454 // not return to the store, because then poll does not run. LP 2026-03-27
3455
3456 // Below is separate from '_vipps_callback_timestamp' which is when callback is sent,
3457 // but also that meta won't be stored until callback is actually processed. LP 2026-03-30
3458 $order->update_meta_data('_vipps_callback_received_at', time());
3459 // Store the callback data in the order. We'll do a cleanup of this when the scheduled job runs. IOK 2026-04-21
3460 $order->update_meta_data('_vipps_callback_data', $result);
3461 $order->save_meta_data();
3462
3463 // Run callback actions for callback received as soon as it is actually received.
3464 $transaction = []; // No longer provided. IOK 2026-05-06
3465 do_action('woo_vipps_callback_received', $order, $result, $transaction);
3466
3467 $action_args = [
3468 'order_id' => $order->get_id(),
3469 'is_checkout' => $ischeckout,
3470 'is_webhook' => $iswebhook,
3471 ];
3472 // We'll check the status of this order in a minute. At that time, the customer should have been able to return to the store
3473 // and have the order finalized the 'normal' way, but if they don't, we'll handle it async. 2026-04-21
3474 $scheduled_at = time() + 60;
3475 $action_id = as_schedule_single_action($scheduled_at, 'woo_vipps_action_process_callback', $action_args, 'woo-vipps', false);
3476 if ($action_id) {
3477 /* translators: order id, scheduled time */
3478 $this->log(sprintf(__('Callback action scheduled at %2$s for order %1$s', 'woo-vipps'), $order->get_id(), $scheduled_at), 'info');
3479 } else {
3480 // The action scheduler error is not returned, only sent to error_log (https://github.com/woocommerce/action-scheduler/blob/25c982c3d0f8134389d5b5884082403c4806322f/classes/ActionScheduler_ActionFactory.php#L268). LP 2026-04-23
3481 /* translators: order id */
3482 $this->log(sprintf(__('Failed to schedule callback process action for order %1$s, check the php error log', 'woo-vipps'), $order->get_id()), 'error');
3483 /* We will not delete the callback data here, to facilitate debugging. But return false to indicate that callback handling will fail. */
3484 /* NB: The order will still be processed with the periodic job, at a later stage. IOK 2026-05-06 */
3485 return false;
3486 }
3487
3488 // Signal that we in fact handled the order.
3489 return true;
3490 }
3491
3492 /** Runs in action scheduler: sync woo status from Vipps callback data. Handle shipping etc. for Express. LP 2026-03-31 */
3493 public function action_process_callback($order_id, $is_checkout, $is_webhook) {
3494 $order = wc_get_order($order_id);
3495 if (!is_a($order, 'WC_Order')) {
3496 /* translators: order id */
3497 $this->log(sprintf(__('Callback process action failed, could not find order %1$s','woo-vipps'), $order_id), 'error');
3498 return false;
3499 }
3500
3501 $oldstatus = $order->get_status();
3502 if ($oldstatus != 'pending') {
3503 // Actually, we are ok with this order, abort the callback handler. IOK 2018-05-30
3504 $order->delete_meta_data('_vipps_callback_data');
3505 clean_post_cache($order->get_id());
3506 return false;
3507 }
3508 $data = $order->get_meta('_vipps_callback_data');
3509
3510 /* translators: order id */
3511 $this->log(sprintf(__('Callback process action running for order %1$s.', 'woo-vipps'), $order_id));
3512
3513 // The payment details field is passed in Checkout, not in Express, but none of them are complete, so we fill out the values
3514 // depending on which one we are IOK 2025-08-13
3515 $details = [];
3516 // Checkout has this as a field, containing *some* of the neccessary data
3517 if (isset($data['paymentDetails'])) {
3518 // Checkout. The sesssion states are # "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
3519 // -- we should only get callbacks for successful sessions actually.
3520
3521 $details = $data['paymentDetails'];
3522 $data['state'] = $data['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($data['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3523 $details['state'] = $data['state'];
3524 $details['paymentMethod'] = $data['paymentMethod'];
3525 } else {
3526 // This should be an epayment webhook callback; which we need to add a lot of data for to get a valid "paymentDetails".
3527 $details = [];
3528 $data['state'] = $data['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3529 $details['state'] = $data['name'];
3530 $details['amount'] = $data['amount']; // currency, value
3531 $details['paymentMethod'] = 'epayment';
3532 }
3533
3534 // For both callbacks, set 'aggregate'
3535 $currency = $details['amount']['currency'];
3536 $nothing = [ 'currency' => $currency, 'value' => 0];
3537 $aggregate = ['authorizedAmount' => $nothing, 'cancelledAmount' => $nothing, 'capturedAmount' => $nothing, 'refundedAmount' => $nothing];
3538 if ($details['state'] == 'AUTHORIZED') {
3539 $aggregate['authorizedAmount'] = $details['amount'];
3540 }
3541 $details['aggregate'] = $aggregate;
3542 $data['paymentDetails'] = $details;
3543
3544 // Do the actual work with a function shared with the periodic job.
3545 $this->set_order_status_by_payment_details($order,$data);
3546
3547 /* translators: payment method name */
3548 $order->add_order_note(sprintf(__('%1$s callback processed','woo-vipps'), $this->get_payment_method_name()));
3549
3550 // We're done, so delete the callback data. IOK 2026-04-22
3551 $order->delete_meta_data('_vipps_callback_data');
3552 }
3553
3554
3555 // Called either by periodic job or by action_process_callback with the callback data *or* with payment details fetched with poll.
3556 // sets order status if neccessary, and will finalize the order for Express via HTTP call if necessary. IOK 2026-05-06
3557 public function set_order_status_by_payment_details($order, $data, $allow_retry=true) {
3558 $data = $this->normalizePaymentDetails($data);
3559 $details = $data['paymentDetails'];
3560 $order_id = $order->get_id();
3561
3562 $vippsstatus = $data['status']; // Will exist now, because of the normalization IOK 2025-08-13
3563 $newstatus = $this->interpret_vipps_order_status($vippsstatus);
3564
3565 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
3566 $transaction = array();
3567 $stamp = ($data['timestamp'] ?? false) ? strtotime($data['timestamp']) : time();
3568 $transaction['timeStamp'] = date('Y-m-d H:i:s', $stamp);
3569 $transaction['amount'] = $details['amount']['value'];
3570 $transaction['currency'] = $details['amount']['currency'];
3571 $transaction['status'] = ($data['state'] ?? $details['state']);
3572 $transaction['paymentmethod'] = $details['paymentMethod'] ?? "";
3573 $this->order_set_transaction_metadata($order, $transaction);
3574
3575 // Dont do anything if order is not finalized. LP 2026-08-31
3576 if (!in_array($newstatus, ['authorized', 'complete', 'cancelled'])) {
3577 return;
3578 }
3579
3580 // This order is ready to set order shipping details etc for IOK 2025-09-19
3581 $ready = in_array($newstatus, ['authorized', 'complete']);
3582
3583 if ($ready) {
3584 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
3585 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3586 $this->reset_erroneous_payment_method($order);
3587 }
3588
3589 $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3590
3591 // Handle session and shipping through http, because we dont want to mess with session here in wp cron (action scheduler). LP 2026-03-30
3592 // NB: This is and must be a *synchronous call*. When done, the order will have shipping, addresses etc. IOK 2026-05-06.
3593 $shipping_set = $order->get_meta('_vipps_shipping_set');
3594 if ($ready && $is_express_or_checkout && !$shipping_set) {
3595 $token = $order->get_meta('_vipps_authtoken');
3596 $args = [
3597 'body' => [
3598 'order_id' => $order_id,
3599 'vipps_order_data' => $data,
3600 ],
3601 'headers' => [
3602 'X-WooVipps-Token' => $token,
3603 ],
3604 ];
3605 $url = Vipps::get_rest_url('v1', '/order-set-shipping');
3606 $response = wp_remote_post($url, $args);
3607 if (is_wp_error($response)) {
3608 /* translators: order id, error message */
3609 $error_msg = $response->get_error_message();
3610 $this->log(sprintf(__('Process callback action failed to finalize shipping through http rest endpoint for order %1$s: %2$s', 'woo-vipps'), $order->get_id(), $error_msg), 'error');
3611 } else if (200 != ($response['response']['code'] ?? -1)) {
3612 /* translators: order id */
3613 $response_msg = print_r($response['body'] ?? ['Missing response body'], true);
3614 $this->log(sprintf(__('Process callback action failed to finalize shipping through http rest endpoint for order %1$s: %2$s', 'woo-vipps'), $order->get_id(), $response_msg), 'error');
3615 }
3616 }
3617
3618 // This must happen *after* finalization for Express, as above. IOK 2026-05-06
3619 // the only status we now care about is AUTHORIZED. Previously we had AUTHORISED and RESERVED and RESERVE as well. And SALE.
3620 if ($vippsstatus == 'AUTHORIZED') {
3621 $this->payment_complete($order);
3622 } else if ($vippsstatus == 'SALE') {
3623 // Direct capture needs special handling because most of the meta values we use are missing IOK 2019-02-26
3624 // Actually not supported anymore, but keep logic. IOK 2025-08-13
3625 // Still supported for finnish direct bank transfer. IOK 2026-04-22
3626 $order->add_order_note(sprintf(__('Payment captured directly at %1$s', 'woo-vipps'), $this->get_payment_method_name()));
3627 $order->payment_complete();
3628 $this->update_vipps_payment_details($order);
3629 } else {
3630 // Not ok status; set to failed/cancelled
3631 $order_is_retryable = $allow_retry && Vipps::order_is_vipps_retryable($order->get_id());
3632 $status_on_fail = $this->get_option('status_on_fail');
3633 $cancel_on_fail = apply_filters('woo_vipps_cancel_failed_orders', false, $order, $vippsstatus);
3634 if ($cancel_on_fail || !$order_is_retryable) {
3635 $status_on_fail = 'cancelled';
3636 }
3637 if (!in_array($status_on_fail, ['cancelled', 'failed'])) {
3638 /* translators: %1 = order status parameter. 'cancelled' is woocommerce order status name */
3639 $this->log(__('Unsupported status for payment failure of \'%1$s\', falling back to cancelled.', 'woo-vipps'), 'warning');
3640 $status_on_fail = 'cancelled';
3641 }
3642
3643 /* translators: company name */
3644 $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
3645 }
3646
3647 $order->save();
3648 clean_post_cache($order_id);
3649 }
3650
3651 /* finalize shipping for express/checkout order. LP 2026-03-30 */
3652 public function rest_order_set_shipping($request) {
3653 $order_id = $request->get_param('order_id');
3654 $data = $request->get_param('vipps_order_data');
3655
3656 $order = wc_get_order($order_id);
3657 if (!is_a($order, 'WC_Order')) {
3658 return new WP_Error('order_not_found', __('Order not found', 'woo-vipps'), ['status' => 404]);
3659 }
3660
3661 $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3662 $shipping_set = $order->get_meta('_vipps_shipping_set');
3663 if (!$is_express_or_checkout || $shipping_set) {
3664 return new WP_Error('order_is_finalized', __('Order does not need to set shipping', 'woo-vipps'), ['status' => 409]);
3665 }
3666
3667 // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3668 // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3669 // made to the session here will be saved. Sorry.
3670 // UPDATE: Should be no more race condition since we moved callback into the action scheduler, and this shipping finalization into this rest endpoint. LP 2026-03-30
3671 Vipps::instance()->callback_restore_session($order_id);
3672
3673 // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3674 // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
3675 $data = $this->ensure_userDetails($data, $order);
3676
3677 // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3678 // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3679 // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3680 // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3681 // we have added a scope of any kind. This is therefore probably dead code.
3682 // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3683 if (!isset($data['userDetails'])) {
3684 // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3685 try {
3686 $details = $this->get_payment_details($order);
3687 $data = $details;
3688 } catch (Exception $e) {
3689 $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3690 $this->log($e->getMessage());
3691 }
3692 }
3693
3694 // Epayment Express Checkout is of course also significantly different from both the old Express and from Checkout in the formatting here. IOK 2025-08-12
3695 $data = $this->normalizeShippingDetails($data, $order);
3696
3697 // We should now always have shipping details.
3698 if (isset($data['shippingDetails'])) {
3699 $billing = isset($data['billingDetails']) ? $data['billingDetails'] : false;
3700 $this->set_order_shipping_details($order,$data['shippingDetails'], $data['userDetails'], $billing, $data);
3701 }
3702 }
3703
3704 // Do the 'payment_complete' logic for non-SALE orders IOK 2020-09-22
3705 public function payment_complete($order,$transactionid='') {
3706 // Orders not needing processing can be autocaptured, so try to do so now.
3707 $autocapture = $this->maybe_complete_payment($order);
3708 if (!$autocapture) {
3709 add_filter('woocommerce_payment_complete_order_status',
3710 function ($status, $orderid, $order) {
3711 return $this->after_vipps_order_status($order);
3712 }, 99, 3);
3713 $order->add_order_note(sprintf(__('Payment authorized at %1$s', 'woo-vipps'), $this->get_payment_method_name()));
3714 }
3715 $order->payment_complete();
3716 }
3717
3718 // Hook run by Woo after order is complete (authorized or sale). We'll add receipt info etc here.
3719 public function order_payment_complete ($orderid) {
3720 $order = wc_get_order($orderid);
3721 if (!is_a($order, 'WC_Order')) return false;
3722 if (! Vipps::is_vipps_order($order)) return false;
3723
3724 $do_order_management = apply_filters('woo_vipps_order_management_on_payment_complete', true, $orderid);
3725 if (!$do_order_management) return;
3726
3727 // We do the actual order management call in a separate request which we call non-blocking to avoid it locking
3728 // up the users' session on return from the store. This won't work if your Wordpress doesn't support non-blocking calls,
3729 // so in this case, allow the user to set a longer timeout (5 seconds should be plenty.) IOK 2022-07-01
3730 // Yes, it will block until timeout even if non-blocking is set. IOK 2022-07-01
3731 $timeout = apply_filters('woo_vipps_asynch_timeout', 0.5);
3732 $data = [ 'action'=> 'woo_vipps_order_management','orderid'=>$orderid, 'orderkey' => $order->get_order_key() ];
3733 $args = array( "method" => "POST", "body"=>$data, "timeout"=>$timeout, "blocking" => false);
3734 $url = admin_url('admin-post.php');
3735 $asynch = wp_remote_request($url,$args);
3736 if (is_wp_error($asynch)) {
3737 $this->log(__("Error calling the Order Management API: %1\$s ", 'woo-vipps'), $asynch->get_error_message());
3738 }
3739 }
3740
3741 // The below actions *may* be long-running, and we can't be sure
3742 // they will happen at callback-time. Some user may be affected, even if using wp-cron.
3743 // We therefore set them up to be run at shutdown, as soon as payment complete is done
3744 public function payment_complete_at_shutdown ($orderid, $orderkey) {
3745 // Ensure consistent environment here
3746 global $Vipps;
3747 if (!$Vipps) $Vipps = Vipps::instance();
3748 $order = wc_get_order($orderid);
3749 if (!is_a($order, 'WC_Order')) {
3750 return false;
3751 }
3752 if (! Vipps::is_vipps_order($order)) return false;
3753 if ($order->get_order_key() != wc_clean($orderkey)) {
3754 return false;
3755 }
3756 try {
3757
3758 $sendreceipt = apply_filters('woo_vipps_send_receipt', ($this->get_option('sendreceipts') == 'yes'), $order);
3759 if ($sendreceipt) {
3760 $this->api->add_receipt($order);
3761 $this->order_add_vipps_categories($order);
3762 }
3763 do_action('woo_vipps_payment_complete_at_shutdown', $order, $this);
3764 } catch (Exception $e) {
3765 // This is/should be non-critical so just log it.
3766 $this->log(sprintf(__("Could not do all payment-complete actions on %1\$s order %2\$d: %3\$s ", 'woo-vipps'), Vipps::CompanyName(), $orderid, $e->etMessage()), "error");
3767 }
3768 }
3769
3770 // This is run on payment complete. Per default will it only add a link to the order confirmation page, but
3771 // the hook added should be used if selling tickets, bookings etc to add these to the Vipps app receipt; with images if desired (eg. QR images).
3772 public function order_add_vipps_categories ($order) {
3773 if (!is_a($order, 'WC_Order')) return;
3774
3775 $none = ['link'=>null, 'image'=>null, 'imagesize'=>null];
3776 $orderconfirmation = ['link' => $this->get_return_url($order), 'image' => null, 'imagesize'=>null];
3777
3778 $receipt_image = $this->get_option('receiptimage');
3779 if ($receipt_image) {
3780 $orderconfirmation['image'] = intval($receipt_image);
3781 $orderconfirmation['imagesize'] = 'full';
3782 }
3783
3784 // Do these in this order, in case we get terminated at some point during processing
3785 $default = ['TICKET'=>$none,'ORDER_CONFIRMATION' => $orderconfirmation, 'RECEIPT' => $none,"BOOKING" => $none, "DELIVERY" => $none, "GENERAL" => $none];
3786 $categories = apply_filters('woo_vipps_add_order_categories', $default, $order, $this);
3787
3788 foreach ($categories as $category=>$data) {
3789 if ($data['link']) {
3790 $this->order_add_vipps_category($category, $order, $data['link'], $data['image'], $data['imagesize']);
3791 }
3792 // Let people handle this in other ways if not using filter above IOK 2022-07-01
3793 do_action('woo_vipps_add_order_category', $category, $order, $this);
3794
3795 }
3796 }
3797
3798 // Use the Order Management API to add a link with a category name and an optional image, which is viewable in the order.
3799 public function order_add_vipps_category($categoryname, $order, $link, $image=null, $imagesize='medium') {
3800 $imageid = $image ? $this->add_vipps_image($image, $imagesize) : null;
3801 return $this->api->add_category($order, $link, $imageid, $categoryname);
3802 }
3803
3804 // Use the Order Management API to upload an image that can be attached to the order.
3805 public function add_vipps_image ($imagespec, $size = 'medium') {
3806 // Imagespec is an attachmend id (of an image) or a filename (to an image), and a size if using an attachment id.
3807 $filename = null;
3808 $imageid = 0;
3809 $imagefile = "";
3810 $mime = "";
3811 $accepted_types = ["image/jpeg", "image/png", "image/jpg"]; // Only accepted types at Vipps
3812 $vippsid = null;
3813 $uploads = wp_get_upload_dir();
3814
3815 if (is_numeric($imagespec)) {
3816 // Don't send same image twice if we have an id
3817 $stored = get_post_meta($imagespec, '_vipps_imageid', true);
3818 if ($stored && !$this->is_test_mode()) {
3819 return $stored;
3820 }
3821 $imageid = intval($imagespec);
3822 $imagefile = get_attached_file($imageid);
3823 $mime = get_post_mime_type($imageid);
3824 }
3825
3826 if (!is_file($imagefile) || !in_array($mime, $accepted_types)) {
3827 $this->log(sprintf(__('%1$s is not an image that can be uploaded to %2$s', 'woo-vipps'), $imagefile, Vipps::CompanyName()), 'error');
3828 $this->log(sprintf(__('File type was %1$s; supported types are %2$s', 'woo-vipps'), $mime, join(", ", $accepted_types)), 'error');
3829 return null;
3830 }
3831
3832 if ($imageid) {
3833 $intermediate = image_get_intermediate_size($imageid, $size);
3834 if ($intermediate && isset($intermediate['path'])) {
3835 $imagefile = join(DIRECTORY_SEPARATOR, [$uploads['basedir'] , $intermediate['path']]);
3836 }
3837 }
3838
3839 if ($imagefile) {
3840 // Check image dimensions before uploading
3841 $dimensions = getimagesize($imagefile);
3842 if ($dimensions && $dimensions[1] < 167) { // [1] is height
3843 $this->log(sprintf(__('Image %1$s is too small - height %2$dpx (minimum 167px required)', 'woo-vipps'),
3844 $imagefile, $dimensions[1]), 'error');
3845 return null;
3846 }
3847
3848 $vippsid = $this->api->add_image($imagefile);
3849 if ($vippsid) {
3850 update_post_meta($imageid, '_vipps_imageid', $vippsid);
3851 }
3852 }
3853 return $vippsid;
3854 }
3855
3856 // For the express checkout mechanism, create a partial order without shipping details by simulating checkout->create_order();
3857 // IOK 2018-05-25
3858 public function create_partial_order($ischeckout=false) {
3859 // This is neccessary for some plugins, like Yith Dynamic Pricing, that adds filters to get_price depending on whether or not ischeckout is true.
3860 // so basically, since we are impersonating WC_Checkout here, we should define this constant too. IOK 2020-07-03
3861 wc_maybe_define_constant('WOOCOMMERCE_CHECKOUT', true );
3862
3863 // In *some* cases you may need to actually load classes and reload the cart, because some plugins do not load when DOING_AJAX.
3864 do_action('woo_vipps_express_checkout_before_calculate_totals');
3865 WC()->cart->calculate_fees();
3866 WC()->cart->calculate_totals();
3867 do_action('woo_vipps_before_create_express_checkout_order', WC()->cart);
3868
3869 // We store this in the order so we don't have to access the cart when initiating payment. This allows us to restart orders etc.
3870 $needs_shipping = WC()->cart->needs_shipping();
3871
3872 $contents = WC()->cart->get_cart_contents();
3873 $contents = apply_filters('woo_vipps_create_express_checkout_cart_contents',$contents);
3874 try {
3875 $cart_hash = md5(json_encode(wc_clean($contents)) . WC()->cart->total);
3876 $order = new WC_Order();
3877 $order->set_status('pending');
3878 $order->set_payment_method($this);
3879 if ($ischeckout) {
3880 $order->update_meta_data('_vipps_api', 'epayment');
3881 $order->set_payment_method_title('Vipps Checkout');
3882 } else {
3883 $order->set_payment_method_title('Vipps Express Checkout');
3884 }
3885 // We use 'checkout' as the created_via key as per requests, but allow merchants to use their own. IOK 2022-09-15
3886 $created_via = apply_filters('woo_vipps_express_checkout_created_via', 'checkout', $order, $ischeckout);
3887 $order->set_created_via($created_via);
3888
3889 $dummy = sprintf(__('Vipps Express Checkout', 'woo-vipps')); // this is so gettext will find this string.
3890 $dummy = sprintf(__('Vipps Checkout', 'woo-vipps')); // this is so gettext will find this string.
3891
3892 $order->update_meta_data('_vipps_express_checkout',1);
3893
3894 # To help with address fields, scope etc in inititate payment
3895 $order->update_meta_data('_vipps_needs_shipping', $needs_shipping);
3896
3897 $order->set_customer_id( apply_filters('woocommerce_checkout_customer_id', get_current_user_id() ) );
3898 $order->set_currency( get_woocommerce_currency() );
3899 $order->set_prices_include_tax('yes' === get_option('woocommerce_prices_include_tax') );
3900 $order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
3901 $order->set_customer_user_agent( wc_get_user_agent() );
3902 $order->set_discount_total( WC()->cart->get_discount_total());
3903 $order->set_discount_tax( WC()->cart->get_discount_tax() );
3904 $order->set_cart_tax( WC()->cart->get_cart_contents_tax() + WC()->cart->get_fee_tax() );
3905
3906 // Use these methods directly - they should be safe.
3907 WC()->checkout->create_order_line_items( $order, WC()->cart);
3908 WC()->checkout->create_order_fee_lines( $order, WC()->cart);
3909 WC()->checkout->create_order_tax_lines( $order, WC()->cart);
3910 WC()->checkout->create_order_coupon_lines( $order, WC()->cart);
3911 do_action('woo_vipps_before_calculate_totals_partial_order', $order);
3912 $order->calculate_totals(true);
3913
3914 // Added to support third-party plugins that wants to do stuff with the order before it is saved. IOK 2020-07-03
3915 do_action('woocommerce_checkout_create_order', $order, array());
3916
3917 $orderid = $order->save();
3918
3919 do_action('woo_vipps_express_checkout_order_created', $orderid);
3920
3921 // Normally done by the WC_Checkout::create_order method, so call it here too. IOK 2018-11-19
3922 do_action('woocommerce_checkout_update_order_meta', $orderid, array());
3923
3924 // It isn't possible to remove the javascript or 'after order notice' actions, because these are added as closures
3925 // before anything else is run. But we can disable the hook that saves data. IOK 2024-01-18
3926 if (WC_Gateway_Vipps::instance()->get_option('vippsorderattribution') != 'yes') {
3927 remove_all_filters( 'woocommerce_order_save_attribution_data');
3928 }
3929
3930 // And another one. IOK 2021-11-24
3931 do_action('woocommerce_checkout_order_created', $order );
3932 } catch ( Exception $e ) {
3933 if ( $order && $order instanceof WC_Order ) {
3934 $order->get_data_store()->release_held_coupons( $order );
3935 do_action('woocommerce_checkout_order_exception', $order );
3936 }
3937 // Any errors gets passed upstream IOK 2021-11-24
3938 throw $e;
3939 }
3940 return $orderid;
3941 }
3942
3943 // The order attribution subsystem of Woo requires extra work for our partial orders. IOK 2024-01-09
3944 // The attribution data is added to the order forms with prefixes (after 'order note') and we need to strip the prefix.
3945 public function get_order_attribution_data($input_data) {
3946 $prefix = (string) apply_filters( 'wc_order_attribution_tracking_field_prefix', 'wc_order_attribution_');
3947 $prefix = trim( $prefix, '_' ) . "_";
3948 $len = strlen($prefix);
3949 $params = [];
3950 foreach($input_data as $key=>$val) {
3951 $found = strpos($key, $prefix);
3952 if ($found === 0) {
3953 $paramkey = substr($key, $len);
3954 $params[$paramkey] = $val;
3955 }
3956 }
3957 return $params;
3958 }
3959
3960 public function save_session_in_order($order) {
3961 // The callbacks from Vipps carry no session cookie, so we must store this in the order and use a special session handler when in a callback.
3962 // The Vipps class will restore the session from this on callbacks.
3963 // IOK 2019-10-21
3964 $sessioncookie = array();
3965 $sessionhandler = WC()->session;
3966 if ($sessionhandler && is_a($sessionhandler, 'WC_Session_Handler')) {
3967 // If customer is actually logged in, take note IOK 2019-10-25
3968 WC()->session->set('express_customer_id',get_current_user_id());
3969 WC()->session->save_data();
3970 $sessioncookie=$sessionhandler->get_session_cookie();
3971 } else {
3972 // This actually can't happen. IOK 2020-04-08. Branch added for debugging only.
3973 }
3974
3975 if (!empty($sessioncookie)) {
3976 // Customer id, session expiration, session-epiring and cookie-hash is the contents. IOK 2019-10-21
3977 $order->update_meta_data('_vipps_sessiondata',json_encode($sessioncookie));
3978 $order->save();
3979 }
3980
3981 }
3982
3983 // Using this internally to allow the 'enable' button or not. Checks SSL in addition to currency,
3984 // is valid_for_use can in principle run on a http version of the page; we only need to have https accessible for callbacks,
3985 // but if so, admin should definitely be HTTPS so we just check that. IOK 2018-06-06
3986 public function can_be_activated () {
3987 if (!is_ssl() && !preg_match("!^https!i",home_url())) return false;
3988 return true;
3989 }
3990
3991 // Used by the ajax thing that 'sets activated' - checks that it can be activated and that all keys are present. IOK 2018-06-06
3992 function needs_setup() {
3993 if (!$this->can_be_activated()) return true;
3994 $required = array('merchantSerialNumber','clientId', 'secret', 'Ocp_Apim_Key_eCommerce');
3995 foreach ($required as $key) {
3996 if (!$this->get_option($key)) return true;
3997 }
3998 return false;
3999 }
4000
4001 // Not present in WooCommerce until 3.4.0. Should be deleted when required versions are incremented. IOK 2018-10-26
4002 public function update_option( $key, $value = '') {
4003 if ( empty( $this->settings ) ) {
4004 $this->init_settings();
4005 }
4006 $this->settings[ $key ] = $value;
4007 return update_option( $this->get_option_key(), apply_filters('woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings ), 'yes');
4008 }
4009
4010
4011 public function admin_options() {
4012 $currency = get_woocommerce_currency();
4013 $payment_method = $this->get_payment_method_name();
4014
4015 if (!$this->can_be_activated()) {
4016 $this->update_option('enabled', 'no');
4017 }
4018
4019 if ($payment_method != "Vipps"): ?>
4020 <style>
4021 #woocommerce_vipps_express_options, #woocommerce_vipps_express_options-table {
4022 display: none;
4023 }
4024 </style>
4025 <?php endif; ?>
4026
4027 <h2 id='vipps-settings-page'><?php echo __(Vipps::CompanyName(),'woo-vipps'); ?> <img style="float:right;max-height:40px;margin-top:-15px" alt="<?php _e($this->title,'woo-vipps'); ?>" src="<?php echo $this->icon; ?>"></h2>
4028 <?php $this->display_errors(); ?>
4029
4030 <?php
4031
4032
4033 if (!$this->payment_method_supports_currency($payment_method, $currency)):
4034 ?>
4035 <div class="inline error">
4036 <p><strong><?php echo sprintf(__('%1$s does not support your currency.', 'woo-vipps'), $payment_method); ?></strong>
4037 <br>
4038 <?php echo sprintf(__('%1$s supported currencies: %2$s', 'woo-vipps'), $payment_method, implode(", ", $this->get_supported_currencies($payment_method))); ?>
4039 </p>
4040 </div>
4041 <?php endif; ?>
4042
4043 <?php if (!is_ssl() && !preg_match("!^https!i",home_url())): ?>
4044 <div class="inline error">
4045 <p><strong><?php _e('Gateway disabled', 'woocommerce'); ?></strong>:
4046 <?php echo sprintf(__('%1$s requires that your site uses HTTPS.', 'woo-vipps'), $payment_method); ?>
4047 </p>
4048 </div>
4049 <?php endif; ?>
4050
4051 <?php // We will only show the Checkout options if the user has activated the feature (thus creating the pages involved etc). IOK 2021-10-01
4052 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
4053 ?>
4054
4055 <?php /* We will *not* allow vipps checkout to be activated at this point, since the product is no longer sold. IOK 2026-04-30 */ ?>
4056 <?php if (!$vipps_checkout_activated): ?>
4057
4058 <?php endif; ?>
4059 <table class="form-table">
4060 <?php $this->generate_settings_html(); ?>
4061 </table> <?php
4062 }
4063
4064 // Validate/mangle input fields
4065 function validate_text_field ($key, $value) {
4066 if ($key != 'orderprefix') return parent::validate_text_field($key,$value);
4067 $value = preg_replace('![^a-zA-Z0-9]!','',$value);
4068 return $value;
4069 }
4070 function validate_checkbox_field($key,$value) {
4071 if ($key == 'testmode' && VIPPS_TEST_MODE) {
4072 return "yes";
4073 } else if ($key == 'developermode' && VIPPS_TEST_MODE) {
4074 return "yes";
4075 } else if ($key == 'enabled') {
4076 if ($value && $this->can_be_activated()) return 'yes';
4077 return "no";
4078 }
4079 return parent::validate_checkbox_field($key,$value);
4080 }
4081
4082 function process_admin_options () {
4083 // Handle options updates
4084 $saved = parent::process_admin_options();
4085 // We may have changed the number of form fields at this point if dev mode was changed
4086 // from off to on,so re-initialize the form fields here. IOK 2019-09-03
4087 $this->init_form_fields();
4088
4089 // Reinitialize keysets in case user added/changed these
4090 $this->keyset = null;
4091 delete_transient('_vipps_keyset');
4092 $keyset = $this->get_keyset();
4093
4094 // IOK FIXME check if we are called using ajax for this; and if so add the notifications to a list of notifications
4095 // instead of doing the adminerr/adminnotify thing. IOK 2024-01-03
4096
4097 list($ok,$msg) = $this->check_connection();
4098 if ($ok) {
4099 $this->adminnotify(sprintf(__("Connection to %1\$s is OK", 'woo-vipps'), Vipps::CompanyName()));
4100 } else {
4101 $this->adminerr(sprintf(__("Could not connect to %1\$s", 'woo-vipps'), Vipps::CompanyName()) . ": $msg");
4102 }
4103
4104 if ($ok) {
4105 $this->check_webhooks();
4106 // Try to ensure we have webhooks defined for the epayment-api IOK 2023-12-19
4107 $hooks = $this->initialize_webhooks();
4108 }
4109
4110 // If enabling this, ensure the page in question exists
4111 if ($this->get_option('vipps_checkout_enabled') == 'yes') {
4112 update_option('woo_vipps_checkout_activated', true, true); // This must be true here, but still, make sure
4113 Vipps::instance()->maybe_create_vipps_pages();
4114 }
4115
4116 // Ensure special page has the necessary shortcode. LP 2026-09-01
4117 $special_page = get_post(Vipps::get_special_page_id());
4118 if ($special_page && !has_shortcode($special_page->post_content, 'vipps_special_page')) {
4119 $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4120 wp_update_post([
4121 'ID' => Vipps::get_special_page_id(),
4122 'post_content' => $new_content,
4123 ]);
4124 } else if (!Vipps::get_special_page_id()) {
4125 // We shouldn't really get here, the page should be ensured to exist in init. LP 2026-09-03
4126 /* translators: %s is current method name */
4127 $this->log(sprintf(__('Missing special page in %s, attempting to fix', 'woo-vipps'), 'process_admin_options'), 'warning');
4128 Vipps::instance()->ensure_special_page_exists();
4129 }
4130
4131 return $saved;
4132 }
4133
4134 // Check our stored webhooks for consistency, which means the callback URLs should point to *this* site. If they don't,
4135 // delete all the ones pointing wrong. If this returns false, you should reinitialize the webhooks. IOK 2023-12-20
4136 public function check_webhooks () {
4137 $local_hooks = get_option('_woo_vipps_webhooks');
4138 if (!$local_hooks) return false;
4139
4140 $callback = $this->webhook_callback_url();
4141 $callback_compare = strtok($callback, '?');
4142 $problems = false;
4143
4144 // We are going to re-initialize our webhooks after this, but just to ensure we're not keeping any 'stale' hooks,
4145 // we'll update the local hooks too. IOK 2025-02-13
4146 $change = false;
4147 $msns = array_keys($local_hooks);
4148 foreach($msns as $msn) {
4149 $hooks = $local_hooks[$msn];
4150 $ids = array_keys($hooks);
4151 foreach ($ids as $id) {
4152 $hook = $hooks[$id];
4153 $noargs = strtok($hook['url'], '?');
4154 if ($noargs == $callback_compare) continue; // This hook is good, probably, unless somebody has deleted it
4155 try {
4156 $this->log(sprintf(__("For msn %s we have a webhook %s %s which is pointed the wrong way (%s) for this website", 'woo-vipps'), $msn, $hook['id'], $hook['url'], $callback_compare));
4157 $this->api->delete_webhook($msn, $hook['id']); // This isn't - it's pointed the wrong way, which means we have changed name of the site or something
4158 } catch (Exception $e) {
4159 $this->log(sprintf(__("Could not delete webhook for this site with url '%2\$s' : %1\$s", 'woo-vipps'), $e->getMessage(), $noargs), 'error');
4160 }
4161 unset($hooks[$id]);
4162 $change = true;
4163 $problems = true;
4164 }
4165
4166 if ($change) {
4167 if (empty($hooks)) {
4168 unset($local_hooks[$msn]);
4169 } else {
4170 $local_hooks[$msn] = $hooks;
4171 }
4172 }
4173 }
4174
4175 if ($change) {
4176 update_option('_woo_vipps_webhooks', $local_hooks, true);
4177 }
4178
4179 if ($problems) return false;
4180 return true;
4181 }
4182
4183 // Returns the local webhooks for the given msn. If the url has changed, it will return nothing. IOK 2023-12-19
4184 public function get_local_webhook($msn) {
4185 $local_hooks = get_option('_woo_vipps_webhooks');
4186 $hooks = $local_hooks[$msn] ?? [];
4187 $callback = $this->webhook_callback_url();
4188 $callback_compare = strtok($callback, '?');
4189
4190 foreach ($hooks as $id=>$hook) {
4191 $noargs = strtok($hook['url'], '?');
4192 if ($noargs == $callback_compare) {
4193 return $hook;
4194 } else {
4195 // May want to log this somehow
4196 }
4197 }
4198 return null;
4199 }
4200
4201
4202 // This is to be used in deactivate/uninstall - it deletes all webhooks for all MSNs for this instance
4203 // Unfortunatetly, we can't delete other msn's webhooks or webhooks pointing to other URLs. IOK 2023-12-20
4204 public function delete_all_webhooks() {
4205 delete_option('_woo_vipps_webhooks');
4206 $callback = $this->webhook_callback_url();
4207 $comparandum = strtok($callback, '?');
4208 $all_hooks = $this->get_webhooks_from_vipps();
4209 foreach($all_hooks as $msn => $data) {
4210 $hooks = $data['webhooks'] ?? [];
4211 foreach ($hooks as $hook) {
4212 $id = $hook['id'];
4213 $url = $hook['url'];
4214 $noargs = strtok($url, '?');
4215 if ($noargs != $comparandum) continue; // Some other shops hook, we will ignore it
4216 $ok = $this->api->delete_webhook($msn,$id);
4217 }
4218 }
4219 }
4220
4221 // This will initalize the webhooks for this instance, for all MSNs that are configured.
4222 // Hooks that point to us that we *do not* know the secret for, have to be deleted.
4223 public function initialize_webhooks() {
4224 // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4225 try {
4226 return $this->initialize_webhooks_internal();
4227 } catch (Exception $e) {
4228 $this->log(sprintf(__("Could not initialize webhooks for this site: %1\$s", 'woo-vipps'), $e->getMessage()), 'error');
4229 return [];
4230 }
4231 }
4232
4233 private function initialize_webhooks_internal () {
4234 $local_hooks = get_option('_woo_vipps_webhooks');
4235 $all_hooks = $this->get_webhooks_from_vipps();
4236 $ourselves = $this->webhook_callback_url();
4237 $keysets = $this->get_keyset();
4238
4239 // Ignore any extra arguments
4240 $comparandum = strtok($ourselves, '?');
4241
4242 $change = false;
4243
4244 // We may need to delete webhooks that have been orphaned. There should be exactly one
4245 // for this sites' callback, and we need to know its secret. All others should be deleted.
4246 $delenda = [];
4247
4248
4249 foreach($all_hooks as $msn => $data) {
4250 $hooks = $data['webhooks'] ?? [];
4251 $gotit = false;
4252 $locals = $local_hooks[$msn] ?? [];
4253
4254 foreach ($hooks as $hook) {
4255 $id = $hook['id'];
4256 $url = $hook['url'];
4257 $noargs = strtok($url, '?');
4258 if ($noargs != $comparandum) {
4259 continue; // Some other shops hook, we will ignore it
4260 }
4261 $local = $locals[$id] ?? false;
4262
4263 // If we haven't gotten our hook yet, but we have a local hook now that we know a secret for, note it and continue
4264 if (!$gotit && $local && isset($local['secret'])) {
4265 $gotit = $local;
4266 continue;
4267 }
4268 // Now we have a hook for our own msn and url, but either we don't know the secret or it is a duplicate. It should be deleted.
4269 $delenda[] = $id;
4270 }
4271
4272 // Delete all the webhooks for this msn that we don't want
4273 foreach ($delenda as $wrong) {
4274 $change = true;
4275 $this->api->delete_webhook($msn,$wrong);
4276 }
4277
4278 if ($gotit) {
4279 // Now if we got a hook, then we should *just* remember that for this msn.
4280 $local_hooks[$msn] = array($gotit['id'] => $gotit);
4281 } else {
4282 // If not, we don't have a hook for this msn and site, so we need to (try to) create one
4283 // but only if the MSN is registered for the payment gateway "vipps" ! IOK 2024-12-03
4284 $keys = $keysets[$msn] ?? [];
4285 $gateway = $keys['gw'] ?? 'vipps';
4286
4287 if ($gateway == 'vipps') {
4288 $change = true;
4289 $result = $this->api->register_webhook($msn, $ourselves);
4290 if ($result) {
4291 $local_hooks[$msn] = [$result['id'] => ['id'=>$result['id'], 'url' => $ourselves, 'secret' => $result['secret']]];
4292 }
4293 }
4294 }
4295 }
4296 update_option('_woo_vipps_webhooks', $local_hooks, true);
4297
4298
4299 if ($change) $all_hooks = $this->get_webhooks_from_vipps();
4300
4301 return $all_hooks;
4302 }
4303
4304
4305 // Checks connection of the 'main' MSN IOK 2023-12-19
4306 public function check_connection ($msn = null) {
4307 if (!$msn) {
4308 $msn = $this->get_merchant_serial();
4309 }
4310 $at = $this->get_key($msn);
4311 $s = $this->get_secret($msn);
4312 $c = $this->get_clientid($msn);
4313 if ($at && $s && $c) {
4314
4315
4316
4317 try {
4318 // First, test the client id / client secret which will give us an access token
4319 $token = $this->api->get_access_token($msn,'force');
4320 if ($token) {
4321 // Then, call the webhooks api to check if the msn/sub key is ok
4322 try {
4323 $this->api->get_webhooks_raw($msn);
4324 update_option('woo-vipps-configured', 1, true);
4325 return array(true,'');
4326 } catch (Exception $e) {
4327 $msg = $e->getMessage();
4328 if ($msg == "403 Forbidden") {
4329 $msg= __("MSN or subscription key (or both) seem to be wrong: ", 'woo-vipps') . $msg;
4330 }
4331 update_option('woo-vipps-configured', 0, true);
4332 return array(false, $msg);
4333 }
4334 }
4335
4336 } catch (Exception $e) {
4337 $msg = __("Your client key or secret is wrong.", 'woo-vipps');
4338 update_option('woo-vipps-configured', 0, true);
4339 return array(false, $msg);
4340 }
4341 }
4342 return array(false, ''); // No configuration
4343 }
4344
4345 public function log ($what,$type='info') {
4346 $logger = function_exists('wc_get_logger') ? wc_get_logger() : false;
4347 if ($logger) {
4348 $context = array('source'=>'woo-vipps');
4349 $logger->log($type,$what,$context);
4350 } else {
4351 error_log("woo-vipps ($type): $what");
4352 }
4353 }
4354
4355 // Ensure chosen name gets used in the checkout page IOK 2018-09-12
4356 public function get_title() {
4357 return apply_filters('woo_vipps_payment_method_title', $this->get_payment_method_name());
4358 }
4359
4360 public function get_payment_method_name() {
4361 return $this->get_option('payment_method_name');
4362 }
4363
4364 public function payment_fields() {
4365 // Use Billing Phone if it is required, otherwise ask for a phone IOK 2018-04-24
4366 // For v2 of the api, just let Vipps ask for then umber
4367 // IOK 2019-09-12 removed dead code only used for v1 of api
4368 // This just prints a description of the payment method.
4369 print $this->get_option('description');
4370 return;
4371 }
4372 public function validate_fields() {
4373 return true;
4374 }
4375
4376
4377 }
4378
4379