PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.1
Pay with Vipps and MobilePay for WooCommerce v6.3.1
6.3.1 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 All 190 releases
← All changes | payment/WC_Gateway_Vipps.class.php +360 -283 6.0.3 → 6.3.1 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) {
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
@@ -267,8 +329,11 @@
267 329 $remaining = intval($order->get_meta('_vipps_capture_remaining'));
268 330
269 331 if ($remaining > 0) {
270 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;
271 336 }
272 337
273 338 $currency = $order->get_currency();
274 339 try {
@@ -536,9 +601,9 @@
536 601 // Webhook callbacks do not pass GET arguments at all, but do provide an X-Vipps-Authorization header for verification. IOK 2023-12-19
537 602 public function webhook_callback_url () {
538 603 $url = home_url("/", 'https');
539 604 $queryargs = ['callback'=>'webhook'];
540 - $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
541 606 // HTTPS required. IOK 2018-05-18
542 607 // If the user for some reason hasn't enabled pretty links, fall back to ancient version. IOK 2018-04-24
543 608 if ( !get_option('permalink_structure')) {
544 609 $queryargs['wc-api'] = $forwhat;
@@ -556,22 +621,8 @@
556 621 }
557 622 public function shipping_details_callback_url($token='',$reference=0) {
558 623 return $this->make_callback_urls('vipps_shipping_details',$token,$reference);
559 624 }
560 - // Callback for the consetn removal callback. Must use template redirect directly, because wc-api doesn't handle DELETE.
561 - // IOK 2018-05-18
562 - public function consent_removal_callback_url () {
563 - $queryargs = [];
564 - $url = home_url("/", 'https');
565 - if ( !get_option('permalink_structure')) {
566 - $queryargs['vipps-consent-removal']=1;
567 - } else {
568 - $url = trailingslashit(home_url('vipps-consent-removal', 'https'));
569 - }
570 - // And we need to add an empty "callback" query arg as the very last arg to receive the actual callback.
571 - // We can't use add_query_arg for that, as an empty argument will remove the equals-sign.
572 - return add_query_arg($queryargs, $url) . "&callback=";
573 - }
574 625
575 626 // Allow user to select the template to be used for the special Vipps MobilePay pages. IOK 2020-02-17
576 627 public function get_theme_page_templates() {
577 628 if (!$this->page_templates) {
@@ -583,22 +634,8 @@
583 634 }
584 635 return $this->page_templates;
585 636 }
586 637
587 - // We can't use get_pages to get a default list of pages for our settings, because it triggers
588 - // actions that can be used by other plugins. Therefore we must use the database directly and cache the results. IOK 2023-08-22
589 - public function get_pagelist () {
590 - if (!$this->page_list) {
591 - global $wpdb;
592 - $page_list = array(''=>__('Use a simulated page (default)', 'woo-vipps'));
593 - foreach($wpdb->get_results("SELECT ID,post_title FROM {$wpdb->prefix}posts WHERE post_type='page' and post_status='publish'") as $page) {
594 - $page_list[$page->ID] = $page->post_title;
595 - }
596 - $this->page_list = $page_list;
597 - }
598 - return $this->page_list;
599 - }
600 -
601 638 // Check to see if the product in question can be bought with express checkout IOK 2018-12-04
602 639 public function product_supports_express_checkout($product) {
603 640 // IOK 2023-12-12 Can only support express checkout for Vipps - not MobilePay (yet!)
604 641 // IOK 2025-09-01 Now supports mobilepay
@@ -658,13 +695,18 @@
658 695
659 696 // True if "Express checkout" should be displayed IOK 2018-06-18
660 697 public function show_express_checkout() {
661 698 if (!$this->express_checkout_available()) return false;
662 - $show = ($this->enabled == 'yes') && ($this->get_option('cartexpress') == 'yes') ;
663 - $show = $show && $this->cart_supports_express_checkout();
699 + $show = 'yes' == $this->enabled && $this->cart_supports_express_checkout();
664 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 + }
665 706 // Earlier, we disabled this if Checkout was active; but we will now respect the setting in all
666 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
667 709
668 710 return apply_filters('woo_vipps_show_express_checkout', $show);
669 711 }
670 712
@@ -745,9 +787,9 @@
745 787 $order->save();
746 788 }
747 789
748 790 // IOK 2024-09-01 In general, we can refund most Vipps Mobilepay orders through the api,
749 - // 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.
750 792 public function can_refund_order( $order ) {
751 793 $method = $order->get_meta('_vipps_api');
752 794 switch ($method) {
753 795 case 'banktransfer':
@@ -755,9 +797,9 @@
755 797 break;
756 798 case 'epayment':
757 799 return true;
758 800 break;
759 - // Default is old-style ecom v2.
801 + // Default is true; but the above are exhaustive IOK 2026-08-18
760 802 default:
761 803 return true;
762 804 break;
763 805 }
@@ -798,9 +840,8 @@
798 840 wc_switch_to_site_locale();
799 841 $the_refund = wc_create_refund($data);
800 842 wc_restore_locale();
801 843 if (is_wp_error($the_refund)) {
802 - $refund_thru_gateway = false;
803 844 $msg = $the_refund->get_error_message();
804 845 $order->add_order_note(sprintf(__("Error when refunding payment through %1\$s:", 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $msg);
805 846 $order->save();
806 847 $this->adminerr($msg);
@@ -1004,9 +1045,8 @@
1004 1045 global $Vipps;
1005 1046
1006 1047 // Used for defaults in the admin interface; however this functions is called a loot more often than that.
1007 1048 $page_templates = $this->get_theme_page_templates();
1008 - $page_list = $this->get_pagelist();
1009 1049
1010 1050 $orderprefix = $Vipps->generate_order_prefix();
1011 1051
1012 1052 // Default handling based on other parameters and earlier values.
@@ -1017,16 +1057,15 @@
1017 1057 if (class_exists('VippsWooLogin')) {
1018 1058 $woodefault = 'yes' === get_option('woocommerce_enable_signup_and_login_from_checkout');
1019 1059 if ($woodefault) {
1020 1060 $expresscreateuserdefault = "yes";
1021 - // $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.
1022 1062 }
1023 1063 }
1024 1064
1025 - // 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
1026 1066 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1027 1067
1028 -
1029 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.
1030 1069 $current = get_option('woocommerce_vipps_settings');
1031 1070 // New defaults based on old defaults
1032 1071 $default_static_shipping_for_checkout = 'no';
@@ -1031,8 +1070,9 @@
1031 1070 // New defaults based on old defaults
1032 1071 $default_static_shipping_for_checkout = 'no';
1033 1072 $default_ask_address_for_express = 'no';
1034 1073 $default_status_on_fail = 'failed';
1074 + $default_express_show_in_checkout = 'yes';
1035 1075 if ($current) {
1036 1076 $default_static_shipping_for_checkout = (isset($current['enablestaticshipping'])) ? $current['enablestaticshipping'] : 'no';
1037 1077 $default_ask_address_for_express = (isset($current['useExplicitCheckoutFlow']) && $current['useExplicitCheckoutFlow'] == "yes") ? "yes" : "no";
1038 1078 // The old default used the same value as for Express Checkout. IOK 2023-07-27
@@ -1040,8 +1080,15 @@
1040 1080
1041 1081 // For existing installs: set failed payments order status to cancelled to keep same default behaviour.
1042 1082 // New installs will be set to failed instead of cancelled. LP 2026-03-26
1043 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 + }
1044 1091 }
1045 1092
1046 1093 // Get the already-set country code. For existing sites, this will guess the country based on the currency; for new sites, use
1047 1094 // the woo base country. IOK 2024-10-17 (previously used the currency here too).
@@ -1181,9 +1228,9 @@
1181 1228 'description' => __('Your phone number where Porterbuddy may send you important messages. Format must be MSISDN (including country code). Example: "4791234567"','woo-vipps'),
1182 1229 'default' => '',
1183 1230 ),
1184 1231
1185 - // 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
1186 1233 'vcs_helthjem' => array(
1187 1234 'title' => __('Helthjem', 'woo-vipps'),
1188 1235 'label' => sprintf(__('Support Helthjem as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1189 1236 'type' => 'checkbox',
@@ -1218,9 +1265,9 @@
1218 1265 ),
1219 1266
1220 1267 );
1221 1268
1222 - /* 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 */
1223 1270 $externals = [];
1224 1271 $external_payment_fields = [];
1225 1272 $allow_external_payments = $this->allow_external_payments_in_checkout();
1226 1273 if ($allow_external_payments) {
@@ -1413,14 +1460,14 @@
1413 1460 'default' => 'none',
1414 1461 ),
1415 1462 );
1416 1463
1417 - $expressfields = array(
1464 + $expressfields = array(
1418 1465 'express_options' => array(
1419 1466 'title' => sprintf(__('Express Checkout', 'woo-vipps')),
1420 1467 'type' => 'title',
1421 1468 'class' => 'tab',
1422 - '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())
1423 1470 ),
1424 1471
1425 1472 'cartexpress' => array(
1426 1473 'title' => __('Enable Express Checkout in cart', 'woo-vipps'),
@@ -1430,8 +1477,17 @@
1430 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()),
1431 1478 'default' => 'yes',
1432 1479 ),
1433 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 +
1434 1490 'singleproductexpress' => array(
1435 1491 'title' => __('Enable Express Checkout for single products', 'woo-vipps'),
1436 1492 'label' => __('Enable Express Checkout for single products', 'woo-vipps'),
1437 1493 'type' => 'select',
@@ -1534,22 +1590,24 @@
1534 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'),
1535 1591 ),
1536 1592
1537 1593 'vippsspecialpagetemplate' => array(
1538 - '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()),
1539 1595 'label' => sprintf(__('Use specific template for %1$s', 'woo-vipps'), Vipps::CompanyName()),
1540 1596 'type' => 'select',
1541 1597 'options' => $page_templates,
1542 - '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()),
1543 1599 'default' => ''),
1544 1600
1601 + // Deprecated, not shown anymore: TODO: remove this option in future. LP 2026-09-01
1545 1602 'vippsspecialpageid' => array(
1546 1603 'title' => sprintf(__('Use a real page ID for the special %1$s pages - neccessary for some themes', 'woo-vipps'), Vipps::CompanyName()),
1547 1604 'label' => __('Use a real page ID', 'woo-vipps'),
1548 1605 'type' => 'select',
1549 - 'options' => $page_list,
1606 + 'options' => [],
1550 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()),
1551 - 'default'=>''),
1608 + 'default' => ''
1609 + ),
1552 1610
1553 1611 'sendreceipts' => array(
1554 1612 'title' => __("Send receipts and order confirmation info to the customers' app on completed purchases.", 'woo-vipps'),
1555 1613 'label' => sprintf(__("Send receipts to the customers %1\$s app", 'woo-vipps'), Vipps::CompanyName()),
@@ -1565,17 +1623,8 @@
1565 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()),
1566 1624 'default' => 0,
1567 1625 ),
1568 1626
1569 -
1570 - 'use_flock' => array (
1571 - 'title' => __('Use flock() to lock orders for Express Checkout', 'woo-vipps'),
1572 - 'label' => __('Use flock() to lock orders for Express Checkout', 'woo-vipps'),
1573 - 'type' => 'checkbox',
1574 - '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'),
1575 - 'default' => 'no',
1576 - ),
1577 -
1578 1627 'delete_settings_on_deactivation' => array (
1579 1628 'title' => __('Delete plugin settings on deactivation', 'woo-vipps'),
1580 1629 'label' => __('Delete plugin settings on deactivation', 'woo-vipps'),
1581 1630 'type' => 'checkbox',
@@ -1713,9 +1762,9 @@
1713 1762 $ok = apply_filters('woo_vipps_is_available', $ok, $this);
1714 1763 return $ok;
1715 1764 }
1716 1765
1717 - // True if the alternative Vipps Checkout screen is both available and activated. Returns the page id of the checkout
1766 + // True if the alternative Checkout screen is both available and activated. Returns the page id of the checkout
1718 1767 // page for convenience. IOK 2021-10-01
1719 1768 public function vipps_checkout_available () {
1720 1769
1721 1770 if ($this->get_option('vipps_checkout_enabled') != 'yes') return false;
@@ -1786,9 +1835,8 @@
1786 1835 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');
1787 1836 return [];
1788 1837 }
1789 1838
1790 -
1791 1839 // From the request, get either [billing_phone] => or [vipps phone]
1792 1840 $phone = '';
1793 1841 if (isset($_POST['vippsphone'])) {
1794 1842 $phone = trim(sanitize_text_field($_POST['vippsphone']));
@@ -1881,9 +1929,8 @@
1881 1929 $limited_session = $this->generate_authtoken();
1882 1930 $returnurl = add_query_arg('ls',$limited_session,$returnurl);
1883 1931 $returnurl = add_query_arg('id', $order_id, $returnurl);
1884 1932
1885 -
1886 1933 try {
1887 1934 // If the order was 'failed', it isnt any more! yet!
1888 1935 if ($order->get_status() == 'failed') {
1889 1936 $order->set_status('pending', __('Setting order status to pending to start payment', 'woo-vipps'));
@@ -1931,12 +1978,14 @@
1931 1978 $order->update_meta_data('_vipps_init_timestamp',$vippstamp);
1932 1979 $order->update_meta_data('_vipps_orderurl', $url);
1933 1980
1934 1981 $order->update_meta_data('_vipps_status','INITIATE'); // INITIATE right now
1935 - $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), $this->get_payment_method_name()));
1936 - $order->add_order_note(sprintf(__('Awaiting %1$s payment confirmation','woo-vipps'), $this->get_payment_method_name()));
1982 +
1983 + $name = $this->get_payment_method_name();
1984 + $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), $name));
1985 + $order->add_order_note(sprintf(__('Awaiting %1$s payment confirmation','woo-vipps'),$name));
1986 +
1937 1987 $order->save();
1938 -
1939 1988 // Create a signal file that we can check without calling wordpress to see if our result is in IOK 2018-05-04
1940 1989 try {
1941 1990 $Vipps->createCallbackSignal($order);
1942 1991 } catch (Exception $e) {
@@ -1941,9 +1990,8 @@
1941 1990 $Vipps->createCallbackSignal($order);
1942 1991 } catch (Exception $e) {
1943 1992 // Could not create a signal file, but that's ok.
1944 1993 }
1945 -
1946 1994 do_action('woo_vipps_before_redirect_to_vipps',$order_id);
1947 1995
1948 1996 // This will send us to a receipt page where we will do the actual work. IOK 2018-04-20
1949 1997 return array('result'=>'success','redirect'=>$url);
@@ -2072,12 +2120,11 @@
2072 2120 if ($api == 'banktransfer') {
2073 2121 // This is an error - we should not ever get to the 'capture' branch if we are a banktransfer payment.
2074 2122 // IOK 2024-01-09
2075 2123 $content = [];
2076 - } elseif ($api == 'epayment') {
2124 + } else {
2125 + // Now the only other api is 'epayment' IOK 2026-08-18
2077 2126 $content = $this->api->epayment_capture_payment($order,$amount,$requestid);
2078 - } else {
2079 - $content = $this->api->capture_payment($order,$amount,$requestid);
2080 2127 }
2081 2128 } catch (TemporaryVippsApiException $e) {
2082 2129 $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');
2083 2130 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . "\n" . $e->getMessage());
@@ -2128,8 +2175,9 @@
2128 2175 return false;
2129 2176 }
2130 2177 // We'll use the same transaction id for all cancel jobs, as we can only do it completely. IOK 2018-05-07
2131 2178 // For epayment, partial cancellations will be possible. IOK 2022-11-12
2179 + // IOK 2026-08-18 actually, epayment does *not* support partial cancellation - all remaining funds are cancelled.
2132 2180 $api = $order->get_meta('_vipps_api');
2133 2181 try {
2134 2182 $requestid = "";
2135 2183 if ($api == 'banktransfer') {
@@ -2134,22 +2182,14 @@
2134 2182 $requestid = "";
2135 2183 if ($api == 'banktransfer') {
2136 2184 // If we are here, and the order is somehow not captured, just do nothing. IOK 2024-01-09
2137 2185 $content = [];
2138 - } elseif ($api == 'epayment') {
2186 + } else {
2187 + // api is here 'epayment'. IOK 2026-07-18
2139 2188 $requestid = 1;
2140 2189 // This will cancel any remaining, not-captured amount IOK 2026-01-28
2141 2190 $content = $this->api->epayment_cancel_payment($order,$requestid);
2142 - } else {
2143 - // If we have captured the order, we can't cancel it with the ecom API IOK 2018-05-07
2144 - $captured = intval($order->get_meta('_vipps_captured'));
2145 - if ($captured>0) {
2146 - $msg = sprintf(__('Cannot cancel a captured %1$s transaction - use refund instead', 'woo-vipps'), "ECOM " . $this->get_payment_method_name());
2147 - $this->adminerr($msg);
2148 - return false;
2149 - }
2150 - $content = $this->api->cancel_payment($order,$requestid);
2151 - }
2191 + }
2152 2192 } catch (TemporaryVippsApiException $e) {
2153 2193 $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');
2154 2194 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage());
2155 2195 return false;
@@ -2163,9 +2203,8 @@
2163 2203 // the epay v2 API would return transactionInfo and Summary with the result, the new epayment api returns nothing.
2164 2204 // Removed epay branch 2025-08-12 IOK
2165 2205 $total = intval($order->get_meta('_vipps_amount'));
2166 2206 $captured = intval($order->get_meta('_vipps_captured'));
2167 -# $cancelled = $amount + intval($order->get_meta('_vipps_cancelled');
2168 2207 $cancelled = $total;
2169 2208 $remaining = $total - $captured - $cancelled;
2170 2209
2171 2210 // We need to assume it worked. Also, we can't do partial cancels yet, so just cancel everything.
@@ -2213,13 +2252,12 @@
2213 2252 if ($api == 'banktransfer') {
2214 2253 $msg = sprintf(__("Cannot refund bank transfer order %1\$d", 'woo-vipps'), $order->get_id());
2215 2254 $this->log($msg, 'error');
2216 2255 throw new Exception($msg);
2217 - } elseif ($api == 'epayment') {
2256 + } else {
2257 + // api is now 'epayment' IOK 2026-08-18
2218 2258 $content = $this->api->epayment_refund_payment($order,$requestid,$amount,$cents);
2219 - } else {
2220 - $content = $this->api->refund_payment($order,$requestid,$amount,$cents);
2221 - }
2259 + }
2222 2260
2223 2261 $currency = $order->get_currency();
2224 2262
2225 2263 // Previously, we got updated transaction info in a transactionInfo field. this is no longer provided,
@@ -2309,15 +2347,15 @@
2309 2347 // Default should never happen, but just to ensure we are in our enumeration
2310 2348 return "initiated";
2311 2349 }
2312 2350
2313 - // This does not normally call Vipps, so if you need to refresh status, please use callback_check_order_status first. IOK 2019-01-23
2351 + // This does not normally call Vipps, so if you need to refresh status, please use poll_and_check_order_status first. IOK 2019-01-23
2314 2352 public function check_payment_status($order) {
2315 2353 if (!$order) return 'cancelled';
2316 2354 $status = $this->interpret_vipps_order_status($order->get_meta('_vipps_status'));
2317 2355 // This can happen if the vipps status is set from the back end for instance. IOK 2020-08-14
2318 2356 if ($order->get_status() == 'pending' && $status != 'initiated') {
2319 - $this->callback_check_order_status($order);
2357 + $this->poll_and_check_order_status($order);
2320 2358 $order = wc_get_order($order->get_id()); // refresh to get the new status IOK 2021-01-20
2321 2359 $status = $this->interpret_vipps_order_status($order->get_meta('_vipps_status'));
2322 2360 }
2323 2361 return $status;
@@ -2322,9 +2360,9 @@
2322 2360 }
2323 2361 return $status;
2324 2362 }
2325 2363
2326 - // Called by callback_check_order_status and handle_callback to handle the situation where
2364 + // Called by poll_and_check_order_status and handle_callback to handle the situation where
2327 2365 // the payment method has been set to something else *after* Vipps has gotten the order.
2328 2366 // This happens very rarely for people who use Vipps as an external payment method in Klarna, so
2329 2367 // we only do it for orders that match this. IOK 2023-02-03
2330 2368 public function reset_erroneous_payment_method($order) {
@@ -2339,8 +2377,10 @@
2339 2377 $checkout = $order->get_meta('_vipps_checkout');
2340 2378 $order->set_payment_method_title('Vipps');
2341 2379 if ($express) $order->set_payment_method_title('Vipps Express Checkout');
2342 2380 if ($checkout) $order->set_payment_method_title('Vipps Checkout');
2381 + // paypal gw resets payment gateway on order save because it has this meta, so delete it before save. LP 2026-06-23
2382 + $order->delete_meta_data('_ppcp_paypal_order_id');
2343 2383 $order->save();
2344 2384
2345 2385 $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());
2346 2386 $this->log($msg, 'debug');
@@ -2349,9 +2389,9 @@
2349 2389 }
2350 2390
2351 2391 // Check status of order at Vipps, in case the callback has been delayed or failed.
2352 2392 // Should only be called if in status 'pending'; it will modify the order when status changes.
2353 - public function callback_check_order_status($order, $allow_retry = true) {
2393 + public function poll_and_check_order_status($order, $allow_retry = true) {
2354 2394 global $Vipps;
2355 2395 $orderid = $order->get_id();
2356 2396
2357 2397 clean_post_cache($order->get_id());
@@ -2384,15 +2424,8 @@
2384 2424 }
2385 2425 // Something changed, so we are now going to sideeffect the order. IOK 2025-10-15
2386 2426 $this->log(sprintf(__("%1\$s poll: Handling order: ", 'woo-vipps'), Vipps::CompanyName()) . " " . $orderid, 'debug');
2387 2427
2388 - // If we are in the process of getting a callback from vipps, don't update anything. Currently, Woo/WP has no locking mechanism,
2389 - // and it isn't feasible to implement one portably. So this reduces somewhat the likelihood of races when this method is called
2390 - // and callbacks happen at the same time.
2391 - if (!$Vipps->lockOrder($order)) {
2392 - return $oldstatus;
2393 - }
2394 -
2395 2428 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
2396 2429 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
2397 2430 if ($ready) {
2398 2431 $this->reset_erroneous_payment_method($order);
@@ -2401,10 +2434,8 @@
2401 2434 $order->update_meta_data('_vipps_status',$newvippsstatus);
2402 2435
2403 2436 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
2404 2437 if (!empty($paymentdetails)) {
2405 -
2406 -
2407 2438 // checkout has a string, epayment has an array with upper case "type" and apparently, cardBin IOK 2025-08-12
2408 2439 $paymentMethod = $paymentdetails['paymentMethod'] ?? "epayment";
2409 2440 // After normalization, all APIs will have data here.
2410 2441 $details = $paymentdetails['paymentDetails'];
@@ -2426,9 +2457,8 @@
2426 2457
2427 2458 } catch (Exception $e) {
2428 2459 $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');
2429 2460 clean_post_cache($order->get_id());
2430 - $Vipps->unlockOrder($order);
2431 2461 return $oldstatus;
2432 2462 }
2433 2463 $order->save();
2434 2464
@@ -2461,9 +2491,8 @@
2461 2491 $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()));
2462 2492 }
2463 2493 }
2464 2494 clean_post_cache($order->get_id());
2465 - $Vipps->unlockOrder($order);
2466 2495 return $oldstatus;
2467 2496 }
2468 2497 }
2469 2498
@@ -2499,9 +2528,8 @@
2499 2528
2500 2529 $order->save();
2501 2530 clean_post_cache($order->get_id());
2502 2531 $newstatus = $order->get_status();
2503 - $Vipps->unlockOrder($order);
2504 2532 return $newstatus;
2505 2533 }
2506 2534
2507 2535 // 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
@@ -2617,73 +2645,11 @@
2617 2645 if (in_array($newstatus, ['authorized', 'complete'])) {
2618 2646 $ready = true;
2619 2647 }
2620 2648
2621 -
2622 - // 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.
2623 - // 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
2624 - // 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
2625 - // Also we don't get this when the state is different from AUTHORIZED. Especially not ABORTED.
2626 - // IOK 2025-09-29: This is *no longer the case* . We actually now get userDetails every time we add the relevant scopes,
2627 - // so this is now probably dead code.
2628 - if ($ready && $express && !$checkout_session && !isset($result['userDetails'])) {
2629 -
2630 - $sub = isset($result['profile']) && isset($result['profile']['sub']) ? $result['profile']['sub'] : null;
2631 - $userinfo = [];
2632 - if (!$sub) {
2633 - // This should never happen, but be prepared
2634 - $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" );
2635 - $order->add_order_note($message);
2636 - $this->log($message , "error");
2637 - } else {
2638 - // If this happens, the merchant *may* be able to retrieve the information from Vipps so add a note for it.
2639 - try {
2640 - $userinfo = $this->api->get_userinfo($sub);
2641 - } catch (Exception $e) {
2642 - $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());
2643 - $order->add_order_note($message);
2644 - $this->log($message, 'woo-vipps', "error");
2645 - }
2646 - }
2647 - if ($userinfo) {
2648 - $userDetails = array(
2649 - 'email_verified' => $userinfo['email_verified'],
2650 - 'email' => $userinfo['email'],
2651 - 'firstName' => $userinfo['given_name'] ?? '',
2652 - 'lastName' => $userinfo['family_name'] ?? '',
2653 - 'mobileNumber' => $userinfo['phone_number'] ?? '',
2654 - 'phoneNumber' => $userinfo['phone_number'] ?? '',
2655 - 'userId' => $userinfo['phone_number'] ?? '',
2656 - 'sub' => $userinfo['sub']
2657 - );
2658 -
2659 - $result['userDetails'] = $userDetails;
2660 -
2661 - // We may have asked for the address of the customer, so add that too, or a dummy.
2662 - if (!isset($result['shippingDetails'])) {
2663 - $countries=new WC_Countries();
2664 - $address =[];
2665 - $address['addressLine1'] = "";
2666 - $address['addressLine2'] = "";
2667 - $address['city'] ="";
2668 - $address['postCode'] = "";
2669 - $address['country'] = $countries->get_base_country();
2670 -
2671 - // This uses other keys than both epayment and checkout, but we'll normalize it later. IOK 2025-08-13
2672 - if (isset($userinfo['address'])) {
2673 - $address['addressLine1'] = $userinfo['address']['street_address'];
2674 - $address['city'] = $userinfo['address']['region'];
2675 - $address['country'] = $userinfo['address']['country'];
2676 - $address['postCode'] = $userinfo['address']['postal_code'];
2677 - }
2678 - $result['shippingDetails'] = ['address' => $address];
2679 - }
2680 - }
2681 - }
2682 -
2683 2649 if ($ready && ($express || $checkout_session)) {
2684 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
2685 - // This will also normalize userDetails, adding 'sub' where required and fields for backwards compatibility. 2025-08-12
2650 + // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
2651 + // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
2686 2652 $result = $this->ensure_userDetails($result, $order);
2687 2653
2688 2654 // 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
2689 2655 // 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
@@ -2755,9 +2721,9 @@
2755 2721 return $result;
2756 2722 }
2757 2723
2758 2724
2759 - // IOK 2024-01-09 If using Vipps Checkout with the BankTransfer method, which is eg. used in Finland,
2725 + // IOK 2024-01-09 If using Checkout with the BankTransfer method, which is eg. used in Finland,
2760 2726 // we are (currently) not receiving any 'state' or 'aggregate', so add this iff the payment is successful.
2761 2727 // The reason for this is that this payment type does not actually use the epayment API at all (!)
2762 2728 // Also moved some other compatibility code here -
2763 2729 // --- reference used to be orderId
@@ -2826,9 +2792,9 @@
2826 2792
2827 2793 return $result;
2828 2794 }
2829 2795
2830 - // Vipps Checkout v3 does *not* provide userDetails. Vipps Checkout v2 and epayment *does*. But Checkout additionally allows
2796 + // Checkout v3 does *not* provide userDetails. Checkout v2 and epayment *does*. But Checkout additionally allows
2831 2797 // 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.
2832 2798 // 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
2833 2799 // if not, we use shippingDetails. IOK 2023-01-10
2834 2800 // Also, epayment uses mobileNumber and checkout uses phoneNumber, so normalize.
@@ -2838,8 +2804,9 @@
2838 2804 // If we have userDetails, use it (ecom API with user data requested - Express Checkout
2839 2805 if (isset($vippsdata['userDetails'])) {
2840 2806 $userDetails = $vippsdata['userDetails'];
2841 2807 // 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
2808 + // 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
2842 2809 $sub = "";
2843 2810 if (isset($vippsdata['profile']) && isset($vippsdata['profile']['sub'])) {
2844 2811 $sub = $vippsdata['profile']['sub'];
2845 2812 }
@@ -3204,9 +3171,14 @@
3204 3171 $is_base64 = $shipping_table ? ( $shipping_table['_is_base64'] ?? false) : false;
3205 3172
3206 3173 if (is_array($shipping_table) && isset($shipping_table[$key])) {
3207 3174 $decoded = $is_base64 ? @base64_decode($shipping_table[$key]) : $shipping_table[$key];
3208 - $shipping_rate = $decoded ? @unserialize($decoded) : null;
3175 +
3176 + // Ensure no shop manager has injected an evil object (that they would have had to add as a plugin) here. IOK 2026-09-18
3177 + $allowed_classes = apply_filters('woo_vipps_express_checkout_allowed_shipping_classes', [WC_Shipping_Rate::class, \stdClass::class]);
3178 + $shipping_rate = $decoded ? @unserialize($decoded, ['allowed_classes' => $allowed_classes]) : null;
3179 + $shipping_rate = is_a($shipping_rate,'WC_Shipping_Rate') ? $shipping_rate : null;
3180 +
3209 3181 if (!$shipping_rate) {
3210 3182 $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');
3211 3183 $this->log(sprintf(__("Serialized data was %1\$s", 'woo-vipps'), $decoded), 'error');
3212 3184 } else {
@@ -3226,9 +3198,9 @@
3226 3198 }
3227 3199 }
3228 3200 }
3229 3201
3230 - // Possible extra metadata from Vipps Checkout IOK 2023-01-17
3202 + // Possible extra metadata from Checkout IOK 2023-01-17
3231 3203 // Store in the order, but also in the shipping rate so it will be visible in the order screen
3232 3204 // along with the shipping ragte
3233 3205 if (isset($shipping['pickupPoint'])) {
3234 3206 $order->update_meta_data('vipps_checkout_pickupPoint', $shipping['pickupPoint']);
@@ -3288,9 +3260,9 @@
3288 3260 $methodclass = $methods_classes[$shipping_rate->get_method_id()] ?? null;
3289 3261 $shipping_method = $methodclass ? new $methodclass($shipping_rate->get_instance_id()) : null;
3290 3262 $is_vipps_checkout_shipping = $shipping_method && is_a($shipping_method, 'VippsCheckout_Shipping_Method');
3291 3263
3292 - // Some Vipps Checkout-specific shipping methods calculate the cost in the Vipps window.
3264 + // Some Checkout-specific shipping methods calculate the cost in the Vipps window.
3293 3265 if ($is_vipps_checkout_shipping && $shipping_method->dynamic_cost) {
3294 3266 $vippsamount = intval($order->get_meta('_vipps_amount'));
3295 3267 $shipping_tax_rate = floatval($order->get_meta('_vipps_shipping_tax_rates'));
3296 3268 $compareamount = $ordertotal * 100;
@@ -3326,9 +3298,9 @@
3326 3298
3327 3299 $order->set_total($ordertotal + $total_shipping + $total_shipping_tax);
3328 3300 $order->update_taxes(); // Necessary for the admin view only; does not recalculate order.
3329 3301
3330 - // Add an early hook for Vipps Checkout orders with special shipping methods
3302 + // Add an early hook for Checkout orders with special shipping methods
3331 3303 $metadata = $shipping_rate->get_meta_data();
3332 3304 if (isset($metadata['type'])) {
3333 3305 do_action('woo_vipps_checkout_special_shipping_method', $order, $shipping_rate, $metadata['type']);
3334 3306 }
@@ -3344,9 +3316,9 @@
3344 3316
3345 3317
3346 3318 // 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.
3347 3319 // If possible and safe, user will be logged in before being sent to the thankyou screen. IOK 2020-10-09
3348 - // Same thing for Vipps Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3320 + // Same thing for Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3349 3321 $customer = false;
3350 3322 if ($assigncustomer) {
3351 3323 $customer = Vipps::instance()->express_checkout_get_vipps_customer($order);
3352 3324 }
@@ -3354,8 +3326,9 @@
3354 3326 // 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
3355 3327 // the same as the 'sub' we get in Login so that must be a future feature. IOK 2020-10-09
3356 3328 // 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
3357 3329 // after calling get_userinfo, so we'll leave that be.
3330 + // 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
3358 3331 if (class_exists('VippsWooLogin') && $customer && !is_wp_error($customer) && !get_user_meta($customer->get_id(), '_vipps_phone',true)) {
3359 3332 update_user_meta($customer->get_id(), '_vipps_phone', $billing['phoneNumber']);
3360 3333 if (isset($user['sub'])) {
3361 3334 $userid = $customer->get_id();
@@ -3389,9 +3362,9 @@
3389 3362 $shipping_rate = apply_filters('woo_vipps_express_checkout_shipping_rate',$shipping_rate,$costExTax,$tax,$method,$product);
3390 3363 return $shipping_rate;
3391 3364 }
3392 3365
3393 - // 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
3366 + // Used by both poll_and_check_order_status and handle_callback - sets the neccessary order metadata after a successful (or not vipps transaction). IOK 2025-08-13
3394 3367 public function order_set_transaction_metadata($order, $transaction) {
3395 3368 // Set Vipps metadata as early as possible
3396 3369 $vippsstamp = strtotime($transaction['timeStamp']);
3397 3370 $vippsamount = $transaction['amount'] ?? '';
@@ -3415,9 +3388,9 @@
3415 3388 $order->update_meta_data('_vipps_api', 'banktransfer');
3416 3389 }
3417 3390 }
3418 3391
3419 - // Handle the callback from Vipps eCom.
3392 + // Handle the callback from Vipps ePayment
3420 3393 public function handle_callback($result, $order, $ischeckout=false, $iswebhook=false) {
3421 3394 global $Vipps;
3422 3395
3423 3396 $vippsorderid = $result['orderId'];
@@ -3425,55 +3398,121 @@
3425 3398
3426 3399 $keyset = $this->get_keyset();
3427 3400 $me = array_keys($keyset);
3428 3401
3402 + // Validate the callback first
3429 3403 if (!in_array($merchant, $me)) {
3430 3404 $this->log(sprintf(__("%1\$s callback with wrong merchantSerialNumber - might be forged",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3431 3405 return false;
3432 3406 }
3433 -
3434 3407 if (!$order) {
3435 3408 $this->log(sprintf(__("%1\$s callback for unknown order",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3436 3409 return false;
3437 3410 }
3438 - $orderid = $order->get_id();
3439 - // We may need to use poll to get data, depending on the content passed.
3440 - $express = $order->get_meta('_vipps_express_checkout');
3441 - $checkout_session = $order->get_meta('_vipps_checkout_session');
3442 -
3411 + $order_id = $order->get_id();
3443 3412 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3444 - $this->log(sprintf(__("Wrong %1\$s Orderid - possibly an attempt to fake a callback ", 'woo-vipps'), Vipps::CompanyName()), 'warning');
3445 - clean_post_cache($order->get_id());
3413 + $this->log(sprintf(__('Wrong %1$s Orderid - possibly an attempt to fake a callback ', 'woo-vipps'), Vipps::CompanyName()), 'warning');
3414 + clean_post_cache($order_id);
3446 3415 exit();
3447 3416 }
3448 3417
3418 + // Note any errors in the callback early
3449 3419 $errorInfo = $result['errorInfo'] ?? '';
3450 3420 if ($errorInfo) {
3451 - $this->log(sprintf(__("Message in callback from %1\$s for order",'woo-vipps'), $this->get_payment_method_name()) . ' ' . $orderid . ' ' . $errorInfo['errorMessage'],'error');
3452 - $order->add_order_note(sprintf(__("Message from %1\$s: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3421 + /* translators: payment method name, order id */
3422 + $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');
3423 + /* translators: payment method name, message */
3424 + $order->add_order_note(sprintf(__('Message from %1$s: %2$s','woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3453 3425 }
3454 3426
3427 + // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3428 + try {
3429 + $Vipps->createCallbackSignal($order,'ok');
3430 + } catch (Exception $e) {
3431 + // Could not create a signal file, but that's ok.
3432 + }
3433 +
3434 + // New callback handling: schedule an Action Scheduler job to process it. The purpose of processing callback
3435 + // is to set finalize the order (set order status, set shipping for express) in the case when customer does
3436 + // not return to the store, because then poll does not run. LP 2026-03-27
3437 +
3438 + // Below is separate from '_vipps_callback_timestamp' which is when callback is sent,
3439 + // but also that meta won't be stored until callback is actually processed. LP 2026-03-30
3440 + $order->update_meta_data('_vipps_callback_received_at', time());
3441 + // Store the callback data in the order. We'll do a cleanup of this when the scheduled job runs. IOK 2026-04-21
3442 + $order->update_meta_data('_vipps_callback_data', $result);
3443 + $order->save_meta_data();
3444 +
3445 + // Run callback actions for callback received as soon as it is actually received.
3446 + $transaction = []; // No longer provided. IOK 2026-05-06
3447 + do_action('woo_vipps_callback_received', $order, $result, $transaction);
3448 +
3449 + $action_args = [
3450 + 'order_id' => $order->get_id(),
3451 + 'is_checkout' => $ischeckout,
3452 + 'is_webhook' => $iswebhook,
3453 + ];
3454 + // 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
3455 + // and have the order finalized the 'normal' way, but if they don't, we'll handle it async. 2026-04-21
3456 + $scheduled_at = time() + 60;
3457 + $action_id = as_schedule_single_action($scheduled_at, 'woo_vipps_action_process_callback', $action_args, 'woo-vipps', false);
3458 + if ($action_id) {
3459 + /* translators: order id, scheduled time */
3460 + $this->log(sprintf(__('Callback action scheduled at %2$s for order %1$s', 'woo-vipps'), $order->get_id(), $scheduled_at), 'info');
3461 + } else {
3462 + // 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
3463 + /* translators: order id */
3464 + $this->log(sprintf(__('Failed to schedule callback process action for order %1$s, check the php error log', 'woo-vipps'), $order->get_id()), 'error');
3465 + /* We will not delete the callback data here, to facilitate debugging. But return false to indicate that callback handling will fail. */
3466 + /* NB: The order will still be processed with the periodic job, at a later stage. IOK 2026-05-06 */
3467 + return false;
3468 + }
3469 +
3470 + // Signal that we in fact handled the order.
3471 + return true;
3472 + }
3473 +
3474 + /** Runs in action scheduler: sync woo status from Vipps callback data. Handle shipping etc. for Express. LP 2026-03-31 */
3475 + public function action_process_callback($order_id, $is_checkout, $is_webhook) {
3476 + $order = wc_get_order($order_id);
3477 + if (!is_a($order, 'WC_Order')) {
3478 + /* translators: order id */
3479 + $this->log(sprintf(__('Callback process action failed, could not find order %1$s','woo-vipps'), $order_id), 'error');
3480 + return false;
3481 + }
3482 +
3483 + $oldstatus = $order->get_status();
3484 + if ($oldstatus != 'pending') {
3485 + // Actually, we are ok with this order, abort the callback handler. IOK 2018-05-30
3486 + $order->delete_meta_data('_vipps_callback_data');
3487 + clean_post_cache($order->get_id());
3488 + return false;
3489 + }
3490 + $data = $order->get_meta('_vipps_callback_data');
3491 +
3492 + /* translators: order id */
3493 + $this->log(sprintf(__('Callback process action running for order %1$s.', 'woo-vipps'), $order_id));
3494 +
3455 3495 // The payment details field is passed in Checkout, not in Express, but none of them are complete, so we fill out the values
3456 3496 // depending on which one we are IOK 2025-08-13
3457 3497 $details = [];
3458 3498 // Checkout has this as a field, containing *some* of the neccessary data
3459 - if (isset($result['paymentDetails'])) {
3499 + if (isset($data['paymentDetails'])) {
3460 3500 // Checkout. The sesssion states are # "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
3461 3501 // -- we should only get callbacks for successful sessions actually.
3462 - $details = $result['paymentDetails'];
3463 - $result['state'] = $result['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($result['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3464 - $details['state'] = $result['state'];
3465 - $details['paymentMethod'] = $result['paymentMethod'];
3502 +
3503 + $details = $data['paymentDetails'];
3504 + $data['state'] = $data['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($data['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3505 + $details['state'] = $data['state'];
3506 + $details['paymentMethod'] = $data['paymentMethod'];
3466 3507 } else {
3467 - // This should be an ecom callback; which we need to add a lot of data for to get a valid "paymentDetails".
3508 + // This should be an epayment webhook callback; which we need to add a lot of data for to get a valid "paymentDetails".
3468 3509 $details = [];
3469 - $result['state'] = $result['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3470 - $details['state'] = $result['name'];
3471 - $details['amount'] = $result['amount']; // currency, value
3510 + $data['state'] = $data['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3511 + $details['state'] = $data['name'];
3512 + $details['amount'] = $data['amount']; // currency, value
3472 3513 $details['paymentMethod'] = 'epayment';
3473 - $currency = $details['amount']['currency'];
3474 - $nothing = [ 'currency' => $currency, 'value' => 0];
3475 - }
3514 + }
3476 3515
3477 3516 // For both callbacks, set 'aggregate'
3478 3517 $currency = $details['amount']['currency'];
3479 3518 $nothing = [ 'currency' => $currency, 'value' => 0];
@@ -3478,68 +3517,52 @@
3478 3517 $currency = $details['amount']['currency'];
3479 3518 $nothing = [ 'currency' => $currency, 'value' => 0];
3480 3519 $aggregate = ['authorizedAmount' => $nothing, 'cancelledAmount' => $nothing, 'capturedAmount' => $nothing, 'refundedAmount' => $nothing];
3481 3520 if ($details['state'] == 'AUTHORIZED') {
3482 - $aggregate['authorizedAmount'] = $details['amount'];
3521 + $aggregate['authorizedAmount'] = $details['amount'];
3483 3522 }
3484 3523 $details['aggregate'] = $aggregate;
3485 - $result['paymentDetails'] = $details;
3524 + $data['paymentDetails'] = $details;
3486 3525
3487 - $result = $this->normalizePaymentDetails($result);
3488 - $details = $result['paymentDetails'];
3526 + // Do the actual work with a function shared with the periodic job.
3527 + $this->set_order_status_by_payment_details($order,$data);
3489 3528
3490 - $vippsstatus = $result['status']; // Will exist now, because of the normalization IOK 2025-08-13
3529 + /* translators: payment method name */
3530 + $order->add_order_note(sprintf(__('%1$s callback processed','woo-vipps'), $this->get_payment_method_name()));
3531 +
3532 + // We're done, so delete the callback data. IOK 2026-04-22
3533 + $order->delete_meta_data('_vipps_callback_data');
3534 + }
3535 +
3536 +
3537 + // Called either by periodic job or by action_process_callback with the callback data *or* with payment details fetched with poll.
3538 + // sets order status if neccessary, and will finalize the order for Express via HTTP call if necessary. IOK 2026-05-06
3539 + public function set_order_status_by_payment_details($order, $data, $allow_retry=true) {
3540 + $data = $this->normalizePaymentDetails($data);
3541 + $details = $data['paymentDetails'];
3542 + $order_id = $order->get_id();
3543 +
3544 + $vippsstatus = $data['status']; // Will exist now, because of the normalization IOK 2025-08-13
3491 3545 $newstatus = $this->interpret_vipps_order_status($vippsstatus);
3492 3546
3493 3547 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
3494 3548 $transaction = array();
3495 - $stamp = ($result['timestamp'] ?? false) ? strtotime($result['timestamp']) : time();
3549 + $stamp = ($data['timestamp'] ?? false) ? strtotime($data['timestamp']) : time();
3496 3550 $transaction['timeStamp'] = date('Y-m-d H:i:s', $stamp);
3497 3551 $transaction['amount'] = $details['amount']['value'];
3498 3552 $transaction['currency'] = $details['amount']['currency'];
3499 - $transaction['status'] = ($result['state'] ?? $details['state']);
3553 + $transaction['status'] = ($data['state'] ?? $details['state']);
3500 3554 $transaction['paymentmethod'] = $details['paymentMethod'] ?? "";
3555 + $this->order_set_transaction_metadata($order, $transaction);
3501 3556
3502 - if (!$transaction) {
3503 - $this->log(sprintf(__("Anomalous callback from %1\$s, handle errors and clean up",'woo-vipps'), $this->get_payment_method_name()),'warning');
3504 - clean_post_cache($order->get_id());
3505 - return false;
3557 + // Dont do anything if order is not finalized. LP 2026-08-31
3558 + if (!in_array($newstatus, ['authorized', 'complete', 'cancelled'])) {
3559 + return;
3506 3560 }
3507 3561
3508 - $order->add_order_note(sprintf(__('%1$s callback received','woo-vipps'), $this->get_payment_method_name()));
3509 - do_action('woo_vipps_callback_received', $order, $result, $transaction);
3562 + // This order is ready to set order shipping details etc for IOK 2025-09-19
3563 + $ready = in_array($newstatus, ['authorized', 'complete']);
3510 3564
3511 - $oldstatus = $order->get_status();
3512 - if ($oldstatus != 'pending') {
3513 - // Actually, we are ok with this order, abort the callback. IOK 2018-05-30
3514 - clean_post_cache($order->get_id());
3515 - return false;
3516 - }
3517 -
3518 - // 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.
3519 - // 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
3520 - // when callbacks happen while we are polling for results. IOK 2018-05-30
3521 - if (!$Vipps->lockOrder($order)) {
3522 - clean_post_cache($order->get_id());
3523 - return false;
3524 - }
3525 -
3526 - // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3527 - // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3528 - // made to the session here will be saved. Sorry.
3529 - $Vipps->callback_restore_session($orderid);
3530 -
3531 - // Set Vipps metadata as early as possible
3532 - $this->order_set_transaction_metadata($order, $transaction);
3533 -
3534 - $this->log(sprintf(__("%1\$s callback: Handling order: ", 'woo-vipps'), Vipps::CompanyName()) . " " . $orderid, 'debug');
3535 -
3536 -
3537 - // This order is ready to set order shipping details etc for IOK 2025-09-19
3538 - $ready = false;
3539 - if (in_array($newstatus, ['authorized', 'complete'])) {
3540 - $ready = true;
3541 - }
3542 3565 if ($ready) {
3543 3566 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
3544 3567 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3545 3568 $this->reset_erroneous_payment_method($order);
@@ -3544,40 +3567,38 @@
3544 3567 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3545 3568 $this->reset_erroneous_payment_method($order);
3546 3569 }
3547 3570
3548 - if ($ready && ($express || $ischeckout)) {
3549 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3550 - // This will also normalize userDetails, adding 'sub' where required and fields for backwards compatibility. 2025-08-12
3551 - $result = $this->ensure_userDetails($result, $order);
3571 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3552 3572
3553 - // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3554 - // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3555 - // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3556 - // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3557 - // we have added a scope of any kind. This is therefore probably dead code.
3558 - // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3559 - if (!isset($result['userDetails'])) {
3560 - // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3561 - try {
3562 - $details = $this->get_payment_details($order);
3563 - $result = $details;
3564 - } catch (Exception $e) {
3565 - $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3566 - $this->log($e->getMessage());
3567 - }
3568 - }
3569 -
3570 - // 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
3571 - $result = $this->normalizeShippingDetails($result, $order);
3572 -
3573 - // We should now always have shipping details.
3574 - if (isset($result['shippingDetails'])) {
3575 - $billing = isset($result['billingDetails']) ? $result['billingDetails'] : false;
3576 - $this->set_order_shipping_details($order,$result['shippingDetails'], $result['userDetails'], $billing, $result);
3573 + // Handle session and shipping through http, because we dont want to mess with session here in wp cron (action scheduler). LP 2026-03-30
3574 + // NB: This is and must be a *synchronous call*. When done, the order will have shipping, addresses etc. IOK 2026-05-06.
3575 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3576 + if ($ready && $is_express_or_checkout && !$shipping_set) {
3577 + $token = $order->get_meta('_vipps_authtoken');
3578 + $args = [
3579 + 'body' => [
3580 + 'order_id' => $order_id,
3581 + 'vipps_order_data' => $data,
3582 + ],
3583 + 'headers' => [
3584 + 'X-WooVipps-Token' => $token,
3585 + ],
3586 + ];
3587 + $url = Vipps::get_rest_url('v1', '/order-set-shipping');
3588 + $response = wp_remote_post($url, $args);
3589 + if (is_wp_error($response)) {
3590 + /* translators: order id, error message */
3591 + $error_msg = $response->get_error_message();
3592 + $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');
3593 + } else if (200 != ($response['response']['code'] ?? -1)) {
3594 + /* translators: order id */
3595 + $response_msg = print_r($response['body'] ?? ['Missing response body'], true);
3596 + $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');
3577 3597 }
3578 3598 }
3579 3599
3600 + // This must happen *after* finalization for Express, as above. IOK 2026-05-06
3580 3601 // the only status we now care about is AUTHORIZED. Previously we had AUTHORISED and RESERVED and RESERVE as well. And SALE.
3581 3602 if ($vippsstatus == 'AUTHORIZED') {
3582 3603 $this->payment_complete($order);
3583 3604 } else if ($vippsstatus == 'SALE') {
@@ -3582,14 +3603,15 @@
3582 3603 $this->payment_complete($order);
3583 3604 } else if ($vippsstatus == 'SALE') {
3584 3605 // Direct capture needs special handling because most of the meta values we use are missing IOK 2019-02-26
3585 3606 // Actually not supported anymore, but keep logic. IOK 2025-08-13
3607 + // Still supported for finnish direct bank transfer. IOK 2026-04-22
3586 3608 $order->add_order_note(sprintf(__('Payment captured directly at %1$s', 'woo-vipps'), $this->get_payment_method_name()));
3587 3609 $order->payment_complete();
3588 3610 $this->update_vipps_payment_details($order);
3589 3611 } else {
3590 3612 // Not ok status; set to failed/cancelled
3591 - $order_is_retryable = Vipps::order_is_vipps_retryable($order->get_id());
3613 + $order_is_retryable = $allow_retry && Vipps::order_is_vipps_retryable($order->get_id());
3592 3614 $status_on_fail = $this->get_option('status_on_fail');
3593 3615 $cancel_on_fail = apply_filters('woo_vipps_cancel_failed_orders', false, $order, $vippsstatus);
3594 3616 if ($cancel_on_fail || !$order_is_retryable) {
3595 3617 $status_on_fail = 'cancelled';
@@ -3594,33 +3616,72 @@
3594 3616 if ($cancel_on_fail || !$order_is_retryable) {
3595 3617 $status_on_fail = 'cancelled';
3596 3618 }
3597 3619 if (!in_array($status_on_fail, ['cancelled', 'failed'])) {
3598 - /* translators: order status name. Cancelled is woocommerce status name */
3620 + /* translators: %1 = order status parameter. 'cancelled' is woocommerce order status name */
3599 3621 $this->log(__('Unsupported status for payment failure of \'%1$s\', falling back to cancelled.', 'woo-vipps'), 'warning');
3600 3622 $status_on_fail = 'cancelled';
3601 3623 }
3602 3624
3603 3625 /* translators: company name */
3604 - $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s', 'woo-vipps'), Vipps::CompanyName()));
3626 + $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
3605 3627 }
3606 3628
3607 3629 $order->save();
3608 - clean_post_cache($order->get_id());
3630 + clean_post_cache($order_id);
3631 + }
3609 3632
3610 - // 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
3611 - $Vipps->callback_restore_session($orderid);
3612 - $Vipps->unlockOrder($order);
3633 + /* finalize shipping for express/checkout order. LP 2026-03-30 */
3634 + public function rest_order_set_shipping($request) {
3635 + $order_id = $request->get_param('order_id');
3636 + $data = $request->get_param('vipps_order_data');
3613 3637
3614 - // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3615 - try {
3616 - $Vipps->createCallbackSignal($order,'ok');
3617 - } catch (Exception $e) {
3618 - // Could not create a signal file, but that's ok.
3638 + $order = wc_get_order($order_id);
3639 + if (!is_a($order, 'WC_Order')) {
3640 + return new WP_Error('order_not_found', __('Order not found', 'woo-vipps'), ['status' => 404]);
3619 3641 }
3620 3642
3621 - // Signal that we in fact handled the order.
3622 - return true;
3643 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3644 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3645 + if (!$is_express_or_checkout || $shipping_set) {
3646 + return new WP_Error('order_is_finalized', __('Order does not need to set shipping', 'woo-vipps'), ['status' => 409]);
3647 + }
3648 +
3649 + // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3650 + // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3651 + // made to the session here will be saved. Sorry.
3652 + // 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
3653 + Vipps::instance()->callback_restore_session($order_id);
3654 +
3655 + // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3656 + // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
3657 + $data = $this->ensure_userDetails($data, $order);
3658 +
3659 + // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3660 + // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3661 + // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3662 + // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3663 + // we have added a scope of any kind. This is therefore probably dead code.
3664 + // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3665 + if (!isset($data['userDetails'])) {
3666 + // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3667 + try {
3668 + $details = $this->get_payment_details($order);
3669 + $data = $details;
3670 + } catch (Exception $e) {
3671 + $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3672 + $this->log($e->getMessage());
3673 + }
3674 + }
3675 +
3676 + // 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
3677 + $data = $this->normalizeShippingDetails($data, $order);
3678 +
3679 + // We should now always have shipping details.
3680 + if (isset($data['shippingDetails'])) {
3681 + $billing = isset($data['billingDetails']) ? $data['billingDetails'] : false;
3682 + $this->set_order_shipping_details($order,$data['shippingDetails'], $data['userDetails'], $billing, $data);
3683 + }
3623 3684 }
3624 3685
3625 3686 // Do the 'payment_complete' logic for non-SALE orders IOK 2020-09-22
3626 3687 public function payment_complete($order,$transactionid='') {
@@ -3683,9 +3744,9 @@
3683 3744 }
3684 3745 do_action('woo_vipps_payment_complete_at_shutdown', $order, $this);
3685 3746 } catch (Exception $e) {
3686 3747 // This is/should be non-critical so just log it.
3687 - $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");
3748 + $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");
3688 3749 }
3689 3750 }
3690 3751
3691 3752 // This is run on payment complete. Per default will it only add a link to the order confirmation page, but
@@ -3792,9 +3853,9 @@
3792 3853
3793 3854 $contents = WC()->cart->get_cart_contents();
3794 3855 $contents = apply_filters('woo_vipps_create_express_checkout_cart_contents',$contents);
3795 3856 try {
3796 - $cart_hash = md5(json_encode(wc_clean($contents)) . WC()->cart->total);
3857 + $cart_hash = WC()->cart->get_cart_hash();
3797 3858 $order = new WC_Order();
3798 3859 $order->set_status('pending');
3799 3860 $order->set_payment_method($this);
3800 3861 if ($ischeckout) {
@@ -3805,8 +3866,9 @@
3805 3866 }
3806 3867 // We use 'checkout' as the created_via key as per requests, but allow merchants to use their own. IOK 2022-09-15
3807 3868 $created_via = apply_filters('woo_vipps_express_checkout_created_via', 'checkout', $order, $ischeckout);
3808 3869 $order->set_created_via($created_via);
3870 + $order->set_cart_hash($cart_hash);
3809 3871
3810 3872 $dummy = sprintf(__('Vipps Express Checkout', 'woo-vipps')); // this is so gettext will find this string.
3811 3873 $dummy = sprintf(__('Vipps Checkout', 'woo-vipps')); // this is so gettext will find this string.
3812 3874
@@ -3968,9 +4030,9 @@
3968 4030 </p>
3969 4031 </div>
3970 4032 <?php endif; ?>
3971 4033
3972 - <?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
4034 + <?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
3973 4035 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
3974 4036 ?>
3975 4037
3976 4038 <?php /* We will *not* allow vipps checkout to be activated at this point, since the product is no longer sold. IOK 2026-04-30 */ ?>
@@ -4031,8 +4093,23 @@
4031 4093 // If enabling this, ensure the page in question exists
4032 4094 if ($this->get_option('vipps_checkout_enabled') == 'yes') {
4033 4095 update_option('woo_vipps_checkout_activated', true, true); // This must be true here, but still, make sure
4034 4096 Vipps::instance()->maybe_create_vipps_pages();
4097 + }
4098 +
4099 + // Ensure special page has the necessary shortcode. LP 2026-09-01
4100 + $special_page = get_post(Vipps::get_special_page_id());
4101 + if ($special_page && !has_shortcode($special_page->post_content, 'vipps_special_page')) {
4102 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4103 + wp_update_post([
4104 + 'ID' => Vipps::get_special_page_id(),
4105 + 'post_content' => $new_content,
4106 + ]);
4107 + } else if (!Vipps::get_special_page_id()) {
4108 + // We shouldn't really get here, the page should be ensured to exist in init. LP 2026-09-03
4109 + /* translators: %s is current method name */
4110 + $this->log(sprintf(__('Missing special page in %s, attempting to fix', 'woo-vipps'), 'process_admin_options'), 'warning');
4111 + Vipps::instance()->ensure_special_page_exists();
4035 4112 }
4036 4113
4037 4114 return $saved;
4038 4115 }