PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.5
Pay with Vipps and MobilePay for WooCommerce v6.2.5
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 5.4.3 5.4.2 All 187 releases
← All changes | payment/WC_Gateway_Vipps.class.php +377 -283 5.4.2 → 6.2.5 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
@@ -233,11 +297,9 @@
233 297 // Ensure we've updated the vipps status before calling this. IOK 2026-01-28
234 298 public function maybe_cancel_reserved_amount ($orderid) {
235 299 $order = wc_get_order($orderid);
236 300 if (!$order) return;
237 - if ('vipps' != $order->get_payment_method()) return false;
238 - // Cannot partially cancel legacy ecom orders
239 - if ('epayment' != $order->get_meta('_vipps_api')) return false;
301 + if (! Vipps::is_vipps_order($order)) 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 {
@@ -469,9 +534,9 @@
469 534 // IOK 2019-08-26
470 535 public function maybe_delete_order ($orderid) {
471 536 $order = wc_get_order($orderid);
472 537 if (!$order) return;
473 - if ('vipps' != $order->get_payment_method()) return false;
538 + if (! Vipps::is_vipps_order($order)) return false;
474 539 $express = $order->get_meta('_vipps_express_checkout');
475 540 if (!$express) return false;
476 541 $email = $order->get_billing_email();
477 542 if ($email) return false;
@@ -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
@@ -674,9 +716,9 @@
674 716
675 717 // Called when orders reach the 'refunded' status. We'll add a complete refund and note that any rest is to be cancelled.
676 718 public function maybe_refund_order($order_id) {
677 719 $order = wc_get_order($order_id);
678 - if ('vipps' != $order->get_payment_method()) return false;
720 + if (! Vipps::is_vipps_order($order)) return false;
679 721 try {
680 722 $order = $this->update_vipps_payment_details($order);
681 723 } catch (Exception $e) {
682 724 //Do nothing with this for now
@@ -686,14 +728,12 @@
686 728 if ($payment == 'initiated' || $payment == 'cancelled') {
687 729 return true; // Can't refund these
688 730 }
689 731
690 - $captured = intval($order->get_meta('_vipps_captured'));
691 - $vippsstatus = $order->get_meta('_vipps_status');
692 - if ($captured > 0 || $vippsstatus == 'SALE') {
693 - // This will create + process a refund for the captured amount. IOK 2026-01-26
694 - $this->wc_order_fully_refunded ($order_id);
695 - }
732 + // This will create + process a refund for the captured amount (if any). IOK 2026-01-26
733 + // We always run this since woo will create a manual refund on this status change: we want the refund to be through our gw instead. LP 2026-06-10
734 + $this->wc_order_fully_refunded ($order_id);
735 +
696 736 // In any case, note that this order is ready for cancellation - we don't actually do this here anymore
697 737 $order->update_meta_data('_vipps_capture_complete',true);
698 738 $order->save();
699 739 }
@@ -701,9 +741,9 @@
701 741 // Called when orders reach the 'cancelled'-status. When this happens, orders will be *refunded*
702 742 // when they have been captured, but for added safety, this is only done when the orders are relatively new.
703 743 public function maybe_cancel_order($order_id) {
704 744 $order = wc_get_order($order_id);
705 - if ('vipps' != $order->get_payment_method()) return false;
745 + if (! Vipps::is_vipps_order($order)) return false;
706 746
707 747 try {
708 748 $order = $this->update_vipps_payment_details($order);
709 749 } catch (Exception $e) {
@@ -747,9 +787,9 @@
747 787 $order->save();
748 788 }
749 789
750 790 // IOK 2024-09-01 In general, we can refund most Vipps Mobilepay orders through the api,
751 - // 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.
752 792 public function can_refund_order( $order ) {
753 793 $method = $order->get_meta('_vipps_api');
754 794 switch ($method) {
755 795 case 'banktransfer':
@@ -757,9 +797,9 @@
757 797 break;
758 798 case 'epayment':
759 799 return true;
760 800 break;
761 - // Default is old-style ecom v2.
801 + // Default is true; but the above are exhaustive IOK 2026-08-18
762 802 default:
763 803 return true;
764 804 break;
765 805 }
@@ -769,9 +809,9 @@
769 809 // so that we can create a through-the-gateway refund for this if neccessary. That way, *our* logic for refunds occur
770 810 // instead of the normal woo logic. IOK 2026-04-16
771 811 public function wc_order_fully_refunded ($orderid) {
772 812 $order = wc_get_order($orderid);
773 - if ('vipps' != $order->get_payment_method()) return false;
813 + if (! Vipps::is_vipps_order($order)) return false;
774 814
775 815 // First check to see if we actually need to refund anything now IOK 2026-02-16
776 816 $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() );
777 817 if ( ! $max_refund ) {
@@ -800,9 +840,8 @@
800 840 wc_switch_to_site_locale();
801 841 $the_refund = wc_create_refund($data);
802 842 wc_restore_locale();
803 843 if (is_wp_error($the_refund)) {
804 - $refund_thru_gateway = false;
805 844 $msg = $the_refund->get_error_message();
806 845 $order->add_order_note(sprintf(__("Error when refunding payment through %1\$s:", 'woo-vipps'), $this->get_payment_method_name()) . ' ' . $msg);
807 846 $order->save();
808 847 $this->adminerr($msg);
@@ -827,8 +866,9 @@
827 866 $tax_data = wc_tax_enabled() ? $item->get_taxes() : false;
828 867 $remaining_tax = [];
829 868 if ($tax_data) {
830 869 foreach($tax_data['total'] as $tax_id => $value) {
870 + if ('' === $value) continue; // don't add empty string as tax value, fatal crash. LP 2026-06-08
831 871 $remaining_tax[$tax_id] = $value;
832 872 }
833 873 }
834 874 // We can then subtract the tax already refunded for each of these items.
@@ -843,13 +883,13 @@
843 883 }
844 884
845 885 // Then the quantity
846 886 $qty = (int) $item->get_quantity();
847 - $refunded_quantity = abs((int) $order->get_qty_refunded_for_item($item_id)); // Documented to be positive since 3.0, seems to be actually negative.
887 + $refunded_quantity = abs((int) $order->get_qty_refunded_for_item($item_id, $item->get_type())); // Documented to be positive since 3.0, seems to be actually negative.
848 888 $remaining_quantity = $qty-$refunded_quantity;
849 889
850 890 $total = $item->get_total();
851 - $refunded_total = $order->get_total_refunded_for_item($item_id); // A positive value
891 + $refunded_total = $order->get_total_refunded_for_item($item_id, $item->get_type()); // A positive value
852 892 $remaining_total = wc_format_decimal($total-$refunded_total);
853 893
854 894
855 895 if ($remaining_quantity>0 || !empty($remaining_tax) || $remaining_total > 0) {
@@ -869,9 +909,9 @@
869 909 // This is for orders that are 'reserved' at Vipps but could actually be captured at once because
870 910 // they don't require payment. So we try to capture. IOK 2020-09-22
871 911 // do NOT call this unless the order is 'reserved' at Vipps!
872 912 protected function maybe_complete_payment($order) {
873 - if ('vipps' != $order->get_payment_method()) return false;
913 + if (! Vipps::is_vipps_order($order)) return false;
874 914 if ($order->needs_processing()) return false; // No auto-capture for orders needing processing
875 915 // IOK 2018-10-03 when implementing partial capture, this must be modified.
876 916 $captured = intval($order->get_meta('_vipps_captured'));
877 917 $vippsstatus = $order->get_meta('_vipps_status');
@@ -896,9 +936,9 @@
896 936 public function woocommerce_create_refund ($refund, $args) {
897 937 $order_id = intval($args['order_id'] ?? 0);
898 938 $order = $order_id ? wc_get_order( $order_id ) : null;
899 939 if ( ! $order ) return;
900 - if ( $order->get_payment_method() !== 'vipps' ) return;
940 + if (! Vipps::is_vipps_order($order)) return;
901 941 // This is for manual refunds only IOK 2026-02-24
902 942 if (!($args['refund_payment'] ?? false)) {
903 943 try {
904 944 $order = $this->update_vipps_payment_details($order);
@@ -1005,9 +1045,8 @@
1005 1045 global $Vipps;
1006 1046
1007 1047 // Used for defaults in the admin interface; however this functions is called a loot more often than that.
1008 1048 $page_templates = $this->get_theme_page_templates();
1009 - $page_list = $this->get_pagelist();
1010 1049
1011 1050 $orderprefix = $Vipps->generate_order_prefix();
1012 1051
1013 1052 // Default handling based on other parameters and earlier values.
@@ -1018,16 +1057,15 @@
1018 1057 if (class_exists('VippsWooLogin')) {
1019 1058 $woodefault = 'yes' === get_option('woocommerce_enable_signup_and_login_from_checkout');
1020 1059 if ($woodefault) {
1021 1060 $expresscreateuserdefault = "yes";
1022 - // $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.
1023 1062 }
1024 1063 }
1025 1064
1026 - // 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
1027 1066 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1028 1067
1029 -
1030 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.
1031 1069 $current = get_option('woocommerce_vipps_settings');
1032 1070 // New defaults based on old defaults
1033 1071 $default_static_shipping_for_checkout = 'no';
@@ -1032,8 +1070,9 @@
1032 1070 // New defaults based on old defaults
1033 1071 $default_static_shipping_for_checkout = 'no';
1034 1072 $default_ask_address_for_express = 'no';
1035 1073 $default_status_on_fail = 'failed';
1074 + $default_express_show_in_checkout = 'yes';
1036 1075 if ($current) {
1037 1076 $default_static_shipping_for_checkout = (isset($current['enablestaticshipping'])) ? $current['enablestaticshipping'] : 'no';
1038 1077 $default_ask_address_for_express = (isset($current['useExplicitCheckoutFlow']) && $current['useExplicitCheckoutFlow'] == "yes") ? "yes" : "no";
1039 1078 // The old default used the same value as for Express Checkout. IOK 2023-07-27
@@ -1041,8 +1080,15 @@
1041 1080
1042 1081 // For existing installs: set failed payments order status to cancelled to keep same default behaviour.
1043 1082 // New installs will be set to failed instead of cancelled. LP 2026-03-26
1044 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 + }
1045 1091 }
1046 1092
1047 1093 // Get the already-set country code. For existing sites, this will guess the country based on the currency; for new sites, use
1048 1094 // the woo base country. IOK 2024-10-17 (previously used the currency here too).
@@ -1182,9 +1228,9 @@
1182 1228 'description' => __('Your phone number where Porterbuddy may send you important messages. Format must be MSISDN (including country code). Example: "4791234567"','woo-vipps'),
1183 1229 'default' => '',
1184 1230 ),
1185 1231
1186 - // 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
1187 1233 'vcs_helthjem' => array(
1188 1234 'title' => __('Helthjem', 'woo-vipps'),
1189 1235 'label' => sprintf(__('Support Helthjem as a shipping method in %1$s', 'woo-vipps'), Vipps::CheckoutName()),
1190 1236 'type' => 'checkbox',
@@ -1219,9 +1265,9 @@
1219 1265 ),
1220 1266
1221 1267 );
1222 1268
1223 - /* 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 */
1224 1270 $externals = [];
1225 1271 $external_payment_fields = [];
1226 1272 $allow_external_payments = $this->allow_external_payments_in_checkout();
1227 1273 if ($allow_external_payments) {
@@ -1414,14 +1460,14 @@
1414 1460 'default' => 'none',
1415 1461 ),
1416 1462 );
1417 1463
1418 - $expressfields = array(
1464 + $expressfields = array(
1419 1465 'express_options' => array(
1420 1466 'title' => sprintf(__('Express Checkout', 'woo-vipps')),
1421 1467 'type' => 'title',
1422 1468 'class' => 'tab',
1423 - '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())
1424 1470 ),
1425 1471
1426 1472 'cartexpress' => array(
1427 1473 'title' => __('Enable Express Checkout in cart', 'woo-vipps'),
@@ -1431,8 +1477,17 @@
1431 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()),
1432 1478 'default' => 'yes',
1433 1479 ),
1434 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 +
1435 1490 'singleproductexpress' => array(
1436 1491 'title' => __('Enable Express Checkout for single products', 'woo-vipps'),
1437 1492 'label' => __('Enable Express Checkout for single products', 'woo-vipps'),
1438 1493 'type' => 'select',
@@ -1535,22 +1590,24 @@
1535 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'),
1536 1591 ),
1537 1592
1538 1593 'vippsspecialpagetemplate' => array(
1539 - '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()),
1540 1595 'label' => sprintf(__('Use specific template for %1$s', 'woo-vipps'), Vipps::CompanyName()),
1541 1596 'type' => 'select',
1542 1597 'options' => $page_templates,
1543 - '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()),
1544 1599 'default' => ''),
1545 1600
1601 + // Deprecated, not shown anymore: TODO: remove this option in future. LP 2026-09-01
1546 1602 'vippsspecialpageid' => array(
1547 1603 'title' => sprintf(__('Use a real page ID for the special %1$s pages - neccessary for some themes', 'woo-vipps'), Vipps::CompanyName()),
1548 1604 'label' => __('Use a real page ID', 'woo-vipps'),
1549 1605 'type' => 'select',
1550 - 'options' => $page_list,
1606 + 'options' => [],
1551 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()),
1552 - 'default'=>''),
1608 + 'default' => ''
1609 + ),
1553 1610
1554 1611 'sendreceipts' => array(
1555 1612 'title' => __("Send receipts and order confirmation info to the customers' app on completed purchases.", 'woo-vipps'),
1556 1613 'label' => sprintf(__("Send receipts to the customers %1\$s app", 'woo-vipps'), Vipps::CompanyName()),
@@ -1714,9 +1771,9 @@
1714 1771 $ok = apply_filters('woo_vipps_is_available', $ok, $this);
1715 1772 return $ok;
1716 1773 }
1717 1774
1718 - // 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
1719 1776 // page for convenience. IOK 2021-10-01
1720 1777 public function vipps_checkout_available () {
1721 1778
1722 1779 if ($this->get_option('vipps_checkout_enabled') != 'yes') return false;
@@ -1787,9 +1844,8 @@
1787 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');
1788 1845 return [];
1789 1846 }
1790 1847
1791 -
1792 1848 // From the request, get either [billing_phone] => or [vipps phone]
1793 1849 $phone = '';
1794 1850 if (isset($_POST['vippsphone'])) {
1795 1851 $phone = trim(sanitize_text_field($_POST['vippsphone']));
@@ -1882,9 +1938,8 @@
1882 1938 $limited_session = $this->generate_authtoken();
1883 1939 $returnurl = add_query_arg('ls',$limited_session,$returnurl);
1884 1940 $returnurl = add_query_arg('id', $order_id, $returnurl);
1885 1941
1886 -
1887 1942 try {
1888 1943 // If the order was 'failed', it isnt any more! yet!
1889 1944 if ($order->get_status() == 'failed') {
1890 1945 $order->set_status('pending', __('Setting order status to pending to start payment', 'woo-vipps'));
@@ -1932,12 +1987,14 @@
1932 1987 $order->update_meta_data('_vipps_init_timestamp',$vippstamp);
1933 1988 $order->update_meta_data('_vipps_orderurl', $url);
1934 1989
1935 1990 $order->update_meta_data('_vipps_status','INITIATE'); // INITIATE right now
1936 - $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), $this->get_payment_method_name()));
1937 - $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 +
1938 1996 $order->save();
1939 -
1940 1997 // Create a signal file that we can check without calling wordpress to see if our result is in IOK 2018-05-04
1941 1998 try {
1942 1999 $Vipps->createCallbackSignal($order);
1943 2000 } catch (Exception $e) {
@@ -1942,9 +1999,8 @@
1942 1999 $Vipps->createCallbackSignal($order);
1943 2000 } catch (Exception $e) {
1944 2001 // Could not create a signal file, but that's ok.
1945 2002 }
1946 -
1947 2003 do_action('woo_vipps_before_redirect_to_vipps',$order_id);
1948 2004
1949 2005 // This will send us to a receipt page where we will do the actual work. IOK 2018-04-20
1950 2006 return array('result'=>'success','redirect'=>$url);
@@ -1953,9 +2009,9 @@
1953 2009
1954 2010 // This tries to capture a Vipps payment, and resets the status to 'on-hold' if it fails. IOK 2018-05-07
1955 2011 public function maybe_capture_payment($orderid) {
1956 2012 $order = wc_get_order($orderid);
1957 - if ('vipps' != $order->get_payment_method()) return false;
2013 + if (! Vipps::is_vipps_order($order)) return false;
1958 2014 $ok = 0;
1959 2015
1960 2016 # Shortcut orders that have been directly captured
1961 2017 $vippsstatus = $order->get_meta('_vipps_status');
@@ -2016,9 +2072,9 @@
2016 2072 // Capture (possibly partially) the order. Only full capture really supported by plugin at this point. IOK 2018-05-07
2017 2073 // Except that we *do* note that money "refunded" through vipps before capture should be "uncapturable". IOK 2024-11-25
2018 2074 public function capture_payment($order) {
2019 2075 $pm = $order->get_payment_method();
2020 - if ($pm != 'vipps') {
2076 + if (! Vipps::is_vipps_order($pm)) {
2021 2077 $this->log(sprintf(__('Trying to capture payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()). ' ' . $order->get_id(), 'error');
2022 2078 $this->adminerr(sprintf(__('Cannot capture payment on orders not made by %1$s','woo-vipps'), $this->get_payment_method_name()));
2023 2079 return false;
2024 2080 }
@@ -2073,12 +2129,11 @@
2073 2129 if ($api == 'banktransfer') {
2074 2130 // This is an error - we should not ever get to the 'capture' branch if we are a banktransfer payment.
2075 2131 // IOK 2024-01-09
2076 2132 $content = [];
2077 - } elseif ($api == 'epayment') {
2133 + } else {
2134 + // Now the only other api is 'epayment' IOK 2026-08-18
2078 2135 $content = $this->api->epayment_capture_payment($order,$amount,$requestid);
2079 - } else {
2080 - $content = $this->api->capture_payment($order,$amount,$requestid);
2081 2136 }
2082 2137 } catch (TemporaryVippsApiException $e) {
2083 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');
2084 2139 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . "\n" . $e->getMessage());
@@ -2122,9 +2177,9 @@
2122 2177
2123 2178 // Cancel (only completely) a reserved but not yet captured order IOK 2018-05-07
2124 2179 public function cancel_payment($order) {
2125 2180 $pm = $order->get_payment_method();
2126 - if ($pm != 'vipps') {
2181 + if (! Vipps::is_vipps_order($pm)) {
2127 2182 $this->log(sprintf(__('Trying to cancel payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()). ' ' .$order->get_id(), 'error');
2128 2183 $this->adminerr(sprintf(__('Cannot cancel payment on orders not made by %1$s','woo-vipps'), $this->get_payment_method_name()));
2129 2184 return false;
2130 2185 }
@@ -2129,8 +2184,9 @@
2129 2184 return false;
2130 2185 }
2131 2186 // We'll use the same transaction id for all cancel jobs, as we can only do it completely. IOK 2018-05-07
2132 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.
2133 2189 $api = $order->get_meta('_vipps_api');
2134 2190 try {
2135 2191 $requestid = "";
2136 2192 if ($api == 'banktransfer') {
@@ -2135,22 +2191,14 @@
2135 2191 $requestid = "";
2136 2192 if ($api == 'banktransfer') {
2137 2193 // If we are here, and the order is somehow not captured, just do nothing. IOK 2024-01-09
2138 2194 $content = [];
2139 - } elseif ($api == 'epayment') {
2195 + } else {
2196 + // api is here 'epayment'. IOK 2026-07-18
2140 2197 $requestid = 1;
2141 2198 // This will cancel any remaining, not-captured amount IOK 2026-01-28
2142 2199 $content = $this->api->epayment_cancel_payment($order,$requestid);
2143 - } else {
2144 - // If we have captured the order, we can't cancel it with the ecom API IOK 2018-05-07
2145 - $captured = intval($order->get_meta('_vipps_captured'));
2146 - if ($captured>0) {
2147 - $msg = sprintf(__('Cannot cancel a captured %1$s transaction - use refund instead', 'woo-vipps'), "ECOM " . $this->get_payment_method_name());
2148 - $this->adminerr($msg);
2149 - return false;
2150 - }
2151 - $content = $this->api->cancel_payment($order,$requestid);
2152 - }
2200 + }
2153 2201 } catch (TemporaryVippsApiException $e) {
2154 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');
2155 2203 $this->adminerr(sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()) . ' ' . $e->getMessage());
2156 2204 return false;
@@ -2164,9 +2212,8 @@
2164 2212 // the epay v2 API would return transactionInfo and Summary with the result, the new epayment api returns nothing.
2165 2213 // Removed epay branch 2025-08-12 IOK
2166 2214 $total = intval($order->get_meta('_vipps_amount'));
2167 2215 $captured = intval($order->get_meta('_vipps_captured'));
2168 -# $cancelled = $amount + intval($order->get_meta('_vipps_cancelled');
2169 2216 $cancelled = $total;
2170 2217 $remaining = $total - $captured - $cancelled;
2171 2218
2172 2219 // We need to assume it worked. Also, we can't do partial cancels yet, so just cancel everything.
@@ -2189,9 +2236,9 @@
2189 2236 // Refund (possibly partially) the captured order. IOK 2018-05-07
2190 2237 // The caller must handle the errors.
2191 2238 public function refund_payment($order,$amount=0,$cents=false) {
2192 2239 $pm = $order->get_payment_method();
2193 - if ($pm != 'vipps') {
2240 + if (! Vipps::is_vipps_order($pm)) {
2194 2241 $msg = sprintf(__('Trying to refund payment on order not made by %1$s:','woo-vipps'), $this->get_payment_method_name()) . ' ' . $order->get_id();
2195 2242 $this->log($msg,'error');
2196 2243 throw new VippsAPIException($msg);
2197 2244 }
@@ -2214,13 +2261,12 @@
2214 2261 if ($api == 'banktransfer') {
2215 2262 $msg = sprintf(__("Cannot refund bank transfer order %1\$d", 'woo-vipps'), $order->get_id());
2216 2263 $this->log($msg, 'error');
2217 2264 throw new Exception($msg);
2218 - } elseif ($api == 'epayment') {
2265 + } else {
2266 + // api is now 'epayment' IOK 2026-08-18
2219 2267 $content = $this->api->epayment_refund_payment($order,$requestid,$amount,$cents);
2220 - } else {
2221 - $content = $this->api->refund_payment($order,$requestid,$amount,$cents);
2222 - }
2268 + }
2223 2269
2224 2270 $currency = $order->get_currency();
2225 2271
2226 2272 // Previously, we got updated transaction info in a transactionInfo field. this is no longer provided,
@@ -2333,16 +2379,17 @@
2333 2379
2334 2380 // New 2026-01-05: we now check all other payment methods that aren't vipps, and reset it back to vipps.
2335 2381 // The issue was using Klarna Payments and pressing 'back' in the browser, then completing the payment in vipps checkout
2336 2382 // the order still had the payment method klarna_payments, since we previously only checked 'kco' = klarna/kustom checkout. LP 2026-01-05
2337 - if ($order->get_payment_method() != "vipps" && $order->get_meta("_vipps_orderid")) {
2383 + if (! Vipps::is_vipps_order($order) && $order->get_meta("_vipps_orderid")) {
2338 2384 $order->set_payment_method('vipps');
2339 -
2340 2385 $express = $order->get_meta('_vipps_express_checkout');
2341 2386 $checkout = $order->get_meta('_vipps_checkout');
2342 2387 $order->set_payment_method_title('Vipps');
2343 2388 if ($express) $order->set_payment_method_title('Vipps Express Checkout');
2344 2389 if ($checkout) $order->set_payment_method_title('Vipps Checkout');
2390 + // paypal gw resets payment gateway on order save because it has this meta, so delete it before save. LP 2026-06-23
2391 + $order->delete_meta_data('_ppcp_paypal_order_id');
2345 2392 $order->save();
2346 2393
2347 2394 $msg = sprintf(__("Payment method reset to %1\$s - it had been set to another payment method while completing the order for %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id());
2348 2395 $this->log($msg, 'debug');
@@ -2403,10 +2450,8 @@
2403 2450 $order->update_meta_data('_vipps_status',$newvippsstatus);
2404 2451
2405 2452 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
2406 2453 if (!empty($paymentdetails)) {
2407 -
2408 -
2409 2454 // checkout has a string, epayment has an array with upper case "type" and apparently, cardBin IOK 2025-08-12
2410 2455 $paymentMethod = $paymentdetails['paymentMethod'] ?? "epayment";
2411 2456 // After normalization, all APIs will have data here.
2412 2457 $details = $paymentdetails['paymentDetails'];
@@ -2619,73 +2664,11 @@
2619 2664 if (in_array($newstatus, ['authorized', 'complete'])) {
2620 2665 $ready = true;
2621 2666 }
2622 2667
2623 -
2624 - // 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.
2625 - // 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
2626 - // 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
2627 - // Also we don't get this when the state is different from AUTHORIZED. Especially not ABORTED.
2628 - // IOK 2025-09-29: This is *no longer the case* . We actually now get userDetails every time we add the relevant scopes,
2629 - // so this is now probably dead code.
2630 - if ($ready && $express && !$checkout_session && !isset($result['userDetails'])) {
2631 -
2632 - $sub = isset($result['profile']) && isset($result['profile']['sub']) ? $result['profile']['sub'] : null;
2633 - $userinfo = [];
2634 - if (!$sub) {
2635 - // This should never happen, but be prepared
2636 - $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" );
2637 - $order->add_order_note($message);
2638 - $this->log($message , "error");
2639 - } else {
2640 - // If this happens, the merchant *may* be able to retrieve the information from Vipps so add a note for it.
2641 - try {
2642 - $userinfo = $this->api->get_userinfo($sub);
2643 - } catch (Exception $e) {
2644 - $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());
2645 - $order->add_order_note($message);
2646 - $this->log($message, 'woo-vipps', "error");
2647 - }
2648 - }
2649 - if ($userinfo) {
2650 - $userDetails = array(
2651 - 'email_verified' => $userinfo['email_verified'],
2652 - 'email' => $userinfo['email'],
2653 - 'firstName' => $userinfo['given_name'] ?? '',
2654 - 'lastName' => $userinfo['family_name'] ?? '',
2655 - 'mobileNumber' => $userinfo['phone_number'] ?? '',
2656 - 'phoneNumber' => $userinfo['phone_number'] ?? '',
2657 - 'userId' => $userinfo['phone_number'] ?? '',
2658 - 'sub' => $userinfo['sub']
2659 - );
2660 -
2661 - $result['userDetails'] = $userDetails;
2662 -
2663 - // We may have asked for the address of the customer, so add that too, or a dummy.
2664 - if (!isset($result['shippingDetails'])) {
2665 - $countries=new WC_Countries();
2666 - $address =[];
2667 - $address['addressLine1'] = "";
2668 - $address['addressLine2'] = "";
2669 - $address['city'] ="";
2670 - $address['postCode'] = "";
2671 - $address['country'] = $countries->get_base_country();
2672 -
2673 - // This uses other keys than both epayment and checkout, but we'll normalize it later. IOK 2025-08-13
2674 - if (isset($userinfo['address'])) {
2675 - $address['addressLine1'] = $userinfo['address']['street_address'];
2676 - $address['city'] = $userinfo['address']['region'];
2677 - $address['country'] = $userinfo['address']['country'];
2678 - $address['postCode'] = $userinfo['address']['postal_code'];
2679 - }
2680 - $result['shippingDetails'] = ['address' => $address];
2681 - }
2682 - }
2683 - }
2684 -
2685 2668 if ($ready && ($express || $checkout_session)) {
2686 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
2687 - // 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
2688 2671 $result = $this->ensure_userDetails($result, $order);
2689 2672
2690 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
2691 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
@@ -2757,9 +2740,9 @@
2757 2740 return $result;
2758 2741 }
2759 2742
2760 2743
2761 - // 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,
2762 2745 // we are (currently) not receiving any 'state' or 'aggregate', so add this iff the payment is successful.
2763 2746 // The reason for this is that this payment type does not actually use the epayment API at all (!)
2764 2747 // Also moved some other compatibility code here -
2765 2748 // --- reference used to be orderId
@@ -2828,9 +2811,9 @@
2828 2811
2829 2812 return $result;
2830 2813 }
2831 2814
2832 - // 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
2833 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.
2834 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
2835 2818 // if not, we use shippingDetails. IOK 2023-01-10
2836 2819 // Also, epayment uses mobileNumber and checkout uses phoneNumber, so normalize.
@@ -2840,8 +2823,9 @@
2840 2823 // If we have userDetails, use it (ecom API with user data requested - Express Checkout
2841 2824 if (isset($vippsdata['userDetails'])) {
2842 2825 $userDetails = $vippsdata['userDetails'];
2843 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
2844 2828 $sub = "";
2845 2829 if (isset($vippsdata['profile']) && isset($vippsdata['profile']['sub'])) {
2846 2830 $sub = $vippsdata['profile']['sub'];
2847 2831 }
@@ -3206,9 +3190,13 @@
3206 3190 $is_base64 = $shipping_table ? ( $shipping_table['_is_base64'] ?? false) : false;
3207 3191
3208 3192 if (is_array($shipping_table) && isset($shipping_table[$key])) {
3209 3193 $decoded = $is_base64 ? @base64_decode($shipping_table[$key]) : $shipping_table[$key];
3210 - $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 + $shipping_rate = $decoded ? @unserialize($decoded, ['allowed_classes' => [WC_Shipping_Rate::class]]) : null;
3197 + $shipping_rate = is_a($shipping_rate,'WC_Shipping_Rate') ? $shipping_rate : null;
3198 +
3211 3199 if (!$shipping_rate) {
3212 3200 $this->log(sprintf(__("%1\$s: Could not deserialize the chosen shipping method %2\$s for order %3\$d", 'woo-vipps'), Vipps::ExpressCheckoutName(), $method, $order->get_id()), 'error');
3213 3201 $this->log(sprintf(__("Serialized data was %1\$s", 'woo-vipps'), $decoded), 'error');
3214 3202 } else {
@@ -3228,9 +3216,9 @@
3228 3216 }
3229 3217 }
3230 3218 }
3231 3219
3232 - // Possible extra metadata from Vipps Checkout IOK 2023-01-17
3220 + // Possible extra metadata from Checkout IOK 2023-01-17
3233 3221 // Store in the order, but also in the shipping rate so it will be visible in the order screen
3234 3222 // along with the shipping ragte
3235 3223 if (isset($shipping['pickupPoint'])) {
3236 3224 $order->update_meta_data('vipps_checkout_pickupPoint', $shipping['pickupPoint']);
@@ -3290,9 +3278,9 @@
3290 3278 $methodclass = $methods_classes[$shipping_rate->get_method_id()] ?? null;
3291 3279 $shipping_method = $methodclass ? new $methodclass($shipping_rate->get_instance_id()) : null;
3292 3280 $is_vipps_checkout_shipping = $shipping_method && is_a($shipping_method, 'VippsCheckout_Shipping_Method');
3293 3281
3294 - // Some Vipps Checkout-specific shipping methods calculate the cost in the Vipps window.
3282 + // Some Checkout-specific shipping methods calculate the cost in the Vipps window.
3295 3283 if ($is_vipps_checkout_shipping && $shipping_method->dynamic_cost) {
3296 3284 $vippsamount = intval($order->get_meta('_vipps_amount'));
3297 3285 $shipping_tax_rate = floatval($order->get_meta('_vipps_shipping_tax_rates'));
3298 3286 $compareamount = $ordertotal * 100;
@@ -3328,9 +3316,9 @@
3328 3316
3329 3317 $order->set_total($ordertotal + $total_shipping + $total_shipping_tax);
3330 3318 $order->update_taxes(); // Necessary for the admin view only; does not recalculate order.
3331 3319
3332 - // Add an early hook for Vipps Checkout orders with special shipping methods
3320 + // Add an early hook for Checkout orders with special shipping methods
3333 3321 $metadata = $shipping_rate->get_meta_data();
3334 3322 if (isset($metadata['type'])) {
3335 3323 do_action('woo_vipps_checkout_special_shipping_method', $order, $shipping_rate, $metadata['type']);
3336 3324 }
@@ -3346,9 +3334,9 @@
3346 3334
3347 3335
3348 3336 // If we have the 'expresscreateuser' thing set to true, we will create or assign the order here, as it is the first-ish place where we can.
3349 3337 // If possible and safe, user will be logged in before being sent to the thankyou screen. IOK 2020-10-09
3350 - // Same thing for Vipps Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3338 + // Same thing for Checkout, mutatis mutandis. The function below returns false if no customer exists or gets created.
3351 3339 $customer = false;
3352 3340 if ($assigncustomer) {
3353 3341 $customer = Vipps::instance()->express_checkout_get_vipps_customer($order);
3354 3342 }
@@ -3356,8 +3344,9 @@
3356 3344 // This would have been used to ensure that we 'enroll' the users the same way as in the Login plugin. Unfortunately, the userId from express checkout isn't
3357 3345 // the same as the 'sub' we get in Login so that must be a future feature. IOK 2020-10-09
3358 3346 // IOK 2025-08-13 we do get the 'sub' now, at least for express checkout. For Checkout, we would have to compare the email of the user with the verified email
3359 3347 // after calling get_userinfo, so we'll leave that be.
3348 + // We *maybe* get the sub - it depends on consent, and *maybe* that a scope has been added in epayment_initate_payment. IOK 2026-08-18
3360 3349 if (class_exists('VippsWooLogin') && $customer && !is_wp_error($customer) && !get_user_meta($customer->get_id(), '_vipps_phone',true)) {
3361 3350 update_user_meta($customer->get_id(), '_vipps_phone', $billing['phoneNumber']);
3362 3351 if (isset($user['sub'])) {
3363 3352 $userid = $customer->get_id();
@@ -3417,9 +3406,9 @@
3417 3406 $order->update_meta_data('_vipps_api', 'banktransfer');
3418 3407 }
3419 3408 }
3420 3409
3421 - // Handle the callback from Vipps eCom.
3410 + // Handle the callback from Vipps ePayment
3422 3411 public function handle_callback($result, $order, $ischeckout=false, $iswebhook=false) {
3423 3412 global $Vipps;
3424 3413
3425 3414 $vippsorderid = $result['orderId'];
@@ -3427,55 +3416,121 @@
3427 3416
3428 3417 $keyset = $this->get_keyset();
3429 3418 $me = array_keys($keyset);
3430 3419
3420 + // Validate the callback first
3431 3421 if (!in_array($merchant, $me)) {
3432 3422 $this->log(sprintf(__("%1\$s callback with wrong merchantSerialNumber - might be forged",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3433 3423 return false;
3434 3424 }
3435 -
3436 3425 if (!$order) {
3437 3426 $this->log(sprintf(__("%1\$s callback for unknown order",'woo-vipps'), $this->get_payment_method_name()) . " " . $order->get_id(), 'warning');
3438 3427 return false;
3439 3428 }
3440 - $orderid = $order->get_id();
3441 - // We may need to use poll to get data, depending on the content passed.
3442 - $express = $order->get_meta('_vipps_express_checkout');
3443 - $checkout_session = $order->get_meta('_vipps_checkout_session');
3444 -
3429 + $order_id = $order->get_id();
3445 3430 if ($vippsorderid != $order->get_meta('_vipps_orderid')) {
3446 - $this->log(sprintf(__("Wrong %1\$s Orderid - possibly an attempt to fake a callback ", 'woo-vipps'), Vipps::CompanyName()), 'warning');
3447 - clean_post_cache($order->get_id());
3431 + $this->log(sprintf(__('Wrong %1$s Orderid - possibly an attempt to fake a callback ', 'woo-vipps'), Vipps::CompanyName()), 'warning');
3432 + clean_post_cache($order_id);
3448 3433 exit();
3449 3434 }
3450 3435
3436 + // Note any errors in the callback early
3451 3437 $errorInfo = $result['errorInfo'] ?? '';
3452 3438 if ($errorInfo) {
3453 - $this->log(sprintf(__("Message in callback from %1\$s for order",'woo-vipps'), $this->get_payment_method_name()) . ' ' . $orderid . ' ' . $errorInfo['errorMessage'],'error');
3454 - $order->add_order_note(sprintf(__("Message from %1\$s: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3439 + /* translators: payment method name, order id */
3440 + $this->log(sprintf(__('Message in callback from %1$s for order %2$s: ','woo-vipps'), $this->get_payment_method_name(), $order_id), $errorInfo['errorMessage'], 'error');
3441 + /* translators: payment method name, message */
3442 + $order->add_order_note(sprintf(__('Message from %1$s: %2$s','woo-vipps'), $this->get_payment_method_name(), $errorInfo['errorMessage']));
3455 3443 }
3456 3444
3445 + // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3446 + try {
3447 + $Vipps->createCallbackSignal($order,'ok');
3448 + } catch (Exception $e) {
3449 + // Could not create a signal file, but that's ok.
3450 + }
3451 +
3452 + // New callback handling: schedule an Action Scheduler job to process it. The purpose of processing callback
3453 + // is to set finalize the order (set order status, set shipping for express) in the case when customer does
3454 + // not return to the store, because then poll does not run. LP 2026-03-27
3455 +
3456 + // Below is separate from '_vipps_callback_timestamp' which is when callback is sent,
3457 + // but also that meta won't be stored until callback is actually processed. LP 2026-03-30
3458 + $order->update_meta_data('_vipps_callback_received_at', time());
3459 + // Store the callback data in the order. We'll do a cleanup of this when the scheduled job runs. IOK 2026-04-21
3460 + $order->update_meta_data('_vipps_callback_data', $result);
3461 + $order->save_meta_data();
3462 +
3463 + // Run callback actions for callback received as soon as it is actually received.
3464 + $transaction = []; // No longer provided. IOK 2026-05-06
3465 + do_action('woo_vipps_callback_received', $order, $result, $transaction);
3466 +
3467 + $action_args = [
3468 + 'order_id' => $order->get_id(),
3469 + 'is_checkout' => $ischeckout,
3470 + 'is_webhook' => $iswebhook,
3471 + ];
3472 + // We'll check the status of this order in a minute. At that time, the customer should have been able to return to the store
3473 + // and have the order finalized the 'normal' way, but if they don't, we'll handle it async. 2026-04-21
3474 + $scheduled_at = time() + 60;
3475 + $action_id = as_schedule_single_action($scheduled_at, 'woo_vipps_action_process_callback', $action_args, 'woo-vipps', false);
3476 + if ($action_id) {
3477 + /* translators: order id, scheduled time */
3478 + $this->log(sprintf(__('Callback action scheduled at %2$s for order %1$s', 'woo-vipps'), $order->get_id(), $scheduled_at), 'info');
3479 + } else {
3480 + // The action scheduler error is not returned, only sent to error_log (https://github.com/woocommerce/action-scheduler/blob/25c982c3d0f8134389d5b5884082403c4806322f/classes/ActionScheduler_ActionFactory.php#L268). LP 2026-04-23
3481 + /* translators: order id */
3482 + $this->log(sprintf(__('Failed to schedule callback process action for order %1$s, check the php error log', 'woo-vipps'), $order->get_id()), 'error');
3483 + /* We will not delete the callback data here, to facilitate debugging. But return false to indicate that callback handling will fail. */
3484 + /* NB: The order will still be processed with the periodic job, at a later stage. IOK 2026-05-06 */
3485 + return false;
3486 + }
3487 +
3488 + // Signal that we in fact handled the order.
3489 + return true;
3490 + }
3491 +
3492 + /** Runs in action scheduler: sync woo status from Vipps callback data. Handle shipping etc. for Express. LP 2026-03-31 */
3493 + public function action_process_callback($order_id, $is_checkout, $is_webhook) {
3494 + $order = wc_get_order($order_id);
3495 + if (!is_a($order, 'WC_Order')) {
3496 + /* translators: order id */
3497 + $this->log(sprintf(__('Callback process action failed, could not find order %1$s','woo-vipps'), $order_id), 'error');
3498 + return false;
3499 + }
3500 +
3501 + $oldstatus = $order->get_status();
3502 + if ($oldstatus != 'pending') {
3503 + // Actually, we are ok with this order, abort the callback handler. IOK 2018-05-30
3504 + $order->delete_meta_data('_vipps_callback_data');
3505 + clean_post_cache($order->get_id());
3506 + return false;
3507 + }
3508 + $data = $order->get_meta('_vipps_callback_data');
3509 +
3510 + /* translators: order id */
3511 + $this->log(sprintf(__('Callback process action running for order %1$s.', 'woo-vipps'), $order_id));
3512 +
3457 3513 // The payment details field is passed in Checkout, not in Express, but none of them are complete, so we fill out the values
3458 3514 // depending on which one we are IOK 2025-08-13
3459 3515 $details = [];
3460 3516 // Checkout has this as a field, containing *some* of the neccessary data
3461 - if (isset($result['paymentDetails'])) {
3517 + if (isset($data['paymentDetails'])) {
3462 3518 // Checkout. The sesssion states are # "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
3463 3519 // -- we should only get callbacks for successful sessions actually.
3464 - $details = $result['paymentDetails'];
3465 - $result['state'] = $result['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($result['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3466 - $details['state'] = $result['state'];
3467 - $details['paymentMethod'] = $result['paymentMethod'];
3520 +
3521 + $details = $data['paymentDetails'];
3522 + $data['state'] = $data['sessionState'] == 'PaymentSuccessful' ? 'AUTHORIZED' : ($data['sessionState'] == 'PaymentTerminated' ? 'TERMINATED' : 'CREATED');
3523 + $details['state'] = $data['state'];
3524 + $details['paymentMethod'] = $data['paymentMethod'];
3468 3525 } else {
3469 - // This should be an ecom callback; which we need to add a lot of data for to get a valid "paymentDetails".
3526 + // This should be an epayment webhook callback; which we need to add a lot of data for to get a valid "paymentDetails".
3470 3527 $details = [];
3471 - $result['state'] = $result['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3472 - $details['state'] = $result['name'];
3473 - $details['amount'] = $result['amount']; // currency, value
3528 + $data['state'] = $data['name']; // The name of the callback - which should be AUTHORIZED, TERMINATED etc
3529 + $details['state'] = $data['name'];
3530 + $details['amount'] = $data['amount']; // currency, value
3474 3531 $details['paymentMethod'] = 'epayment';
3475 - $currency = $details['amount']['currency'];
3476 - $nothing = [ 'currency' => $currency, 'value' => 0];
3477 - }
3532 + }
3478 3533
3479 3534 // For both callbacks, set 'aggregate'
3480 3535 $currency = $details['amount']['currency'];
3481 3536 $nothing = [ 'currency' => $currency, 'value' => 0];
@@ -3480,68 +3535,52 @@
3480 3535 $currency = $details['amount']['currency'];
3481 3536 $nothing = [ 'currency' => $currency, 'value' => 0];
3482 3537 $aggregate = ['authorizedAmount' => $nothing, 'cancelledAmount' => $nothing, 'capturedAmount' => $nothing, 'refundedAmount' => $nothing];
3483 3538 if ($details['state'] == 'AUTHORIZED') {
3484 - $aggregate['authorizedAmount'] = $details['amount'];
3539 + $aggregate['authorizedAmount'] = $details['amount'];
3485 3540 }
3486 3541 $details['aggregate'] = $aggregate;
3487 - $result['paymentDetails'] = $details;
3542 + $data['paymentDetails'] = $details;
3488 3543
3489 - $result = $this->normalizePaymentDetails($result);
3490 - $details = $result['paymentDetails'];
3544 + // Do the actual work with a function shared with the periodic job.
3545 + $this->set_order_status_by_payment_details($order,$data);
3491 3546
3492 - $vippsstatus = $result['status']; // Will exist now, because of the normalization IOK 2025-08-13
3547 + /* translators: payment method name */
3548 + $order->add_order_note(sprintf(__('%1$s callback processed','woo-vipps'), $this->get_payment_method_name()));
3549 +
3550 + // We're done, so delete the callback data. IOK 2026-04-22
3551 + $order->delete_meta_data('_vipps_callback_data');
3552 + }
3553 +
3554 +
3555 + // Called either by periodic job or by action_process_callback with the callback data *or* with payment details fetched with poll.
3556 + // sets order status if neccessary, and will finalize the order for Express via HTTP call if necessary. IOK 2026-05-06
3557 + public function set_order_status_by_payment_details($order, $data, $allow_retry=true) {
3558 + $data = $this->normalizePaymentDetails($data);
3559 + $details = $data['paymentDetails'];
3560 + $order_id = $order->get_id();
3561 +
3562 + $vippsstatus = $data['status']; // Will exist now, because of the normalization IOK 2025-08-13
3493 3563 $newstatus = $this->interpret_vipps_order_status($vippsstatus);
3494 3564
3495 3565 // Extract order metadata from either Checkout or Epayment - set below IOK 2025-08-13
3496 3566 $transaction = array();
3497 - $stamp = ($result['timestamp'] ?? false) ? strtotime($result['timestamp']) : time();
3567 + $stamp = ($data['timestamp'] ?? false) ? strtotime($data['timestamp']) : time();
3498 3568 $transaction['timeStamp'] = date('Y-m-d H:i:s', $stamp);
3499 3569 $transaction['amount'] = $details['amount']['value'];
3500 3570 $transaction['currency'] = $details['amount']['currency'];
3501 - $transaction['status'] = ($result['state'] ?? $details['state']);
3571 + $transaction['status'] = ($data['state'] ?? $details['state']);
3502 3572 $transaction['paymentmethod'] = $details['paymentMethod'] ?? "";
3573 + $this->order_set_transaction_metadata($order, $transaction);
3503 3574
3504 - if (!$transaction) {
3505 - $this->log(sprintf(__("Anomalous callback from %1\$s, handle errors and clean up",'woo-vipps'), $this->get_payment_method_name()),'warning');
3506 - clean_post_cache($order->get_id());
3507 - return false;
3575 + // Dont do anything if order is not finalized. LP 2026-08-31
3576 + if (!in_array($newstatus, ['authorized', 'complete', 'cancelled'])) {
3577 + return;
3508 3578 }
3509 3579
3510 - $order->add_order_note(sprintf(__('%1$s callback received','woo-vipps'), $this->get_payment_method_name()));
3511 - do_action('woo_vipps_callback_received', $order, $result, $transaction);
3580 + // This order is ready to set order shipping details etc for IOK 2025-09-19
3581 + $ready = in_array($newstatus, ['authorized', 'complete']);
3512 3582
3513 - $oldstatus = $order->get_status();
3514 - if ($oldstatus != 'pending') {
3515 - // Actually, we are ok with this order, abort the callback. IOK 2018-05-30
3516 - clean_post_cache($order->get_id());
3517 - return false;
3518 - }
3519 -
3520 - // 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.
3521 - // 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
3522 - // when callbacks happen while we are polling for results. IOK 2018-05-30
3523 - if (!$Vipps->lockOrder($order)) {
3524 - clean_post_cache($order->get_id());
3525 - return false;
3526 - }
3527 -
3528 - // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3529 - // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3530 - // made to the session here will be saved. Sorry.
3531 - $Vipps->callback_restore_session($orderid);
3532 -
3533 - // Set Vipps metadata as early as possible
3534 - $this->order_set_transaction_metadata($order, $transaction);
3535 -
3536 - $this->log(sprintf(__("%1\$s callback: Handling order: ", 'woo-vipps'), Vipps::CompanyName()) . " " . $orderid, 'debug');
3537 -
3538 -
3539 - // This order is ready to set order shipping details etc for IOK 2025-09-19
3540 - $ready = false;
3541 - if (in_array($newstatus, ['authorized', 'complete'])) {
3542 - $ready = true;
3543 - }
3544 3583 if ($ready) {
3545 3584 // Failsafe for rare bug when using Klarna Checkout with Vipps as an external payment method
3546 3585 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3547 3586 $this->reset_erroneous_payment_method($order);
@@ -3546,40 +3585,38 @@
3546 3585 // IOK 2024-01-09 ensure this is called only when order is complete/authorized
3547 3586 $this->reset_erroneous_payment_method($order);
3548 3587 }
3549 3588
3550 - if ($ready && ($express || $ischeckout)) {
3551 - // For Vipps Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3552 - // This will also normalize userDetails, adding 'sub' where required and fields for backwards compatibility. 2025-08-12
3553 - $result = $this->ensure_userDetails($result, $order);
3589 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3554 3590
3555 - // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3556 - // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3557 - // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3558 - // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3559 - // we have added a scope of any kind. This is therefore probably dead code.
3560 - // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3561 - if (!isset($result['userDetails'])) {
3562 - // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3563 - try {
3564 - $details = $this->get_payment_details($order);
3565 - $result = $details;
3566 - } catch (Exception $e) {
3567 - $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3568 - $this->log($e->getMessage());
3569 - }
3570 - }
3571 -
3572 - // 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
3573 - $result = $this->normalizeShippingDetails($result, $order);
3574 -
3575 - // We should now always have shipping details.
3576 - if (isset($result['shippingDetails'])) {
3577 - $billing = isset($result['billingDetails']) ? $result['billingDetails'] : false;
3578 - $this->set_order_shipping_details($order,$result['shippingDetails'], $result['userDetails'], $billing, $result);
3591 + // Handle session and shipping through http, because we dont want to mess with session here in wp cron (action scheduler). LP 2026-03-30
3592 + // NB: This is and must be a *synchronous call*. When done, the order will have shipping, addresses etc. IOK 2026-05-06.
3593 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3594 + if ($ready && $is_express_or_checkout && !$shipping_set) {
3595 + $token = $order->get_meta('_vipps_authtoken');
3596 + $args = [
3597 + 'body' => [
3598 + 'order_id' => $order_id,
3599 + 'vipps_order_data' => $data,
3600 + ],
3601 + 'headers' => [
3602 + 'X-WooVipps-Token' => $token,
3603 + ],
3604 + ];
3605 + $url = Vipps::get_rest_url('v1', '/order-set-shipping');
3606 + $response = wp_remote_post($url, $args);
3607 + if (is_wp_error($response)) {
3608 + /* translators: order id, error message */
3609 + $error_msg = $response->get_error_message();
3610 + $this->log(sprintf(__('Process callback action failed to finalize shipping through http rest endpoint for order %1$s: %2$s', 'woo-vipps'), $order->get_id(), $error_msg), 'error');
3611 + } else if (200 != ($response['response']['code'] ?? -1)) {
3612 + /* translators: order id */
3613 + $response_msg = print_r($response['body'] ?? ['Missing response body'], true);
3614 + $this->log(sprintf(__('Process callback action failed to finalize shipping through http rest endpoint for order %1$s: %2$s', 'woo-vipps'), $order->get_id(), $response_msg), 'error');
3579 3615 }
3580 3616 }
3581 3617
3618 + // This must happen *after* finalization for Express, as above. IOK 2026-05-06
3582 3619 // the only status we now care about is AUTHORIZED. Previously we had AUTHORISED and RESERVED and RESERVE as well. And SALE.
3583 3620 if ($vippsstatus == 'AUTHORIZED') {
3584 3621 $this->payment_complete($order);
3585 3622 } else if ($vippsstatus == 'SALE') {
@@ -3584,14 +3621,15 @@
3584 3621 $this->payment_complete($order);
3585 3622 } else if ($vippsstatus == 'SALE') {
3586 3623 // Direct capture needs special handling because most of the meta values we use are missing IOK 2019-02-26
3587 3624 // Actually not supported anymore, but keep logic. IOK 2025-08-13
3625 + // Still supported for finnish direct bank transfer. IOK 2026-04-22
3588 3626 $order->add_order_note(sprintf(__('Payment captured directly at %1$s', 'woo-vipps'), $this->get_payment_method_name()));
3589 3627 $order->payment_complete();
3590 3628 $this->update_vipps_payment_details($order);
3591 3629 } else {
3592 3630 // Not ok status; set to failed/cancelled
3593 - $order_is_retryable = Vipps::order_is_vipps_retryable($order->get_id());
3631 + $order_is_retryable = $allow_retry && Vipps::order_is_vipps_retryable($order->get_id());
3594 3632 $status_on_fail = $this->get_option('status_on_fail');
3595 3633 $cancel_on_fail = apply_filters('woo_vipps_cancel_failed_orders', false, $order, $vippsstatus);
3596 3634 if ($cancel_on_fail || !$order_is_retryable) {
3597 3635 $status_on_fail = 'cancelled';
@@ -3596,33 +3634,72 @@
3596 3634 if ($cancel_on_fail || !$order_is_retryable) {
3597 3635 $status_on_fail = 'cancelled';
3598 3636 }
3599 3637 if (!in_array($status_on_fail, ['cancelled', 'failed'])) {
3600 - /* translators: order status name. Cancelled is woocommerce status name */
3638 + /* translators: %1 = order status parameter. 'cancelled' is woocommerce order status name */
3601 3639 $this->log(__('Unsupported status for payment failure of \'%1$s\', falling back to cancelled.', 'woo-vipps'), 'warning');
3602 3640 $status_on_fail = 'cancelled';
3603 3641 }
3604 3642
3605 3643 /* translators: company name */
3606 - $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s', 'woo-vipps'), Vipps::CompanyName()));
3644 + $order->update_status($status_on_fail, sprintf(__('Callback: Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
3607 3645 }
3608 3646
3609 3647 $order->save();
3610 - clean_post_cache($order->get_id());
3648 + clean_post_cache($order_id);
3649 + }
3611 3650
3612 - // 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
3613 - $Vipps->callback_restore_session($orderid);
3614 - $Vipps->unlockOrder($order);
3651 + /* finalize shipping for express/checkout order. LP 2026-03-30 */
3652 + public function rest_order_set_shipping($request) {
3653 + $order_id = $request->get_param('order_id');
3654 + $data = $request->get_param('vipps_order_data');
3615 3655
3616 - // Create a signal file (if possible) so the confirm screen knows to check status IOK 2018-05-04
3617 - try {
3618 - $Vipps->createCallbackSignal($order,'ok');
3619 - } catch (Exception $e) {
3620 - // Could not create a signal file, but that's ok.
3656 + $order = wc_get_order($order_id);
3657 + if (!is_a($order, 'WC_Order')) {
3658 + return new WP_Error('order_not_found', __('Order not found', 'woo-vipps'), ['status' => 404]);
3621 3659 }
3622 3660
3623 - // Signal that we in fact handled the order.
3624 - return true;
3661 + $is_express_or_checkout = $order->get_meta('_vipps_express_checkout');
3662 + $shipping_set = $order->get_meta('_vipps_shipping_set');
3663 + if (!$is_express_or_checkout || $shipping_set) {
3664 + return new WP_Error('order_is_finalized', __('Order does not need to set shipping', 'woo-vipps'), ['status' => 409]);
3665 + }
3666 +
3667 + // Ensure we use the same session as for the original order from here on. IOK 2019-10-21
3668 + // IOK 2023-07-18 but because of the race condition issue, we cannot guarantee that any changes
3669 + // made to the session here will be saved. Sorry.
3670 + // UPDATE: Should be no more race condition since we moved callback into the action scheduler, and this shipping finalization into this rest endpoint. LP 2026-03-30
3671 + Vipps::instance()->callback_restore_session($order_id);
3672 +
3673 + // For Checkout version 3 there are no more userDetails, so we will add it, including defaults for anonymous purchases IOK 2023-01-10
3674 + // This will also normalize userDetails, adding 'sub' where possible and fields for backwards compatibility. 2025-08-12
3675 + $data = $this->ensure_userDetails($data, $order);
3676 +
3677 + // Some Express Checkout orders aren't really express checkout orders, but normal orders to which we have
3678 + // added scope name, email, phoneNumber. The reason is that we don't care about the address. But then
3679 + // we also get no user data in the callback, so we must replace the callback with a user info call. IOK 2023-03-10
3680 + // IOK 2025-09-29: This is probably *no longer true* - we now almost certainly *always* get a userDetails field if
3681 + // we have added a scope of any kind. This is therefore probably dead code.
3682 + // This being dead code, we'll not try to handle errors gracefully here. IOK 2026-03-18
3683 + if (!isset($data['userDetails'])) {
3684 + // This also calls ensure_userDetails and normalizeShippingDetails - but NB: it could fail, so call only when neccessary.
3685 + try {
3686 + $details = $this->get_payment_details($order);
3687 + $data = $details;
3688 + } catch (Exception $e) {
3689 + $this->log(sprintf(__("Could not get payment results for order %1\$s", 'woo-vipps'), $order->get_id()));
3690 + $this->log($e->getMessage());
3691 + }
3692 + }
3693 +
3694 + // Epayment Express Checkout is of course also significantly different from both the old Express and from Checkout in the formatting here. IOK 2025-08-12
3695 + $data = $this->normalizeShippingDetails($data, $order);
3696 +
3697 + // We should now always have shipping details.
3698 + if (isset($data['shippingDetails'])) {
3699 + $billing = isset($data['billingDetails']) ? $data['billingDetails'] : false;
3700 + $this->set_order_shipping_details($order,$data['shippingDetails'], $data['userDetails'], $billing, $data);
3701 + }
3625 3702 }
3626 3703
3627 3704 // Do the 'payment_complete' logic for non-SALE orders IOK 2020-09-22
3628 3705 public function payment_complete($order,$transactionid='') {
@@ -3641,9 +3718,9 @@
3641 3718 // Hook run by Woo after order is complete (authorized or sale). We'll add receipt info etc here.
3642 3719 public function order_payment_complete ($orderid) {
3643 3720 $order = wc_get_order($orderid);
3644 3721 if (!is_a($order, 'WC_Order')) return false;
3645 - if ($order->get_payment_method() != 'vipps') return false;
3722 + if (! Vipps::is_vipps_order($order)) return false;
3646 3723
3647 3724 $do_order_management = apply_filters('woo_vipps_order_management_on_payment_complete', true, $orderid);
3648 3725 if (!$do_order_management) return;
3649 3726
@@ -3671,9 +3748,9 @@
3671 3748 $order = wc_get_order($orderid);
3672 3749 if (!is_a($order, 'WC_Order')) {
3673 3750 return false;
3674 3751 }
3675 - if ($order->get_payment_method() != 'vipps') return false;
3752 + if (! Vipps::is_vipps_order($order)) return false;
3676 3753 if ($order->get_order_key() != wc_clean($orderkey)) {
3677 3754 return false;
3678 3755 }
3679 3756 try {
@@ -3685,9 +3762,9 @@
3685 3762 }
3686 3763 do_action('woo_vipps_payment_complete_at_shutdown', $order, $this);
3687 3764 } catch (Exception $e) {
3688 3765 // This is/should be non-critical so just log it.
3689 - $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");
3766 + $this->log(sprintf(__("Could not do all payment-complete actions on %1\$s order %2\$d: %3\$s ", 'woo-vipps'), Vipps::CompanyName(), $orderid, $e->getMessage()), "error");
3690 3767 }
3691 3768 }
3692 3769
3693 3770 // This is run on payment complete. Per default will it only add a link to the order confirmation page, but
@@ -3794,9 +3871,9 @@
3794 3871
3795 3872 $contents = WC()->cart->get_cart_contents();
3796 3873 $contents = apply_filters('woo_vipps_create_express_checkout_cart_contents',$contents);
3797 3874 try {
3798 - $cart_hash = md5(json_encode(wc_clean($contents)) . WC()->cart->total);
3875 + $cart_hash = WC()->cart->get_cart_hash();
3799 3876 $order = new WC_Order();
3800 3877 $order->set_status('pending');
3801 3878 $order->set_payment_method($this);
3802 3879 if ($ischeckout) {
@@ -3807,8 +3884,9 @@
3807 3884 }
3808 3885 // We use 'checkout' as the created_via key as per requests, but allow merchants to use their own. IOK 2022-09-15
3809 3886 $created_via = apply_filters('woo_vipps_express_checkout_created_via', 'checkout', $order, $ischeckout);
3810 3887 $order->set_created_via($created_via);
3888 + $order->set_cart_hash($cart_hash);
3811 3889
3812 3890 $dummy = sprintf(__('Vipps Express Checkout', 'woo-vipps')); // this is so gettext will find this string.
3813 3891 $dummy = sprintf(__('Vipps Checkout', 'woo-vipps')); // this is so gettext will find this string.
3814 3892
@@ -3970,9 +4048,9 @@
3970 4048 </p>
3971 4049 </div>
3972 4050 <?php endif; ?>
3973 4051
3974 - <?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
4052 + <?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
3975 4053 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
3976 4054 ?>
3977 4055
3978 4056 <?php /* We will *not* allow vipps checkout to be activated at this point, since the product is no longer sold. IOK 2026-04-30 */ ?>
@@ -4035,8 +4113,23 @@
4035 4113 update_option('woo_vipps_checkout_activated', true, true); // This must be true here, but still, make sure
4036 4114 Vipps::instance()->maybe_create_vipps_pages();
4037 4115 }
4038 4116
4117 + // Ensure special page has the necessary shortcode. LP 2026-09-01
4118 + $special_page = get_post(Vipps::get_special_page_id());
4119 + if ($special_page && !has_shortcode($special_page->post_content, 'vipps_special_page')) {
4120 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4121 + wp_update_post([
4122 + 'ID' => Vipps::get_special_page_id(),
4123 + 'post_content' => $new_content,
4124 + ]);
4125 + } else if (!Vipps::get_special_page_id()) {
4126 + // We shouldn't really get here, the page should be ensured to exist in init. LP 2026-09-03
4127 + /* translators: %s is current method name */
4128 + $this->log(sprintf(__('Missing special page in %s, attempting to fix', 'woo-vipps'), 'process_admin_options'), 'warning');
4129 + Vipps::instance()->ensure_special_page_exists();
4130 + }
4131 +
4039 4132 return $saved;
4040 4133 }
4041 4134
4042 4135 // Check our stored webhooks for consistency, which means the callback URLs should point to *this* site. If they don't,
@@ -4187,9 +4280,9 @@
4187 4280 // Now if we got a hook, then we should *just* remember that for this msn.
4188 4281 $local_hooks[$msn] = array($gotit['id'] => $gotit);
4189 4282 } else {
4190 4283 // If not, we don't have a hook for this msn and site, so we need to (try to) create one
4191 - // but only if the MSN is registered for the payment gateway 'vipps' ! IOK 2024-12-03
4284 + // but only if the MSN is registered for the payment gateway "vipps" ! IOK 2024-12-03
4192 4285 $keys = $keysets[$msn] ?? [];
4193 4286 $gateway = $keys['gw'] ?? 'vipps';
4194 4287
4195 4288 if ($gateway == 'vipps') {
@@ -4272,8 +4365,9 @@
4272 4365 public function payment_fields() {
4273 4366 // Use Billing Phone if it is required, otherwise ask for a phone IOK 2018-04-24
4274 4367 // For v2 of the api, just let Vipps ask for then umber
4275 4368 // IOK 2019-09-12 removed dead code only used for v1 of api
4369 + // This just prints a description of the payment method.
4276 4370 print $this->get_option('description');
4277 4371 return;
4278 4372 }
4279 4373 public function validate_fields() {