PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.6
Pay with Vipps and MobilePay for WooCommerce v6.2.6
6.2.6 6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 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 All 189 releases
← All changes | payment/WC_Gateway_Vipps.class.php +350 -259 6.0.5 → 6.2.6 View file →
@@ -215,8 +215,26 @@
215 215 add_action('woocommerce_order_status_refunded', array($this, 'maybe_refund_order'), 9, 1);
216 216
217 217 // Possibly delete orders that never went anywhere
218 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 +
219 237 // Handle orders when authorized
220 238 add_action('woocommerce_payment_complete', array($this, 'order_payment_complete'), 10, 1);
221 239
222 240 // when an order is complete, we need to check if there is reserved amount that is not captured
@@ -225,8 +243,54 @@
225 243 // Also for orders that have been partially or completely refunded, or need to be set to cancelled IOK 2026-01-26
226 244 add_action('woocommerce_order_status_completed', array($this, 'maybe_cancel_reserved_amount'), 99);
227 245 add_action('woocommerce_order_status_refunded', array($this, 'maybe_cancel_reserved_amount'), 99, 1);
228 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 + });
229 293 }
230 294
231 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
232 296 // if there still is money reserved, then this amount is cancelled PMB 2024-11-21
@@ -234,10 +298,8 @@
234 298 public function maybe_cancel_reserved_amount ($orderid) {
235 299 $order = wc_get_order($orderid);
236 300 if (!$order) return;
237 301 if (! Vipps::is_vipps_order($order)) return false;
238 - // Cannot partially cancel legacy ecom orders
239 - if ('epayment' != $order->get_meta('_vipps_api')) return false;
240 302
241 303 // Check that the normal maybe_capture_order hook has actually ran *and* done something,
242 304 // it's only after this we know we have captured 'everything' so if there is anything left,
243 305 // it should be cancelled. IOK 2025-05-04
@@ -539,9 +601,9 @@
539 601 // Webhook callbacks do not pass GET arguments at all, but do provide an X-Vipps-Authorization header for verification. IOK 2023-12-19
540 602 public function webhook_callback_url () {
541 603 $url = home_url("/", 'https');
542 604 $queryargs = ['callback'=>'webhook'];
543 - $forwhat = 'wc_gateway_vipps'; // Same callback as for ecom, checkout, express checkout
605 + $forwhat = 'wc_gateway_vipps'; // Same callback as for epayment, checkout, express checkout
544 606 // HTTPS required. IOK 2018-05-18
545 607 // If the user for some reason hasn't enabled pretty links, fall back to ancient version. IOK 2018-04-24
546 608 if ( !get_option('permalink_structure')) {
547 609 $queryargs['wc-api'] = $forwhat;
@@ -559,22 +621,8 @@
559 621 }
560 622 public function shipping_details_callback_url($token='',$reference=0) {
561 623 return $this->make_callback_urls('vipps_shipping_details',$token,$reference);
562 624 }
563 - // Callback for the consetn removal callback. Must use template redirect directly, because wc-api doesn't handle DELETE.
564 - // IOK 2018-05-18
565 - public function consent_removal_callback_url () {
566 - $queryargs = [];
567 - $url = home_url("/", 'https');
568 - if ( !get_option('permalink_structure')) {
569 - $queryargs['vipps-consent-removal']=1;
570 - } else {
571 - $url = trailingslashit(home_url('vipps-consent-removal', 'https'));
572 - }
573 - // And we need to add an empty "callback" query arg as the very last arg to receive the actual callback.
574 - // We can't use add_query_arg for that, as an empty argument will remove the equals-sign.
575 - return add_query_arg($queryargs, $url) . "&callback=";
576 - }
577 625
578 626 // Allow user to select the template to be used for the special Vipps MobilePay pages. IOK 2020-02-17
579 627 public function get_theme_page_templates() {
580 628 if (!$this->page_templates) {
@@ -586,22 +634,8 @@
586 634 }
587 635 return $this->page_templates;
588 636 }
589 637
590 - // We can't use get_pages to get a default list of pages for our settings, because it triggers
591 - // actions that can be used by other plugins. Therefore we must use the database directly and cache the results. IOK 2023-08-22
592 - public function get_pagelist () {
593 - if (!$this->page_list) {
594 - global $wpdb;
595 - $page_list = array(''=>__('Use a simulated page (default)', 'woo-vipps'));
596 - foreach($wpdb->get_results("SELECT ID,post_title FROM {$wpdb->prefix}posts WHERE post_type='page' and post_status='publish'") as $page) {
597 - $page_list[$page->ID] = $page->post_title;
598 - }
599 - $this->page_list = $page_list;
600 - }
601 - return $this->page_list;
602 - }
603 -
604 638 // Check to see if the product in question can be bought with express checkout IOK 2018-12-04
605 639 public function product_supports_express_checkout($product) {
606 640 // IOK 2023-12-12 Can only support express checkout for Vipps - not MobilePay (yet!)
607 641 // IOK 2025-09-01 Now supports mobilepay
@@ -661,13 +695,18 @@
661 695
662 696 // True if "Express checkout" should be displayed IOK 2018-06-18
663 697 public function show_express_checkout() {
664 698 if (!$this->express_checkout_available()) return false;
665 - $show = ($this->enabled == 'yes') && ($this->get_option('cartexpress') == 'yes') ;
666 - $show = $show && $this->cart_supports_express_checkout();
699 + $show = 'yes' == $this->enabled && $this->cart_supports_express_checkout();
667 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 + }
668 706 // Earlier, we disabled this if Checkout was active; but we will now respect the setting in all
669 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
670 709
671 710 return apply_filters('woo_vipps_show_express_checkout', $show);
672 711 }
673 712
@@ -748,9 +787,9 @@
748 787 $order->save();
749 788 }
750 789
751 790 // IOK 2024-09-01 In general, we can refund most Vipps Mobilepay orders through the api,
752 - // however, this is not the case for the Bank Transfer method available through Vipps Checkout.
791 + // however, this is not the case for the Bank Transfer method available through Checkout.
753 792 public function can_refund_order( $order ) {
754 793 $method = $order->get_meta('_vipps_api');
755 794 switch ($method) {
756 795 case 'banktransfer':
@@ -758,9 +797,9 @@
758 797 break;
759 798 case 'epayment':
760 799 return true;
761 800 break;
762 - // Default is old-style ecom v2.
801 + // Default is true; but the above are exhaustive IOK 2026-08-18
763 802 default:
764 803 return true;
765 804 break;
766 805 }
@@ -801,9 +840,8 @@
801 840 wc_switch_to_site_locale();
802 841 $the_refund = wc_create_refund($data);
803 842 wc_restore_locale();
804 843 if (is_wp_error($the_refund)) {
805 - $refund_thru_gateway = false;
806 844 $msg = $the_refund->get_error_message();
807 845 $order->add_order_note(sprintf(__("Error when refunding payment through %1\$s:", 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $msg);
808 846 $order->save();
809 847 $this->adminerr($msg);
@@ -1007,9 +1045,8 @@
1007 1045 global $Vipps;
1008 1046
1009 1047 // Used for defaults in the admin interface; however this functions is called a loot more often than that.
1010 1048 $page_templates = $this->get_theme_page_templates();
1011 - $page_list = $this->get_pagelist();
1012 1049
1013 1050 $orderprefix = $Vipps->generate_order_prefix();
1014 1051
1015 1052 // Default handling based on other parameters and earlier values.
@@ -1020,16 +1057,15 @@
1020 1057 if (class_exists('VippsWooLogin')) {
1021 1058 $woodefault = 'yes' === get_option('woocommerce_enable_signup_and_login_from_checkout');
1022 1059 if ($woodefault) {
1023 1060 $expresscreateuserdefault = "yes";
1024 - // $vippscreateuserdefault = "yes"; // However, for Vipps Checkout the email address is freetext so we'll treat the default a bit different.
1061 + // $vippscreateuserdefault = "yes"; // However, for Checkout the email address is freetext so we'll treat the default a bit different.
1025 1062 }
1026 1063 }
1027 1064
1028 - // We will only show the Vipps Checkout options if the user has activated the feature (thus creating the pages involved etc). IOK 2021-10-01
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
1029 1066 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1030 1067
1031 -
1032 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.
1033 1069 $current = get_option('woocommerce_vipps_settings');
1034 1070 // New defaults based on old defaults
1035 1071 $default_static_shipping_for_checkout = 'no';
@@ -1034,8 +1070,9 @@
1034 1070 // New defaults based on old defaults
1035 1071 $default_static_shipping_for_checkout = 'no';
1036 1072 $default_ask_address_for_express = 'no';
1037 1073 $default_status_on_fail = 'failed';
1074 + $default_express_show_in_checkout = 'yes';
1038 1075 if ($current) {
1039 1076 $default_static_shipping_for_checkout = (isset($current['enablestaticshipping'])) ? $current['enablestaticshipping'] : 'no';
1040 1077 $default_ask_address_for_express = (isset($current['useExplicitCheckoutFlow']) && $current['useExplicitCheckoutFlow'] == "yes") ? "yes" : "no";
1041 1078 // The old default used the same value as for Express Checkout. IOK 2023-07-27
@@ -1043,8 +1080,15 @@
1043 1080
1044 1081 // For existing installs: set failed payments order status to cancelled to keep same default behaviour.
1045 1082 // New installs will be set to failed instead of cancelled. LP 2026-03-26
1046 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 + }
1047 1091 }
1048 1092
1049 1093 // Get the already-set country code. For existing sites, this will guess the country based on the currency; for new sites, use
1050 1094 // the woo base country. IOK 2024-10-17 (previously used the currency here too).
@@ -1184,9 +1228,9 @@
1184 1228 'description' => __('Your phone number where Porterbuddy may send you important messages. Format must be MSISDN (including country code). Example: "4791234567"','woo-vipps'),
1185 1229 'default' => '',
1186 1230 ),
1187 1231
1188 - // Vipps checkout *shipping options* - extra shipping options that only work with Vipps Checkout
1232 + // Vipps checkout *shipping options* - extra shipping options that only work with Checkout
1189 1233 'vcs_helthjem' => array(
1190 1234 'title' => __('Helthjem', 'woo-vipps'),
1191 1235 'label' => sprintf(__('Support Helthjem as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1192 1236 'type' => 'checkbox',
@@ -1221,9 +1265,9 @@
1221 1265 ),
1222 1266
1223 1267 );
1224 1268
1225 - /* Support for *certain* external payment methods in Vipps Checkout. IOK 2024-05-27 */
1269 + /* Support for *certain* external payment methods in Checkout. IOK 2024-05-27 */
1226 1270 $externals = [];
1227 1271 $external_payment_fields = [];
1228 1272 $allow_external_payments = $this->allow_external_payments_in_checkout();
1229 1273 if ($allow_external_payments) {
@@ -1416,14 +1460,14 @@
1416 1460 'default' => 'none',
1417 1461 ),
1418 1462 );
1419 1463
1420 - $expressfields = array(
1464 + $expressfields = array(
1421 1465 'express_options' => array(
1422 1466 'title' => sprintf(__('Express Checkout', 'woo-vipps')),
1423 1467 'type' => 'title',
1424 1468 'class' => 'tab',
1425 - 'description' => sprintf(__("%1\$s allows you to buy products by a single click from the cart page 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())
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())
1426 1470 ),
1427 1471
1428 1472 'cartexpress' => array(
1429 1473 'title' => __('Enable Express Checkout in cart', 'woo-vipps'),
@@ -1433,8 +1477,17 @@
1433 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()),
1434 1478 'default' => 'yes',
1435 1479 ),
1436 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 +
1437 1490 'singleproductexpress' => array(
1438 1491 'title' => __('Enable Express Checkout for single products', 'woo-vipps'),
1439 1492 'label' => __('Enable Express Checkout for single products', 'woo-vipps'),
1440 1493 'type' => 'select',
@@ -1537,22 +1590,24 @@
1537 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'),
1538 1591 ),
1539 1592
1540 1593 'vippsspecialpagetemplate' => array(
1541 - 'title' => sprintf(__('Override page template used for the special %1$s pages', 'woo-vipps'), Vipps::CompanyName()),
1594 + 'title' => sprintf(__('Legacy: Override page template used for the special %1$s page', 'woo-vipps'), Vipps::CompanyName()),
1542 1595 'label' => sprintf(__('Use specific template for %1$s', 'woo-vipps'), Vipps::CompanyName()),
1543 1596 'type' => 'select',
1544 1597 'options' => $page_templates,
1545 - 'description' => sprintf(__('Use this template from your theme or child-theme to display all the special %1$s pages. You will probably want a full-width template and it should call \'the_content()\' normally.', 'woo-vipps'), Vipps::CompanyName()),
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()),
1546 1599 'default' => ''),
1547 1600
1601 + // Deprecated, not shown anymore: TODO: remove this option in future. LP 2026-09-01
1548 1602 'vippsspecialpageid' => array(
1549 1603 'title' => sprintf(__('Use a real page ID for the special %1$s pages - neccessary for some themes', 'woo-vipps'), Vipps::CompanyName()),
1550 1604 'label' => __('Use a real page ID', 'woo-vipps'),
1551 1605 'type' => 'select',
1552 - 'options' => $page_list,
1606 + 'options' => [],
1553 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()),
1554 - 'default'=>''),
1608 + 'default' => ''
1609 + ),
1555 1610
1556 1611 'sendreceipts' => array(
1557 1612 'title' => __("Send receipts and order confirmation info to the customers' app on completed purchases.", 'woo-vipps'),
1558 1613 'label' => sprintf(__("Send receipts to the customers %1\$s app", 'woo-vipps'), Vipps::CompanyName()),
@@ -1716,9 +1771,9 @@
1716 1771 $ok = apply_filters('woo_vipps_is_available', $ok, $this);
1717 1772 return $ok;
1718 1773 }
1719 1774
1720 - // True if the alternative Vipps Checkout screen is both available and activated. Returns the page id of the checkout
1775 + // True if the alternative Checkout screen is both available and activated. Returns the page id of the checkout
1721 1776 // page for convenience. IOK 2021-10-01
1722 1777 public function vipps_checkout_available () {
1723 1778
1724 1779 if ($this->get_option('vipps_checkout_enabled') != 'yes') return false;
@@ -1789,9 +1844,8 @@
1789 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');
1790 1845 return [];
1791 1846 }
1792 1847
1793 -
1794 1848 // From the request, get either [billing_phone] => or [vipps phone]
1795 1849 $phone = '';
1796 1850 if (isset($_POST['vippsphone'])) {
1797 1851 $phone = trim(sanitize_text_field($_POST['vippsphone']));
@@ -1884,9 +1938,8 @@
1884 1938 $limited_session = $this->generate_authtoken();
1885 1939 $returnurl = add_query_arg('ls',$limited_session,$returnurl);
1886 1940 $returnurl = add_query_arg('id', $order_id, $returnurl);
1887 1941
1888 -
1889 1942 try {
1890 1943 // If the order was 'failed', it isnt any more! yet!
1891 1944 if ($order->get_status() == 'failed') {
1892 1945 $order->set_status('pending', __('Setting order status to pending to start payment', 'woo-vipps'));
@@ -1934,12 +1987,14 @@
1934 1987 $order->update_meta_data('_vipps_init_timestamp',$vippstamp);
1935 1988 $order->update_meta_data('_vipps_orderurl', $url);
1936 1989
1937 1990 $order->update_meta_data('_vipps_status','INITIATE'); // INITIATE right now
1938 - $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), $this->get_payment_method_name()));
1939 - $order->add_order_note(sprintf(__('Awaiting %1$s payment confirmation','woo-vipps'), $this->get_payment_method_name()));
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 +
1940 1996 $order->save();
1941 -
1942 1997 // Create a signal file that we can check without calling wordpress to see if our result is in IOK 2018-05-04
1943 1998 try {
1944 1999 $Vipps->createCallbackSignal($order);
1945 2000 } catch (Exception $e) {
@@ -1944,9 +1999,8 @@
1944 1999 $Vipps->createCallbackSignal($order);
1945 2000 } catch (Exception $e) {
1946 2001 // Could not create a signal file, but that's ok.
1947 2002 }
1948 -
1949 2003 do_action('woo_vipps_before_redirect_to_vipps',$order_id);
1950 2004
1951 2005 // This will send us to a receipt page where we will do the actual work. IOK 2018-04-20
1952 2006 return array('result'=>'success','redirect'=>$url);
@@ -2075,12 +2129,11 @@
2075 2129 if ($api == 'banktransfer') {
2076 2130 // This is an error - we should not ever get to the 'capture' branch if we are a banktransfer payment.
2077 2131 // IOK 2024-01-09
2078 2132 $content = [];
2079 - } elseif ($api == 'epayment') {
2133 + } else {
2134 + // Now the only other api is 'epayment' IOK 2026-08-18
2080 2135 $content = $this->api->epayment_capture_payment($order,$amount,$requestid);
2081 - } else {
2082 - $content = $this->api->capture_payment($order,$amount,$requestid);
2083 2136 }
2084 2137 } catch (TemporaryVippsApiException $e) {
2085 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');
2086 2139 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . "\n" . $e->getMessage());
@@ -2131,8 +2184,9 @@
2131 2184 return false;
2132 2185 }
2133 2186 // We'll use the same transaction id for all cancel jobs, as we can only do it completely. IOK 2018-05-07
2134 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.
2135 2189 $api = $order->get_meta('_vipps_api');
2136 2190 try {
2137 2191 $requestid = "";
2138 2192 if ($api == 'banktransfer') {
@@ -2137,22 +2191,14 @@
2137 2191 $requestid = "";
2138 2192 if ($api == 'banktransfer') {
2139 2193 // If we are here, and the order is somehow not captured, just do nothing. IOK 2024-01-09
2140 2194 $content = [];
2141 - } elseif ($api == 'epayment') {
2195 + } else {
2196 + // api is here 'epayment'. IOK 2026-07-18
2142 2197 $requestid = 1;
2143 2198 // This will cancel any remaining, not-captured amount IOK 2026-01-28
2144 2199 $content = $this->api->epayment_cancel_payment($order,$requestid);
2145 - } else {
2146 - // If we have captured the order, we can't cancel it with the ecom API IOK 2018-05-07
2147 - $captured = intval($order->get_meta('_vipps_captured'));
2148 - if ($captured>0) {
2149 - $msg = sprintf(__('Cannot cancel a captured %1$s transaction - use refund instead', 'woo-vipps'), "ECOM " . $this->get_payment_method_name());
2150 - $this->adminerr($msg);
2151 - return false;
2152 - }
2153 - $content = $this->api->cancel_payment($order,$requestid);
2154 - }
2200 + }
2155 2201 } catch (TemporaryVippsApiException $e) {
2156 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');
2157 2203 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage());
2158 2204 return false;
@@ -2166,9 +2212,8 @@
2166 2212 // the epay v2 API would return transactionInfo and Summary with the result, the new epayment api returns nothing.
2167 2213 // Removed epay branch 2025-08-12 IOK
2168 2214 $total = intval($order->get_meta('_vipps_amount'));
2169 2215 $captured = intval($order->get_meta('_vipps_captured'));
2170 -# $cancelled = $amount + intval($order->get_meta('_vipps_cancelled');
2171 2216 $cancelled = $total;
2172 2217 $remaining = $total - $captured - $cancelled;
2173 2218
2174 2219 // We need to assume it worked. Also, we can't do partial cancels yet, so just cancel everything.
@@ -2216,13 +2261,12 @@
2216 2261 if ($api == 'banktransfer') {
2217 2262 $msg = sprintf(__("Cannot refund bank transfer order %1\$d", 'woo-vipps'), $order->get_id());
2218 2263 $this->log($msg, 'error');
2219 2264 throw new Exception($msg);
2220 - } elseif ($api == 'epayment') {
2265 + } else {
2266 + // api is now 'epayment' IOK 2026-08-18
2221 2267 $content = $this->api->epayment_refund_payment($order,$requestid,$amount,$cents);
2222 - } else {
2223 - $content = $this->api->refund_payment($order,$requestid,$amount,$cents);
2224 - }
2268 + }
2225 2269
2226 2270 $currency = $order->get_currency();
2227 2271
2228 2272 // Previously, we got updated transaction info in a transactionInfo field. this is no longer provided,
@@ -2406,10 +2450,8 @@
2406 2450 $order->update_meta_data('_vipps_status',$newvippsstatus);
2407 2451
2408 2452 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
2409 2453 if (!empty($paymentdetails)) {
2410 -
2411 -
2412 2454 // checkout has a string, epayment has an array with upper case "type" and apparently, cardBin IOK 2025-08-12
2413 2455 $paymentMethod = $paymentdetails['paymentMethod'] ?? "epayment";
2414 2456 // After normalization, all APIs will have data here.
2415 2457 $details = $paymentdetails['paymentDetails'];
@@ -2622,73 +2664,11 @@
2622 2664 if (in_array($newstatus, ['authorized', 'complete'])) {
2623 2665 $ready = true;
2624 2666 }
2625 2667
2626 -
2627 - // if this is *express - not checkout * and there is no user information, this is probably because we only get that when adding the 'address' scope.
2628 - // if we didn't want the address, we now need to ask for user details using the login get_userinfo api. IOK 2025-08-12
2629 - // This is also the only way to get "email_verified", so we may want to add a setting that always calls this if neccessary. IOK 2025-08-13
2630 - // Also we don't get this when the state is different from AUTHORIZED. Especially not ABORTED.
2631 - // IOK 2025-09-29: This is *no longer the case* . We actually now get userDetails every time we add the relevant scopes,
2632 - // so this is now probably dead code.
2633 - if ($ready && $express && !$checkout_session && !isset($result['userDetails'])) {
2634 -
2635 - $sub = isset($result['profile']) && isset($result['profile']['sub']) ? $result['profile']['sub'] : null;
2636 - $userinfo = [];
2637 - if (!$sub) {
2638 - // This should never happen, but be prepared
2639 - $message = sprintf(__("Could not get user info for order %1\$d using the userinfo API: %2\$s. Please use the 'get complete transaction details' on the button to try to recover this. ", 'woo-vipps'), $order->get_id(), "No 'sub' passed for user ID" );
2640 - $order->add_order_note($message);
2641 - $this->log($message , "error");
2642 - } else {
2643 - // If this happens, the merchant *may* be able to retrieve the information from Vipps so add a note for it.
2644 - try {
2645 - $userinfo = $this->api->get_userinfo($sub);
2646 - } catch (Exception $e) {
2647 - $message = sprintf(__("Could not get user info for order %1\$d using the userinfo API: %2\$s. Please use the 'get complete transaction details' on the button to try to recover this. ", 'woo-vipps'), $order->get_id(), $e->getMessage());
2648 - $order->add_order_note($message);
2649 - $this->log($message, 'woo-vipps', "error");
2650 - }
2651 - }
2652 - if ($userinfo) {
2653 - $userDetails = array(
2654 - 'email_verified' => $userinfo['email_verified'],
2655 - 'email' => $userinfo['email'],
2656 - 'firstName' => $userinfo['given_name'] ?? '',
2657 - 'lastName' => $userinfo['family_name'] ?? '',
2658 - 'mobileNumber' => $userinfo['phone_number'] ?? '',
2659 - 'phoneNumber' => $userinfo['phone_number'] ?? '',
2660 - 'userId' => $userinfo['phone_number'] ?? '',
2661 - 'sub' => $userinfo['sub']
2662 - );
2663 -
2664 - $result['userDetails'] = $userDetails;
2665 -
2666 - // We may have asked for the address of the customer, so add that too, or a dummy.
2667 - if (!isset($result['shippingDetails'])) {
2668 - $countries=new WC_Countries();
2669 - $address =[];
2670 - $address['addressLine1'] = "";
2671 - $address['addressLine2'] = "";
2672 - $address['city'] ="";
2673 - $address['postCode'] = "";
2674 - $address['country'] = $countries->get_base_country();
2675 -
2676 - // This uses other keys than both epayment and checkout, but we'll normalize it later. IOK 2025-08-13
2677 - if (isset($userinfo['address'])) {
2678 - $address['addressLine1'] = $userinfo['address']['street_address'];
2679 - $address['city'] = $userinfo['address']['region'];
2680 - $address['country'] = $userinfo['address']['country'];
2681 - $address['postCode'] = $userinfo['address']['postal_code'];
2682 - }
2683 - $result['shippingDetails'] = ['address' => $address];
2684 - }
2685 - }
2686 - }
2687 -
2688 2668 if ($ready && ($express || $checkout_session)) {
2689 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
2690 - // This will also normalize userDetails, adding 'sub' where required and fields for backwards compatibility. 2025-08-12
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
2691 2671 $result = $this->ensure_userDetails($result, $order);
2692 2672
2693 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
2694 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
@@ -2760,9 +2740,9 @@
2760 2740 return $result;
2761 2741 }
2762 2742
2763 2743
2764 - // IOK 2024-01-09 If using Vipps Checkout with the BankTransfer method, which is eg. used in Finland,
2744 + // IOK 2024-01-09 If using Checkout with the BankTransfer method, which is eg. used in Finland,
2765 2745 // we are (currently) not receiving any 'state' or 'aggregate', so add this iff the payment is successful.
2766 2746 // The reason for this is that this payment type does not actually use the epayment API at all (!)
2767 2747 // Also moved some other compatibility code here -
2768 2748 // --- reference used to be orderId
@@ -2831,9 +2811,9 @@
2831 2811
2832 2812 return $result;
2833 2813 }
2834 2814
2835 - // Vipps Checkout v3 does *not* provide userDetails. Vipps Checkout v2 and epayment *does*. But Checkout additionally allows
2815 + // Checkout v3 does *not* provide userDetails. Checkout v2 and epayment *does*. But Checkout additionally allows
2836 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.
2837 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
2838 2818 // if not, we use shippingDetails. IOK 2023-01-10
2839 2819 // Also, epayment uses mobileNumber and checkout uses phoneNumber, so normalize.
@@ -2843,8 +2823,9 @@
2843 2823 // If we have userDetails, use it (ecom API with user data requested - Express Checkout
2844 2824 if (isset($vippsdata['userDetails'])) {
2845 2825 $userDetails = $vippsdata['userDetails'];
2846 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
2847 2828 $sub = "";
2848 2829 if (isset($vippsdata['profile']) && isset($vippsdata['profile']['sub'])) {
2849 2830 $sub = $vippsdata['profile']['sub'];
2850 2831 }
@@ -3209,9 +3190,14 @@
3209 3190 $is_base64 = $shipping_table ? ( $shipping_table['_is_base64'] ?? false) : false;
3210 3191
3211 3192 if (is_array($shipping_table) && isset($shipping_table[$key])) {
3212 3193 $decoded = $is_base64 ? @base64_decode($shipping_table[$key]) : $shipping_table[$key];
3213 - $shipping_rate = $decoded ? @unserialize($decoded) : null;
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 + $allowed_classes = apply_filters('woo_vipps_express_checkout_allowed_shipping_classes', [WC_Shipping_Rate::class, \stdClass::class]);
3197 + $shipping_rate = $decoded ? @unserialize($decoded, ['allowed_classes' => $allowed_classes]) : null;
3198 + $shipping_rate = is_a($shipping_rate,'WC_Shipping_Rate') ? $shipping_rate : null;
3199 +
3214 3200 if (!$shipping_rate) {
3215 3201 $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');
3216 3202 $this->log(sprintf(__("Serialized data was %1\$s", 'woo-vipps'), $decoded), 'error');
3217 3203 } else {
@@ -3231,9 +3217,9 @@
3231 3217 }
3232 3218 }
3233 3219 }
3234 3220
3235 - // Possible extra metadata from Vipps Checkout IOK 2023-01-17
3221 + // Possible extra metadata from Checkout IOK 2023-01-17
3236 3222 // Store in the order, but also in the shipping rate so it will be visible in the order screen
3237 3223 // along with the shipping ragte
3238 3224 if (isset($shipping['pickupPoint'])) {
3239 3225 $order->update_meta_data('vipps_checkout_pickupPoint', $shipping['pickupPoint']);
@@ -3293,9 +3279,9 @@
3293 3279 $methodclass = $methods_classes[$shipping_rate->get_method_id()] ?? null;
3294 3280 $shipping_method = $methodclass ? new $methodclass($shipping_rate->get_instance_id()) : null;
3295 3281 $is_vipps_checkout_shipping = $shipping_method && is_a($shipping_method, 'VippsCheckout_Shipping_Method');
3296 3282
3297 - // Some Vipps Checkout-specific shipping methods calculate the cost in the Vipps window.
3283 + // Some Checkout-specific shipping methods calculate the cost in the Vipps window.
3298 3284 if ($is_vipps_checkout_shipping && $shipping_method->dynamic_cost) {
3299 3285 $vippsamount = intval($order->get_meta('_vipps_amount'));
3300 3286 $shipping_tax_rate = floatval($order->get_meta('_vipps_shipping_tax_rates'));
3301 3287 $compareamount = $ordertotal * 100;
@@ -3331,9 +3317,9 @@
3331 3317
3332 3318 $order->set_total($ordertotal + $total_shipping + $total_shipping_tax);
3333 3319 $order->update_taxes(); // Necessary for the admin view only; does not recalculate order.
3334 3320
3335 - // Add an early hook for Vipps Checkout orders with special shipping methods
3321 + // Add an early hook for Checkout orders with special shipping methods
3336 3322 $metadata = $shipping_rate->get_meta_data();
3337 3323 if (isset($metadata['type'])) {
3338 3324 do_action('woo_vipps_checkout_special_shipping_method', $order, $shipping_rate, $metadata['type']);
3339 3325 }
@@ -3349,9 +3335,9 @@
3349 3335
3350 3336
3351 3337 // 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.
3352 3338 // If possible and safe, user will be logged in before being sent to the thankyou screen. IOK 2020-10-09
3353 - // Same thing for Vipps Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3339 + // Same thing for Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3354 3340 $customer = false;
3355 3341 if ($assigncustomer) {
3356 3342 $customer = Vipps::instance()->express_checkout_get_vipps_customer($order);
3357 3343 }
@@ -3359,8 +3345,9 @@
3359 3345 // 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
3360 3346 // the same as the 'sub' we get in Login so that must be a future feature. IOK 2020-10-09
3361 3347 // 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
3362 3348 // after calling get_userinfo, so we'll leave that be.
3349 + // 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
3363 3350 if (class_exists('VippsWooLogin') && $customer && !is_wp_error($customer) && !get_user_meta($customer->get_id(), '_vipps_phone',true)) {
3364 3351 update_user_meta($customer->get_id(), '_vipps_phone', $billing['phoneNumber']);
3365 3352 if (isset($user['sub'])) {
3366 3353 $userid = $customer->get_id();
@@ -3420,9 +3407,9 @@
3420 3407 $order->update_meta_data('_vipps_api', 'banktransfer');
3421 3408 }
3422 3409 }
3423 3410
3424 - // Handle the callback from Vipps eCom.
3411 + // Handle the callback from Vipps ePayment
3425 3412 public function handle_callback($result, $order, $ischeckout=false, $iswebhook=false) {
3426 3413 global $Vipps;
3427 3414
3428 3415 $vippsorderid = $result['orderId'];
@@ -3430,55 +3417,121 @@
3430 3417
3431 3418 $keyset = $this->get_keyset();
3432 3419 $me = array_keys($keyset);
3433 3420
3421 + // Validate the callback first
3434 3422 if (!in_array($merchant, $me)) {
3435 3423 $this->log(sprintf(__("%1\$s callback with wrong merchantSerialNumber - might be forged",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3436 3424 return false;
3437 3425 }
3438 -
3439 3426 if (!$order) {
3440 3427 $this->log(sprintf(__("%1\$s callback for unknown order",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3441 3428 return false;
3442 3429 }
3443 - $orderid = $order->get_id();
3444 - // We may need to use poll to get data, depending on the content passed.
3445 - $express = $order->get_meta('_vipps_express_checkout');
3446 - $checkout_session = $order->get_meta('_vipps_checkout_session');
3447 -
3430 + $order_id = $order->get_id();
3448 3431 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3449 - $this->log(sprintf(__("Wrong %1\$s Orderid - possibly an attempt to fake a callback ", 'woo-vipps'), Vipps::CompanyName()), 'warning');
3450 - clean_post_cache($order->get_id());
3432 + $this->log(sprintf(__('Wrong %1$s Orderid - possibly an attempt to fake a callback ', 'woo-vipps'), Vipps::CompanyName()), 'warning');
3433 + clean_post_cache($order_id);
3451 3434 exit();
3452 3435 }
3453 3436
3437 + // Note any errors in the callback early
3454 3438 $errorInfo = $result['errorInfo'] ?? '';
3455 3439 if ($errorInfo) {
3456 - $this->log(sprintf(__("Message in callback from %1\$s for order",'woo-vipps'), $this->get_payment_method_name()) . ' ' . $orderid . ' ' . $errorInfo['errorMessage'],'error');
3457 - $order->add_order_note(sprintf(__("Message from %1\$s: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3440 + /* translators: payment method name, order id */
3441 + $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');
3442 + /* translators: payment method name, message */
3443 + $order->add_order_note(sprintf(__('Message from %1$s: %2$s','woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3458 3444 }
3459 3445
3446 + // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3447 + try {
3448 + $Vipps->createCallbackSignal($order,'ok');
3449 + } catch (Exception $e) {
3450 + // Could not create a signal file, but that's ok.
3451 + }
3452 +
3453 + // New callback handling: schedule an Action Scheduler job to process it. The purpose of processing callback
3454 + // is to set finalize the order (set order status, set shipping for express) in the case when customer does
3455 + // not return to the store, because then poll does not run. LP 2026-03-27
3456 +
3457 + // Below is separate from '_vipps_callback_timestamp' which is when callback is sent,
3458 + // but also that meta won't be stored until callback is actually processed. LP 2026-03-30
3459 + $order->update_meta_data('_vipps_callback_received_at', time());
3460 + // Store the callback data in the order. We'll do a cleanup of this when the scheduled job runs. IOK 2026-04-21
3461 + $order->update_meta_data('_vipps_callback_data', $result);
3462 + $order->save_meta_data();
3463 +
3464 + // Run callback actions for callback received as soon as it is actually received.
3465 + $transaction = []; // No longer provided. IOK 2026-05-06
3466 + do_action('woo_vipps_callback_received', $order, $result, $transaction);
3467 +
3468 + $action_args = [
3469 + 'order_id' => $order->get_id(),
3470 + 'is_checkout' => $ischeckout,
3471 + 'is_webhook' => $iswebhook,
3472 + ];
3473 + // 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
3474 + // and have the order finalized the 'normal' way, but if they don't, we'll handle it async. 2026-04-21
3475 + $scheduled_at = time() + 60;
3476 + $action_id = as_schedule_single_action($scheduled_at, 'woo_vipps_action_process_callback', $action_args, 'woo-vipps', false);
3477 + if ($action_id) {
3478 + /* translators: order id, scheduled time */
3479 + $this->log(sprintf(__('Callback action scheduled at %2$s for order %1$s', 'woo-vipps'), $order->get_id(), $scheduled_at), 'info');
3480 + } else {
3481 + // 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
3482 + /* translators: order id */
3483 + $this->log(sprintf(__('Failed to schedule callback process action for order %1$s, check the php error log', 'woo-vipps'), $order->get_id()), 'error');
3484 + /* We will not delete the callback data here, to facilitate debugging. But return false to indicate that callback handling will fail. */
3485 + /* NB: The order will still be processed with the periodic job, at a later stage. IOK 2026-05-06 */
3486 + return false;
3487 + }
3488 +
3489 + // Signal that we in fact handled the order.
3490 + return true;
3491 + }
3492 +
3493 + /** Runs in action scheduler: sync woo status from Vipps callback data. Handle shipping etc. for Express. LP 2026-03-31 */
3494 + public function action_process_callback($order_id, $is_checkout, $is_webhook) {
3495 + $order = wc_get_order($order_id);
3496 + if (!is_a($order, 'WC_Order')) {
3497 + /* translators: order id */
3498 + $this->log(sprintf(__('Callback process action failed, could not find order %1$s','woo-vipps'), $order_id), 'error');
3499 + return false;
3500 + }
3501 +
3502 + $oldstatus = $order->get_status();
3503 + if ($oldstatus != 'pending') {
3504 + // Actually, we are ok with this order, abort the callback handler. IOK 2018-05-30
3505 + $order->delete_meta_data('_vipps_callback_data');
3506 + clean_post_cache($order->get_id());
3507 + return false;
3508 + }
3509 + $data = $order->get_meta('_vipps_callback_data');
3510 +
3511 + /* translators: order id */
3512 + $this->log(sprintf(__('Callback process action running for order %1$s.', 'woo-vipps'), $order_id));
3513 +
3460 3514 // The payment details field is passed in Checkout, not in Express, but none of them are complete, so we fill out the values
3461 3515 // depending on which one we are IOK 2025-08-13
3462 3516 $details = [];
3463 3517 // Checkout has this as a field, containing *some* of the neccessary data
3464 - if (isset($result['paymentDetails'])) {
3518 + if (isset($data['paymentDetails'])) {
3465 3519 // Checkout. The sesssion states are # "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
3466 3520 // -- we should only get callbacks for successful sessions actually.
3467 - $details = $result['paymentDetails'];
3468 - $result['state'] = $result['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($result['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3469 - $details['state'] = $result['state'];
3470 - $details['paymentMethod'] = $result['paymentMethod'];
3521 +
3522 + $details = $data['paymentDetails'];
3523 + $data['state'] = $data['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($data['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3524 + $details['state'] = $data['state'];
3525 + $details['paymentMethod'] = $data['paymentMethod'];
3471 3526 } else {
3472 - // This should be an ecom callback; which we need to add a lot of data for to get a valid "paymentDetails".
3527 + // This should be an epayment webhook callback; which we need to add a lot of data for to get a valid "paymentDetails".
3473 3528 $details = [];
3474 - $result['state'] = $result['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3475 - $details['state'] = $result['name'];
3476 - $details['amount'] = $result['amount']; // currency, value
3529 + $data['state'] = $data['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3530 + $details['state'] = $data['name'];
3531 + $details['amount'] = $data['amount']; // currency, value
3477 3532 $details['paymentMethod'] = 'epayment';
3478 - $currency = $details['amount']['currency'];
3479 - $nothing = [ 'currency' => $currency, 'value' => 0];
3480 - }
3533 + }
3481 3534
3482 3535 // For both callbacks, set 'aggregate'
3483 3536 $currency = $details['amount']['currency'];
3484 3537 $nothing = [ 'currency' => $currency, 'value' => 0];
@@ -3483,68 +3536,52 @@
3483 3536 $currency = $details['amount']['currency'];
3484 3537 $nothing = [ 'currency' => $currency, 'value' => 0];
3485 3538 $aggregate = ['authorizedAmount' => $nothing, 'cancelledAmount' => $nothing, 'capturedAmount' => $nothing, 'refundedAmount' => $nothing];
3486 3539 if ($details['state'] == 'AUTHORIZED') {
3487 - $aggregate['authorizedAmount'] = $details['amount'];
3540 + $aggregate['authorizedAmount'] = $details['amount'];
3488 3541 }
3489 3542 $details['aggregate'] = $aggregate;
3490 - $result['paymentDetails'] = $details;
3543 + $data['paymentDetails'] = $details;
3491 3544
3492 - $result = $this->normalizePaymentDetails($result);
3493 - $details = $result['paymentDetails'];
3545 + // Do the actual work with a function shared with the periodic job.
3546 + $this->set_order_status_by_payment_details($order,$data);
3494 3547
3495 - $vippsstatus = $result['status']; // Will exist now, because of the normalization IOK 2025-08-13
3548 + /* translators: payment method name */
3549 + $order->add_order_note(sprintf(__('%1$s callback processed','woo-vipps'), $this->get_payment_method_name()));
3550 +
3551 + // We're done, so delete the callback data. IOK 2026-04-22
3552 + $order->delete_meta_data('_vipps_callback_data');
3553 + }
3554 +
3555 +
3556 + // Called either by periodic job or by action_process_callback with the callback data *or* with payment details fetched with poll.
3557 + // sets order status if neccessary, and will finalize the order for Express via HTTP call if necessary. IOK 2026-05-06
3558 + public function set_order_status_by_payment_details($order, $data, $allow_retry=true) {
3559 + $data = $this->normalizePaymentDetails($data);
3560 + $details = $data['paymentDetails'];
3561 + $order_id = $order->get_id();
3562 +
3563 + $vippsstatus = $data['status']; // Will exist now, because of the normalization IOK 2025-08-13
3496 3564 $newstatus = $this->interpret_vipps_order_status($vippsstatus);
3497 3565
3498 3566 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
3499 3567 $transaction = array();
3500 - $stamp = ($result['timestamp'] ?? false) ? strtotime($result['timestamp']) : time();
3568 + $stamp = ($data['timestamp'] ?? false) ? strtotime($data['timestamp']) : time();
3501 3569 $transaction['timeStamp'] = date('Y-m-d H:i:s', $stamp);
3502 3570 $transaction['amount'] = $details['amount']['value'];
3503 3571 $transaction['currency'] = $details['amount']['currency'];
3504 - $transaction['status'] = ($result['state'] ?? $details['state']);
3572 + $transaction['status'] = ($data['state'] ?? $details['state']);
3505 3573 $transaction['paymentmethod'] = $details['paymentMethod'] ?? "";
3574 + $this->order_set_transaction_metadata($order, $transaction);
3506 3575
3507 - if (!$transaction) {
3508 - $this->log(sprintf(__("Anomalous callback from %1\$s, handle errors and clean up",'woo-vipps'), $this->get_payment_method_name()),'warning');
3509 - clean_post_cache($order->get_id());
3510 - return false;
3576 + // Dont do anything if order is not finalized. LP 2026-08-31
3577 + if (!in_array($newstatus, ['authorized', 'complete', 'cancelled'])) {
3578 + return;
3511 3579 }
3512 3580
3513 - $order->add_order_note(sprintf(__('%1$s callback received','woo-vipps'), $this->get_payment_method_name()));
3514 - do_action('woo_vipps_callback_received', $order, $result, $transaction);
3581 + // This order is ready to set order shipping details etc for IOK 2025-09-19
3582 + $ready = in_array($newstatus, ['authorized', 'complete']);
3515 3583
3516 - $oldstatus = $order->get_status();
3517 - if ($oldstatus != 'pending') {
3518 - // Actually, we are ok with this order, abort the callback. IOK 2018-05-30
3519 - clean_post_cache($order->get_id());
3520 - return false;
3521 - }
3522 -
3523 - // If the callback is late, and we have called get order status, and this is in progress, we'll log it and just drop the callback.
3524 - // We do this because neither Woo nor WP has locking, and it isn't feasible to implement one portably. So this reduces somewhat the likelihood of race conditions
3525 - // when callbacks happen while we are polling for results. IOK 2018-05-30
3526 - if (!$Vipps->lockOrder($order)) {
3527 - clean_post_cache($order->get_id());
3528 - return false;
3529 - }
3530 -
3531 - // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3532 - // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3533 - // made to the session here will be saved. Sorry.
3534 - $Vipps->callback_restore_session($orderid);
3535 -
3536 - // Set Vipps metadata as early as possible
3537 - $this->order_set_transaction_metadata($order, $transaction);
3538 -
3539 - $this->log(sprintf(__("%1\$s callback: Handling order: ", 'woo-vipps'), Vipps::CompanyName()) . " " . $orderid, 'debug');
3540 -
3541 -
3542 - // This order is ready to set order shipping details etc for IOK 2025-09-19
3543 - $ready = false;
3544 - if (in_array($newstatus, ['authorized', 'complete'])) {
3545 - $ready = true;
3546 - }
3547 3584 if ($ready) {
3548 3585 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
3549 3586 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3550 3587 $this->reset_erroneous_payment_method($order);
@@ -3549,40 +3586,38 @@
3549 3586 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3550 3587 $this->reset_erroneous_payment_method($order);
3551 3588 }
3552 3589
3553 - if ($ready && ($express || $ischeckout)) {
3554 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3555 - // This will also normalize userDetails, adding 'sub' where required and fields for backwards compatibility. 2025-08-12
3556 - $result = $this->ensure_userDetails($result, $order);
3590 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3557 3591
3558 - // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3559 - // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3560 - // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3561 - // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3562 - // we have added a scope of any kind. This is therefore probably dead code.
3563 - // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3564 - if (!isset($result['userDetails'])) {
3565 - // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3566 - try {
3567 - $details = $this->get_payment_details($order);
3568 - $result = $details;
3569 - } catch (Exception $e) {
3570 - $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3571 - $this->log($e->getMessage());
3572 - }
3573 - }
3574 -
3575 - // 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
3576 - $result = $this->normalizeShippingDetails($result, $order);
3577 -
3578 - // We should now always have shipping details.
3579 - if (isset($result['shippingDetails'])) {
3580 - $billing = isset($result['billingDetails']) ? $result['billingDetails'] : false;
3581 - $this->set_order_shipping_details($order,$result['shippingDetails'], $result['userDetails'], $billing, $result);
3592 + // Handle session and shipping through http, because we dont want to mess with session here in wp cron (action scheduler). LP 2026-03-30
3593 + // NB: This is and must be a *synchronous call*. When done, the order will have shipping, addresses etc. IOK 2026-05-06.
3594 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3595 + if ($ready && $is_express_or_checkout && !$shipping_set) {
3596 + $token = $order->get_meta('_vipps_authtoken');
3597 + $args = [
3598 + 'body' => [
3599 + 'order_id' => $order_id,
3600 + 'vipps_order_data' => $data,
3601 + ],
3602 + 'headers' => [
3603 + 'X-WooVipps-Token' => $token,
3604 + ],
3605 + ];
3606 + $url = Vipps::get_rest_url('v1', '/order-set-shipping');
3607 + $response = wp_remote_post($url, $args);
3608 + if (is_wp_error($response)) {
3609 + /* translators: order id, error message */
3610 + $error_msg = $response->get_error_message();
3611 + $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');
3612 + } else if (200 != ($response['response']['code'] ?? -1)) {
3613 + /* translators: order id */
3614 + $response_msg = print_r($response['body'] ?? ['Missing response body'], true);
3615 + $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');
3582 3616 }
3583 3617 }
3584 3618
3619 + // This must happen *after* finalization for Express, as above. IOK 2026-05-06
3585 3620 // the only status we now care about is AUTHORIZED. Previously we had AUTHORISED and RESERVED and RESERVE as well. And SALE.
3586 3621 if ($vippsstatus == 'AUTHORIZED') {
3587 3622 $this->payment_complete($order);
3588 3623 } else if ($vippsstatus == 'SALE') {
@@ -3587,14 +3622,15 @@
3587 3622 $this->payment_complete($order);
3588 3623 } else if ($vippsstatus == 'SALE') {
3589 3624 // Direct capture needs special handling because most of the meta values we use are missing IOK 2019-02-26
3590 3625 // Actually not supported anymore, but keep logic. IOK 2025-08-13
3626 + // Still supported for finnish direct bank transfer. IOK 2026-04-22
3591 3627 $order->add_order_note(sprintf(__('Payment captured directly at %1$s', 'woo-vipps'), $this->get_payment_method_name()));
3592 3628 $order->payment_complete();
3593 3629 $this->update_vipps_payment_details($order);
3594 3630 } else {
3595 3631 // Not ok status; set to failed/cancelled
3596 - $order_is_retryable = Vipps::order_is_vipps_retryable($order->get_id());
3632 + $order_is_retryable = $allow_retry && Vipps::order_is_vipps_retryable($order->get_id());
3597 3633 $status_on_fail = $this->get_option('status_on_fail');
3598 3634 $cancel_on_fail = apply_filters('woo_vipps_cancel_failed_orders', false, $order, $vippsstatus);
3599 3635 if ($cancel_on_fail || !$order_is_retryable) {
3600 3636 $status_on_fail = 'cancelled';
@@ -3599,33 +3635,72 @@
3599 3635 if ($cancel_on_fail || !$order_is_retryable) {
3600 3636 $status_on_fail = 'cancelled';
3601 3637 }
3602 3638 if (!in_array($status_on_fail, ['cancelled', 'failed'])) {
3603 - /* translators: order status name. Cancelled is woocommerce status name */
3639 + /* translators: %1 = order status parameter. 'cancelled' is woocommerce order status name */
3604 3640 $this->log(__('Unsupported status for payment failure of \'%1$s\', falling back to cancelled.', 'woo-vipps'), 'warning');
3605 3641 $status_on_fail = 'cancelled';
3606 3642 }
3607 3643
3608 3644 /* translators: company name */
3609 - $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s', 'woo-vipps'), Vipps::CompanyName()));
3645 + $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
3610 3646 }
3611 3647
3612 3648 $order->save();
3613 - clean_post_cache($order->get_id());
3649 + clean_post_cache($order_id);
3650 + }
3614 3651
3615 - // Restore the session again so that we aren't causing issues with the customer-return branch, which may have to update the session concurrently. IOK 2023-018
3616 - $Vipps->callback_restore_session($orderid);
3617 - $Vipps->unlockOrder($order);
3652 + /* finalize shipping for express/checkout order. LP 2026-03-30 */
3653 + public function rest_order_set_shipping($request) {
3654 + $order_id = $request->get_param('order_id');
3655 + $data = $request->get_param('vipps_order_data');
3618 3656
3619 - // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3620 - try {
3621 - $Vipps->createCallbackSignal($order,'ok');
3622 - } catch (Exception $e) {
3623 - // Could not create a signal file, but that's ok.
3657 + $order = wc_get_order($order_id);
3658 + if (!is_a($order, 'WC_Order')) {
3659 + return new WP_Error('order_not_found', __('Order not found', 'woo-vipps'), ['status' => 404]);
3624 3660 }
3625 3661
3626 - // Signal that we in fact handled the order.
3627 - return true;
3662 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3663 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3664 + if (!$is_express_or_checkout || $shipping_set) {
3665 + return new WP_Error('order_is_finalized', __('Order does not need to set shipping', 'woo-vipps'), ['status' => 409]);
3666 + }
3667 +
3668 + // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3669 + // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3670 + // made to the session here will be saved. Sorry.
3671 + // 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
3672 + Vipps::instance()->callback_restore_session($order_id);
3673 +
3674 + // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3675 + // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
3676 + $data = $this->ensure_userDetails($data, $order);
3677 +
3678 + // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3679 + // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3680 + // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3681 + // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3682 + // we have added a scope of any kind. This is therefore probably dead code.
3683 + // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3684 + if (!isset($data['userDetails'])) {
3685 + // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3686 + try {
3687 + $details = $this->get_payment_details($order);
3688 + $data = $details;
3689 + } catch (Exception $e) {
3690 + $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3691 + $this->log($e->getMessage());
3692 + }
3693 + }
3694 +
3695 + // 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
3696 + $data = $this->normalizeShippingDetails($data, $order);
3697 +
3698 + // We should now always have shipping details.
3699 + if (isset($data['shippingDetails'])) {
3700 + $billing = isset($data['billingDetails']) ? $data['billingDetails'] : false;
3701 + $this->set_order_shipping_details($order,$data['shippingDetails'], $data['userDetails'], $billing, $data);
3702 + }
3628 3703 }
3629 3704
3630 3705 // Do the 'payment_complete' logic for non-SALE orders IOK 2020-09-22
3631 3706 public function payment_complete($order,$transactionid='') {
@@ -3688,9 +3763,9 @@
3688 3763 }
3689 3764 do_action('woo_vipps_payment_complete_at_shutdown', $order, $this);
3690 3765 } catch (Exception $e) {
3691 3766 // This is/should be non-critical so just log it.
3692 - $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 + $this->log(sprintf(__("Could not do all payment-complete actions on %1\$s order %2\$d: %3\$s ", 'woo-vipps'), Vipps::CompanyName(), $orderid, $e->getMessage()), "error");
3693 3768 }
3694 3769 }
3695 3770
3696 3771 // This is run on payment complete. Per default will it only add a link to the order confirmation page, but
@@ -3797,9 +3872,9 @@
3797 3872
3798 3873 $contents = WC()->cart->get_cart_contents();
3799 3874 $contents = apply_filters('woo_vipps_create_express_checkout_cart_contents',$contents);
3800 3875 try {
3801 - $cart_hash = md5(json_encode(wc_clean($contents)) . WC()->cart->total);
3876 + $cart_hash = WC()->cart->get_cart_hash();
3802 3877 $order = new WC_Order();
3803 3878 $order->set_status('pending');
3804 3879 $order->set_payment_method($this);
3805 3880 if ($ischeckout) {
@@ -3810,8 +3885,9 @@
3810 3885 }
3811 3886 // We use 'checkout' as the created_via key as per requests, but allow merchants to use their own. IOK 2022-09-15
3812 3887 $created_via = apply_filters('woo_vipps_express_checkout_created_via', 'checkout', $order, $ischeckout);
3813 3888 $order->set_created_via($created_via);
3889 + $order->set_cart_hash($cart_hash);
3814 3890
3815 3891 $dummy = sprintf(__('Vipps Express Checkout', 'woo-vipps')); // this is so gettext will find this string.
3816 3892 $dummy = sprintf(__('Vipps Checkout', 'woo-vipps')); // this is so gettext will find this string.
3817 3893
@@ -3973,9 +4049,9 @@
3973 4049 </p>
3974 4050 </div>
3975 4051 <?php endif; ?>
3976 4052
3977 - <?php // We will only show the Vipps Checkout options if the user has activated the feature (thus creating the pages involved etc). IOK 2021-10-01
4053 + <?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
3978 4054 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
3979 4055 ?>
3980 4056
3981 4057 <?php /* We will *not* allow vipps checkout to be activated at this point, since the product is no longer sold. IOK 2026-04-30 */ ?>
@@ -4036,8 +4112,23 @@
4036 4112 // If enabling this, ensure the page in question exists
4037 4113 if ($this->get_option('vipps_checkout_enabled') == 'yes') {
4038 4114 update_option('woo_vipps_checkout_activated', true, true); // This must be true here, but still, make sure
4039 4115 Vipps::instance()->maybe_create_vipps_pages();
4116 + }
4117 +
4118 + // Ensure special page has the necessary shortcode. LP 2026-09-01
4119 + $special_page = get_post(Vipps::get_special_page_id());
4120 + if ($special_page && !has_shortcode($special_page->post_content, 'vipps_special_page')) {
4121 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4122 + wp_update_post([
4123 + 'ID' => Vipps::get_special_page_id(),
4124 + 'post_content' => $new_content,
4125 + ]);
4126 + } else if (!Vipps::get_special_page_id()) {
4127 + // We shouldn't really get here, the page should be ensured to exist in init. LP 2026-09-03
4128 + /* translators: %s is current method name */
4129 + $this->log(sprintf(__('Missing special page in %s, attempting to fix', 'woo-vipps'), 'process_admin_options'), 'warning');
4130 + Vipps::instance()->ensure_special_page_exists();
4040 4131 }
4041 4132
4042 4133 return $saved;
4043 4134 }