| 1 |
<?php |
| 2 |
/* |
| 3 |
This class is for hooks and plugin managent, and is instantiated as a singleton and set globally as $Vipps. IOK 2018-02-07 |
| 4 |
For WP-specific interactions. |
| 5 |
|
| 6 |
|
| 7 |
This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce |
| 8 |
Copyright (c) 2019 WP-Hosting AS |
| 9 |
|
| 10 |
MIT License |
| 11 |
|
| 12 |
Copyright (c) 2019 WP-Hosting AS |
| 13 |
|
| 14 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
| 15 |
of this software and associated documentation files (the "Software"), to deal |
| 16 |
in the Software without restriction, including without limitation the rights |
| 17 |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 18 |
copies of the Software, and to permit persons to whom the Software is |
| 19 |
furnished to do so, subject to the following conditions: |
| 20 |
|
| 21 |
The above copyright notice and this permission notice shall be included in all |
| 22 |
copies or substantial portions of the Software. |
| 23 |
|
| 24 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 25 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 26 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 27 |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 28 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 29 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 30 |
SOFTWARE. |
| 31 |
|
| 32 |
|
| 33 |
*/ |
| 34 |
if ( ! defined( 'ABSPATH' ) ) { |
| 35 |
exit; // Exit if accessed directly |
| 36 |
} |
| 37 |
require_once(dirname(__FILE__) . "/VippsAPIException.class.php"); |
| 38 |
|
| 39 |
class Vipps { |
| 40 |
/* Rest api consts. LP 2026-03-30 */ |
| 41 |
private const REST_NAMESPACE_BASE = 'woo-vipps'; |
| 42 |
private const REST_CURRENT_VERSION = 'v1'; // don't use this directly, use methods get_rest_namespace() and get_rest_url(). LP 2026-04-22 |
| 43 |
|
| 44 |
private static $instance = null; |
| 45 |
|
| 46 |
/* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */ |
| 47 |
public static $installed_gateways = []; |
| 48 |
|
| 49 |
/* This directory stores the files used to speed up the callbacks checking the order status. IOK 2018-05-04 */ |
| 50 |
private $callbackDirname = 'wc-vipps-status'; |
| 51 |
private $countrymap = null; |
| 52 |
// Used to provide the order in a callback to the session handler etc. IOK 2019-10-21 |
| 53 |
public $callbackorder = 0; |
| 54 |
|
| 55 |
// True if HPOS is being used |
| 56 |
public $HPOSActive = null; |
| 57 |
|
| 58 |
// used in the fake locking mechanism using transients |
| 59 |
private $lockKey = null; |
| 60 |
|
| 61 |
public $vippsJSConfig = array(); |
| 62 |
|
| 63 |
public $button_options_version = '2.0'; |
| 64 |
public $button_options_express_version = '2.0'; |
| 65 |
|
| 66 |
// IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction |
| 67 |
public static function CompanyName() { |
| 68 |
return __("Vipps MobilePay", 'woo-vipps'); |
| 69 |
} |
| 70 |
public static function CheckoutName($order=null) { |
| 71 |
return "Vipps MobilePay Checkout"; // Do not translate |
| 72 |
} |
| 73 |
public static function ExpressCheckoutName($order=null) { |
| 74 |
return __("Vipps MobilePay Express Checkout", 'woo-vipps'); |
| 75 |
} |
| 76 |
public static function LoginName() { |
| 77 |
return __("Login with Vipps", 'woo-vipps'); |
| 78 |
} |
| 79 |
|
| 80 |
public static function instance() { |
| 81 |
if (!static::$instance) static::$instance = new Vipps(); |
| 82 |
return static::$instance; |
| 83 |
} |
| 84 |
|
| 85 |
// recognize our own gateways or gateway ids IOK 2026-05-26 |
| 86 |
// param is either order or order's payment method id string. LP 2026-05-28 |
| 87 |
public static function is_vipps_order($order_or_string) { |
| 88 |
$id = is_string($order_or_string) ? $order_or_string : $order_or_string->get_payment_method(); |
| 89 |
return in_array($id , ['vipps', 'vipps_card']); |
| 90 |
} |
| 91 |
|
| 92 |
// To simplify development, we load translations from the plugins' own .mos on development branches. IOK 2023-11-28 |
| 93 |
public static function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) { |
| 94 |
$development = apply_filters('woo_vipps_use_plugin_translations', false); |
| 95 |
if (!$development) { |
| 96 |
return load_plugin_textdomain($domain, $deprecated, $plugin_rel_path); |
| 97 |
} |
| 98 |
// Available since 6.1.0 only IOK 2023-01-25 |
| 99 |
global $wp_textdomain_registry; |
| 100 |
if ($wp_textdomain_registry) { |
| 101 |
$locale = apply_filters( 'plugin_locale', determine_locale(), $domain ); |
| 102 |
$mofile = $domain . '-' . $locale . '.mo'; |
| 103 |
$path = WP_PLUGIN_DIR . '/' . trim( $plugin_rel_path, '/' ); |
| 104 |
$wp_textdomain_registry->set_custom_path( $domain, $path ); |
| 105 |
return load_textdomain( $domain, $path . '/' . $mofile, $locale ); |
| 106 |
} |
| 107 |
} |
| 108 |
|
| 109 |
public static function register_hooks() { |
| 110 |
$Vipps = static::instance(); |
| 111 |
register_activation_hook(WC_VIPPS_MAIN_FILE, array($Vipps,'activate')); |
| 112 |
register_deactivation_hook(WC_VIPPS_MAIN_FILE,array('Vipps','deactivate')); |
| 113 |
if (is_admin()) { |
| 114 |
add_action('admin_init',array($Vipps,'admin_init')); |
| 115 |
add_action('admin_menu',array($Vipps,'admin_menu')); |
| 116 |
} else { |
| 117 |
add_action('wp_footer', array($Vipps,'footer')); |
| 118 |
} |
| 119 |
add_action( 'plugins_loaded', array($Vipps,'plugins_loaded')); |
| 120 |
add_action( 'after_setup_theme', array($Vipps,'after_setup_theme')); |
| 121 |
add_action('init',array($Vipps,'init')); |
| 122 |
add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded')); |
| 123 |
add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter')); |
| 124 |
add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']); |
| 125 |
// Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may |
| 126 |
// not have loaded if the default checkout solution isn't the Checkout block. We'll load it anyway if the user has any local pickup locations |
| 127 |
// stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25 |
| 128 |
add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90); |
| 129 |
|
| 130 |
// Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist |
| 131 |
// Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this |
| 132 |
// is important. |
| 133 |
add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1); |
| 134 |
} |
| 135 |
|
| 136 |
// Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10 |
| 137 |
public function woocommerce_blocks_loaded() { |
| 138 |
if ( ! function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) { |
| 139 |
return; |
| 140 |
} |
| 141 |
woocommerce_store_api_register_endpoint_data( |
| 142 |
array( |
| 143 |
'endpoint' => Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema::IDENTIFIER, |
| 144 |
'namespace' => 'woo-vipps', |
| 145 |
'data_callback' => [$this, 'woo_vipps_store_api_cart_data'], |
| 146 |
'schema_callback' => [$this, 'woo_vipps_store_api_cart_schema'], |
| 147 |
'schema_type' => ARRAY_A, |
| 148 |
) |
| 149 |
); |
| 150 |
} |
| 151 |
|
| 152 |
// Some different bits and pieces: If we are on the pay-for-order page, we cannot provide Vipps for an order that has been at Vipps. IOK 2024-05-17 |
| 153 |
// Since we now support Vipps restart sessions, we *may* now provide Vipps a payment option on this page even if the order has been at Vipps. LP 2026-03-10 |
| 154 |
public function payment_gateway_filter ($gateways) { |
| 155 |
if (is_checkout_pay_page()) { |
| 156 |
$orderid = absint(get_query_var( 'order-pay')); |
| 157 |
$order = $orderid ? wc_get_order($orderid) : null; |
| 158 |
if (is_a($order, 'WC_Order') |
| 159 |
&& $order->get_meta('_vipps_init_timestamp') // allow vipps payment for new orders, like when creating an order from backend. LP 2026-05-28 |
| 160 |
) { |
| 161 |
// Existing override that allows repayment. IOK 2024-06-04 |
| 162 |
// i.e a third party plugin that implemented payment retrying for our plugin, we used to enable repayment only if this plugin was found. |
| 163 |
// $allow_repayment = class_exists('\Site\Plugins\WooVipps\WooVippsPayForOrder'); |
| 164 |
// However, now we implement payment retrying ourselves. LP 2026-03-18 |
| 165 |
|
| 166 |
$vipps_status = $order->get_meta('_vipps_status'); |
| 167 |
$retry_count = $order->get_meta('_vipps_retry_count'); |
| 168 |
$retry_enabled = apply_filters('woo_vipps_enable_payment_retry', true, $order, $vipps_status, $retry_count); |
| 169 |
$order_is_retryable = static::order_is_vipps_retryable($order->get_id()); |
| 170 |
|
| 171 |
// by default enable repayment if we can retry the order. LP 2026-03-18 |
| 172 |
$allow_repayment = apply_filters('woo_vipps_allow_repayment', $retry_enabled && $order_is_retryable, $order); // legacy filter |
| 173 |
if (!$allow_repayment) unset($gateways['vipps']); |
| 174 |
} |
| 175 |
} |
| 176 |
return $gateways; |
| 177 |
} |
| 178 |
|
| 179 |
// Get the singleton WC_GatewayVipps instance |
| 180 |
public function gateway() { |
| 181 |
if (class_exists('WC_Payment_Gateway')) { |
| 182 |
require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php"); |
| 183 |
return WC_Gateway_Vipps::instance(); |
| 184 |
} else { |
| 185 |
$this->log(__("Error: Cannot instantiate payment gateway, because WooCommerce is not loaded! This can happen when WooCommerce updates itself; but if it didn't, please activate WooCommerce again", 'woo-vipps'), 'error'); |
| 186 |
return null; |
| 187 |
} |
| 188 |
} |
| 189 |
|
| 190 |
|
| 191 |
// These are strings that should be available for translation possibly at some future point. Partly to be easier to work with translate.wordpress.org |
| 192 |
// Other usages are to translate any dynamic strings that may come from APIs etc. IOK 2021-03-18 |
| 193 |
private function translatable_strings() { |
| 194 |
// Nothing here right now |
| 195 |
return false; |
| 196 |
} |
| 197 |
|
| 198 |
// True iff support for HPOS has been activated IOK 2022-12-07 |
| 199 |
public function useHPOS() { |
| 200 |
if ($this->HPOSActive == null) { |
| 201 |
|
| 202 |
// Current way of checking IOK 2023-12-19 |
| 203 |
if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil')) { |
| 204 |
if (Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) { |
| 205 |
$this->HPOSActive = true; |
| 206 |
} else { |
| 207 |
$this->HPOSActive = false; |
| 208 |
} |
| 209 |
return $this->HPOSActive; |
| 210 |
} |
| 211 |
|
| 212 |
// This works in the backend, so ensures we are good with the meta fields etc. |
| 213 |
if (function_exists('wc_get_container') && // 4.4.0 |
| 214 |
function_exists('wc_get_page_screen_id') && // Part of HPOS, not yet released |
| 215 |
class_exists("Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController") && |
| 216 |
wc_get_container()->get( Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { |
| 217 |
$this->HPOSActive = true; |
| 218 |
} else { |
| 219 |
$this->HPOSActive = false; |
| 220 |
} |
| 221 |
} |
| 222 |
return $this->HPOSActive; |
| 223 |
} |
| 224 |
|
| 225 |
public function init () { |
| 226 |
|
| 227 |
// Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block |
| 228 |
// needs these to be defined in the backend. IOK 2024-04-16 |
| 229 |
add_action('wp_loaded', array($this, 'wp_register_scripts')); |
| 230 |
add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts')); |
| 231 |
|
| 232 |
// Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26 |
| 233 |
add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2); |
| 234 |
|
| 235 |
// Used in 'compat mode' only to add products to the cart |
| 236 |
add_filter('woocommerce_add_to_cart_redirect', array($this, 'woocommerce_add_to_cart_redirect'), 10, 1); |
| 237 |
|
| 238 |
$this->add_shortcodes(); |
| 239 |
$this->maybe_add_vipps_badge_feature(); |
| 240 |
|
| 241 |
// Handle the asynch call to send Order Management data on payment complete - this will push order data to the users' Vipps app |
| 242 |
add_action('admin_post_nopriv_woo_vipps_order_management', array($this, 'do_order_management')); |
| 243 |
add_action('admin_post_woo_vipps_order_management', array($this, 'do_order_management')); |
| 244 |
|
| 245 |
// Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02 |
| 246 |
add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action')); |
| 247 |
|
| 248 |
// Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22 |
| 249 |
add_action('rest_api_init', function() { |
| 250 |
register_rest_route(self::get_rest_namespace('v1'), '/express-products', [ |
| 251 |
'methods' => 'GET', |
| 252 |
'callback' => [$this, 'rest_express_checkout_products'], |
| 253 |
'permission_callback' => '__return_true', |
| 254 |
]); |
| 255 |
}); |
| 256 |
|
| 257 |
// We need a 5-minute scheduled event for the handler for missed callbacks. Using the |
| 258 |
// action scheduler would be better, but we can't do that just yet because of backwards |
| 259 |
// compatibility. At some point, support for older woo-versions should be dropped; then this |
| 260 |
// should use the action scheduler instead. IOK 2021-06-21 |
| 261 |
add_filter('cron_schedules', function ($schedules) { |
| 262 |
if(!isset($schedules["5min"])){ |
| 263 |
$schedules["5min"] = array( |
| 264 |
'interval' => 5*60, |
| 265 |
'display' => __('Once every 5 minutes')); |
| 266 |
} |
| 267 |
return $schedules; |
| 268 |
}); |
| 269 |
// Offload work to wp-cron so it can be done in the background on sites with heavy load IOK 2020-04-01 |
| 270 |
add_action('vipps_cron_cleanup_hook', array($this, 'cron_cleanup_hook')); |
| 271 |
// Check periodically for orders that are stuck pending with no callback IOK 2021-06-21 |
| 272 |
add_action('vipps_cron_missing_callback_hook', array($this, 'cron_check_for_missing_callbacks')); |
| 273 |
|
| 274 |
// For the rest, we need to read the payment gateways setting, and the payment gateway may not actually |
| 275 |
// exist at this point. This is because for it to exist, WooCommerce must have loaded, and if it hasn't, for instance |
| 276 |
// because it is self-updating or because it has been deactivated just now or something, we won't have access to it. |
| 277 |
// Therefore test it first. IOK 2022-12-08 |
| 278 |
$gw = $this->gateway(); |
| 279 |
|
| 280 |
// This is a developer-mode level feature because flock() is not portable. This ensures callbacks and shopreturns do not |
| 281 |
// simultaneously update the orders, in particular not the express checkout order lines wrt shipping. IOK 2020-05-19 |
| 282 |
if ($gw && $gw->get_option('use_flock') == 'yes') { |
| 283 |
add_filter('woo_vipps_lock_order', array($this,'flock_lock_order')); |
| 284 |
add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order')); |
| 285 |
} |
| 286 |
|
| 287 |
// Set default button options, migrating any older setup IOK 2026-07-15 |
| 288 |
$this->init_button_options(); |
| 289 |
|
| 290 |
/* |
| 291 |
From version 6.2.x we create a real physical page to handle the "special" vipps pages, |
| 292 |
where we earlier used just a fake page with no real page id, unless especially configured. |
| 293 |
We therefore need to add code to maintain this special page. |
| 294 |
|
| 295 |
woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages, |
| 296 |
and we hook unto this with woocommerce_create_pages. LP 2026-09-03 |
| 297 |
*/ |
| 298 |
|
| 299 |
// Delete special page id option when its deleted or trashed, so that we dont have to load |
| 300 |
// in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03 |
| 301 |
$delete_special_page_id = function($post_id, $post = null) { |
| 302 |
if (static::get_special_page_id() === $post_id) { |
| 303 |
delete_option('woocommerce_vipps_special_page_page_id'); |
| 304 |
} |
| 305 |
}; |
| 306 |
add_action('delete_post', $delete_special_page_id, 10, 2); |
| 307 |
add_action('wp_trash_post', $delete_special_page_id, 10, 2); |
| 308 |
|
| 309 |
$this->ensure_special_page_exists(); |
| 310 |
|
| 311 |
|
| 312 |
// We want this special page to have a certain title and maybe special scripts and so on, |
| 313 |
// this gets run in template redirect for these pages. |
| 314 |
add_action('woo_vipps_before_handling_special_page', function ($action) { |
| 315 |
// Change title dynamically depending on action. LP 2026-09-02 |
| 316 |
add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2); |
| 317 |
}); |
| 318 |
|
| 319 |
// Add an admin interface for this page as well IOK 2026-09-11 |
| 320 |
add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages')); |
| 321 |
|
| 322 |
} |
| 323 |
|
| 324 |
public function admin_init () { |
| 325 |
$gw = $this->gateway(); |
| 326 |
require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php"); |
| 327 |
$adminSettings = VippsAdminSettings::instance(); |
| 328 |
// Stuff for the Order screen |
| 329 |
add_action('woocommerce_order_item_add_action_buttons', array($this, 'order_item_add_action_buttons'), 10, 1); |
| 330 |
|
| 331 |
// Don't allow deletion of refunds made through Vipps IOK 2025-11-17 |
| 332 |
add_action('woocommerce_after_order_refund_item_name', function ($refund) { |
| 333 |
$orderid = $refund->get_parent_id(); |
| 334 |
$order = wc_get_order($orderid); |
| 335 |
if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) { |
| 336 |
$id = $refund->get_id(); |
| 337 |
$gw = $refund->get_refunded_payment(); |
| 338 |
if ($gw) { |
| 339 |
$msg = sprintf(__('Refunded through %1$s', 'woo-vipps'), $this->get_payment_method_name()); |
| 340 |
echo "<style>#woocommerce-order-items tr.refund[data-order_refund_id=\"" . intval($id) . "\"] .wc-order-edit-line-item .wc-order-edit-line-item-actions a.delete_refund { display: none; }</style>"; |
| 341 |
echo "<i>" . esc_html($msg) . "</i>"; |
| 342 |
} |
| 343 |
}}); |
| 344 |
|
| 345 |
require_once(dirname(__FILE__) . "/VippsDismissibleAdminBanners.class.php"); |
| 346 |
VippsDismissibleAdminBanners::add(); |
| 347 |
|
| 348 |
// Styling etc |
| 349 |
add_action('admin_head', array($this, 'admin_head')); |
| 350 |
|
| 351 |
// Scripts |
| 352 |
$this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce'); |
| 353 |
wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig); |
| 354 |
add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts')); |
| 355 |
|
| 356 |
// IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page. |
| 357 |
add_action('current_screen', function ($screen) { |
| 358 |
if (!is_admin() || !$screen || $screen->id !== 'woocommerce_page_wc-settings') return; |
| 359 |
$section = ($_GET['section'] ?? ""); |
| 360 |
if (($_GET['tab'] ?? "")!= 'checkout' |
| 361 |
|| !in_array($section, ['vipps', 'vipps_card']) |
| 362 |
) return; |
| 363 |
|
| 364 |
$settings_tab = ''; |
| 365 |
if ('vipps_card' === $section) $settings_tab = '#Card payments'; |
| 366 |
|
| 367 |
wp_safe_redirect(admin_url("admin.php?page=vipps_settings_menu$settings_tab")); |
| 368 |
exit(); |
| 369 |
}); |
| 370 |
|
| 371 |
// Custom product properties |
| 372 |
// IOK 2024-01-17 temporary: The special product properties are currenlty only active for Vipps |
| 373 |
// IOK 2025-09-01 now available for all |
| 374 |
add_filter('woocommerce_product_data_tabs', array($this,'woocommerce_product_data_tabs'),99); |
| 375 |
add_action('woocommerce_product_data_panels', array($this,'woocommerce_product_data_panels'),99); |
| 376 |
add_action('woocommerce_process_product_meta', array($this, 'process_product_meta'), 10, 2); |
| 377 |
|
| 378 |
add_action('add_meta_boxes', array($this, 'add_meta_boxes')); |
| 379 |
|
| 380 |
// Keep admin notices during redirects IOK 2018-05-07 |
| 381 |
add_action('admin_notices',array($this,'stored_admin_notices')); |
| 382 |
|
| 383 |
// Ajax just for the backend |
| 384 |
add_action('wp_ajax_vipps_create_shareable_link', array($this, 'ajax_vipps_create_shareable_link')); |
| 385 |
add_action('wp_ajax_vipps_payment_details', array($this, 'ajax_vipps_payment_details')); |
| 386 |
add_action('wp_ajax_vipps_update_admin_settings', array($adminSettings, 'ajax_vipps_update_admin_settings')); |
| 387 |
|
| 388 |
// POST actions for the backend |
| 389 |
add_action('admin_post_update_vipps_badge_settings', array($this, 'update_badge_settings')); |
| 390 |
add_action('admin_post_update_vipps_button_settings', array($this, 'update_button_settings')); |
| 391 |
add_action('admin_post_vipps_delete_webhook', array($this, 'vipps_delete_webhook')); |
| 392 |
add_action('admin_post_vipps_add_webhook', array($this, 'vipps_add_webhook')); |
| 393 |
|
| 394 |
// Link to the settings page from the plugin list |
| 395 |
add_filter( 'plugin_action_links_'.plugin_basename(WC_VIPPS_MAIN_FILE ), array($this, 'plugin_action_links')); |
| 396 |
|
| 397 |
if ($gw->enabled == 'yes' && $gw->is_test_mode()) { |
| 398 |
$what = sprintf(__('%1$s is currently in test mode - no real transactions will occur', 'woo-vipps'), Vipps::CompanyName()); |
| 399 |
$this->add_vipps_admin_notice($what,'info', '', 'test-mode'); |
| 400 |
} |
| 401 |
|
| 402 |
|
| 403 |
// This requires merchants using the old shipping callback filter to choose between this or the new shipping method mechanism. IOK 2020-02-17 |
| 404 |
if (has_action('woo_vipps_shipping_methods')) { |
| 405 |
$option = $gw->get_option('newshippingcallback'); |
| 406 |
if ($option != 'old' && $option != 'new') { |
| 407 |
$what = __('Your theme or a plugin is currently overriding the <code>\'woo_vipps_shipping_methods\'</code> filter to customize your shipping alternatives. While this works, this disables the newer Express Checkout shipping system, which is neccessary if your shipping is to include metadata. You can do this, or stop this message, from the <a href="%1$s">settings page</a>', 'woo-vipps'); |
| 408 |
$this->add_vipps_admin_notice($what,'info'); |
| 409 |
} |
| 410 |
} |
| 411 |
|
| 412 |
// IOK 2020-04-01 If the plugin is updated, the normal 'activate' hook may not run. Add the scheduled events if not present. |
| 413 |
// Normal updates will not need this, but if updates are 'sideloaded', it is neccessary still. This call will only do work if the |
| 414 |
// jobs are not scheduled. We'll ensure the action is active first time an admin logs in. |
| 415 |
if (!defined('DOING_AJAX') || !DOING_AJAX) { |
| 416 |
static::maybe_add_cron_event(); |
| 417 |
if (!get_option('woo-vipps-configured')) { |
| 418 |
list($ok, $msg) = $gw->check_connection(); |
| 419 |
if (!$ok){ |
| 420 |
if ($msg) { |
| 421 |
$this->add_vipps_admin_notice(sprintf(__("<p>%1\$s not yet correctly configured: please go to <a href='%2\$s'>the %1\$s settings</a> to complete your setup:<br> %3\$s</p>", 'woo-vipps'), Vipps::CompanyName(), admin_url('/admin.php?page=vipps_settings_menu'), $msg)); |
| 422 |
} else { |
| 423 |
$this->add_vipps_admin_notice(sprintf(__("<p>%1\$s not yet configured: please go to <a href='%2\$s'>the %1\$s settings</a> to complete your setup!</p>", 'woo-vipps'), Vipps::CompanyName(), admin_url('/admin.php?page=vipps_settings_menu'))); |
| 424 |
} |
| 425 |
} |
| 426 |
|
| 427 |
} |
| 428 |
// If we are configured, but we don't have any webhooks yet, initialize them for the epayment api. IOK 2023-12-20 |
| 429 |
// if we do have them, check them for consistency |
| 430 |
if (get_option('woo-vipps-configured')) { |
| 431 |
if (empty(get_option('_woo_vipps_webhooks'))) { |
| 432 |
$gw->initialize_webhooks(); |
| 433 |
} else { |
| 434 |
$ok = $gw->check_webhooks(); |
| 435 |
if (!$ok) { |
| 436 |
$gw->initialize_webhooks(); |
| 437 |
}; |
| 438 |
} |
| 439 |
} |
| 440 |
} |
| 441 |
} |
| 442 |
|
| 443 |
|
| 444 |
/** Ensure we have a special page for payment flows |
| 445 |
* |
| 446 |
* woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages, |
| 447 |
* and we hook unto this with woocommerce_create_pages. LP 2026-09-03 |
| 448 |
**/ |
| 449 |
public function ensure_special_page_exists() { |
| 450 |
if (static::get_special_page_id()) return; |
| 451 |
$this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info'); |
| 452 |
|
| 453 |
// If user had in previous version overriden the fake page with a real one: migrate this page to be the special page. LP 2026-09-01 |
| 454 |
$old_special_page_id = $this->gateway()->get_option('vippsspecialpageid'); |
| 455 |
if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) { |
| 456 |
$this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info'); |
| 457 |
// there is no wc_set_page_id() so we update the option directly. LP 2026-09-01 |
| 458 |
update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id); |
| 459 |
|
| 460 |
// Ensure this page has the necessary shortcode. LP 2026-09-01 |
| 461 |
if (!has_shortcode($special_page->post_content, 'vipps_special_page')) { |
| 462 |
$new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->"; |
| 463 |
wp_update_post([ |
| 464 |
'ID' => $old_special_page_id, |
| 465 |
'post_content' => $new_content, |
| 466 |
]); |
| 467 |
} |
| 468 |
} else { |
| 469 |
// Create special page if its missing. LP 2026-09-01 |
| 470 |
$this->maybe_create_vipps_pages(); |
| 471 |
} |
| 472 |
} |
| 473 |
|
| 474 |
// Admin interface for the special page on woo/advanced/pages |
| 475 |
public function woocommerce_settings_pages ($settings) { |
| 476 |
$i = -1; |
| 477 |
foreach($settings as $entry) { |
| 478 |
$i++; |
| 479 |
if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') { |
| 480 |
break; |
| 481 |
} |
| 482 |
} |
| 483 |
if ($i > 0) { |
| 484 |
$vippspagesettings = array( |
| 485 |
array( |
| 486 |
'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()), |
| 487 |
'desc' => sprintf(__('This page is used for various special pages used by %1$s', 'woo-vipps'), Vipps::CompanyName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_special_page') , |
| 488 |
'id' => 'woocommerce_vipps_special_page_page_id', |
| 489 |
'type' => 'single_select_page_with_search', |
| 490 |
'default' => '', |
| 491 |
'class' => 'wc-page-search', |
| 492 |
'css' => 'min-width:300px;', |
| 493 |
'args' => array( |
| 494 |
'exclude' => |
| 495 |
array( |
| 496 |
wc_get_page_id( 'myaccount' ), |
| 497 |
wc_get_page_id( 'checkout' ), |
| 498 |
wc_get_page_id( 'cart' ), |
| 499 |
), |
| 500 |
), |
| 501 |
'desc_tip' => true, |
| 502 |
'autoload' => false, |
| 503 |
)); |
| 504 |
array_splice($settings, $i, 0, $vippspagesettings); |
| 505 |
} |
| 506 |
|
| 507 |
return $settings; |
| 508 |
} |
| 509 |
|
| 510 |
|
| 511 |
|
| 512 |
// Runs on init, adds the Vipps badge feature if activated |
| 513 |
public function maybe_add_vipps_badge_feature () { |
| 514 |
$badge_options = get_option('vipps_badge_options'); |
| 515 |
if (!$badge_options || !@$badge_options['badgeon']) return false; |
| 516 |
|
| 517 |
add_action('wp_enqueue_scripts', function () { wp_enqueue_script('vipps-onsite-messageing'); }); |
| 518 |
add_action('woocommerce_before_add_to_cart_form', function () use ($badge_options) { |
| 519 |
global $product; |
| 520 |
if (!is_a($product, 'WC_Product')) return; |
| 521 |
|
| 522 |
$show = intval(@$badge_options['defaultall']); |
| 523 |
$forthis = $product->get_meta('_vipps_show_badge', true); |
| 524 |
$dontshow = ($forthis == 'none'); |
| 525 |
|
| 526 |
$doshow = !$dontshow && ($show || ($forthis && $forthis != 'none')); |
| 527 |
|
| 528 |
if (!apply_filters('woo_vipps_show_vipps_badge_for_product', $doshow, $product)) { |
| 529 |
return; |
| 530 |
} |
| 531 |
|
| 532 |
$attr = ""; |
| 533 |
if ($forthis != 'none' || isset($badge_options['variant'])) { |
| 534 |
$variant = ($forthis && $forthis != 'none') ? $forthis : $badge_options['variant']; |
| 535 |
$attr .= " variant='" . sanitize_title($variant) . "' "; |
| 536 |
} |
| 537 |
|
| 538 |
$lang = $this->get_customer_language(); |
| 539 |
if ($lang) { |
| 540 |
$attr .= " language='". $lang . "' "; |
| 541 |
} |
| 542 |
|
| 543 |
$brand = $this->get_payment_method_name(); |
| 544 |
if ($brand) $attr .= " brand='". strtolower($brand) . "' "; |
| 545 |
|
| 546 |
|
| 547 |
$badge = "<vipps-mobilepay-badge $attr></vipps-mobilepay-badge>"; |
| 548 |
|
| 549 |
echo apply_filters('woo_vipps_product_badge_html', $badge); |
| 550 |
}); |
| 551 |
|
| 552 |
} |
| 553 |
|
| 554 |
// A small interface for editing and managing the webhooks for the MSNs for this site IOK 2023-12-20 |
| 555 |
public function webhook_menu_page () { |
| 556 |
if (!current_user_can('manage_woocommerce')) { |
| 557 |
wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps')); |
| 558 |
} |
| 559 |
$portalurl = 'https://portal.vippsmobilepay.com'; |
| 560 |
$webhookapi = 'https://developer.vippsmobilepay.com/docs/APIs/webhooks-api/'; |
| 561 |
|
| 562 |
echo "<div class='wrap vipps-badge-settings'>\n"; |
| 563 |
echo "<h1>" . __('Webhooks', 'woo-vipps') . "</h1>\n"; |
| 564 |
echo "<p>"; printf(__('Whenever an event like a payment or a cancellation occurs on a %1$s account, you can be notified of this using a <i>webhook</i>. This is used by this plugin to get noticed of payments by users even when they do not return to your store.', 'woo-vipps'), Vipps::CompanyName()); echo "</p>"; |
| 565 |
echo "<p>"; __('To do this, the plugin will automatically add webhooks for the MSN - Merchant Serial Numbers - configured on this site', 'woo-vipps'); echo "</p>"; |
| 566 |
echo "<p>"; __('If your MSN has registered other callbacks, for instance for another website, you can manage these here - and you can also add your own hooks that will be notified of payment events to any other URL you enter.', 'woo-vipps'); echo "</p>"; |
| 567 |
echo "<p>"; printf(__('Implementing a webhook is not trivial, so you will probably need a developer for this. You can read more about what is required <a href="%1$s">here</a>. ', 'woo-vipps'), $webhookapi); |
| 568 |
printf(__('Please note that there is normally a limit of <em><strong>5</strong> webhooks per MSN</em> - contact %1$s if you need more', 'woo-vipps'), Vipps::CompanyName()); |
| 569 |
echo "</p>"; |
| 570 |
echo "<p>"; print __('The following is a listing of your webhooks. If you have changed your website name, you may see some hooks that you do not recognize - these should be deleted', 'woo-vipps'); echo "</p>"; |
| 571 |
|
| 572 |
$keyset = $this->gateway()->get_keyset(); |
| 573 |
$recurrings = $this->gateway()->get_keyset(); |
| 574 |
foreach($recurrings as $msn=> $keys) { |
| 575 |
if (!isset($keyset[$msn])) { |
| 576 |
$keyset[$msn] = $keys; |
| 577 |
} |
| 578 |
} |
| 579 |
$allhooks = $this->gateway()->initialize_webhooks(); |
| 580 |
$localhooks = get_option('_woo_vipps_webhooks'); |
| 581 |
|
| 582 |
echo "<form method='post' action='" . admin_url("admin-post.php") . "' autocomplete='off' id=webhook_action_form>"; |
| 583 |
echo "<input type='hidden' id='webhook_id' name='webhook_id' value='' autocomplete='false'>"; |
| 584 |
echo "<input type='hidden' id='webhook_msn' name='webhook_msn' value='' autocomplete='false'>"; |
| 585 |
echo "<input type='hidden' id='webhook_url' name='webhook_url' value='' autocomplete='false'>"; |
| 586 |
echo "<input type='hidden' id='webhook_events' name='webhook_events' value='' autocomplete='false'>"; |
| 587 |
echo "<input type='hidden' id='webhook_post_action' name='action' value='' autocomplete='false'>"; |
| 588 |
wp_nonce_field('webhook_nonce', 'webhook_nonce'); |
| 589 |
echo "</form>"; |
| 590 |
|
| 591 |
foreach ($keyset as $msn => $data) { |
| 592 |
$testmode = $data['testmode'] ?? false; |
| 593 |
echo "<div style='margin-top: 2rem; margin-bottom: 2rem'>"; |
| 594 |
echo "<h2>"; |
| 595 |
echo sprintf(__('Merchant Serial Number %1$s', 'woo-vipps'), $msn); |
| 596 |
if ($testmode) echo " (" . __('Test mode', 'woo-vipps') . ")"; |
| 597 |
echo "<a style='float:right; font-size:smaller' class='webhook-adder' href='javascript:void(0)' data-msn='" . esc_attr($msn) . "'>[" . __('Add a webhook to this MSN', 'woo-vipps') . "]</a>"; |
| 598 |
echo "</h2>"; |
| 599 |
|
| 600 |
$all = $allhooks[$msn] ?? []; |
| 601 |
$thehooks = $all['webhooks'] ?? []; |
| 602 |
$locals = $localhooks[$msn] ?? []; |
| 603 |
|
| 604 |
echo "<table class='table webhook-table'><thead><tr><th style='text-align: left'>" . __('Webhook', 'woo-vipps') . "</th><th>" . __('Action', 'woo-vipps') . "</th>" . "</tr></thead>"; |
| 605 |
echo "<tbody>"; |
| 606 |
foreach($thehooks as $hook) { |
| 607 |
$id = $hook['id']; |
| 608 |
$url = $hook['url']; |
| 609 |
$events = $hook['events']; |
| 610 |
$local = $locals[$id] ?? false; |
| 611 |
|
| 612 |
|
| 613 |
echo "<tr" . ($local ? " class='local' " : '') . " data-webhook-id='" . esc_attr($id) . "' data-msn='" . esc_attr($msn) . "'"; |
| 614 |
echo " data-hookdata='" . json_encode($hook) . "'>"; |
| 615 |
echo "<td>" . esc_html($url) . "</td>"; |
| 616 |
echo "<td class='actions'>"; |
| 617 |
echo "<a href='javascript:void(0)' class='webhook-viewer'>[" . __('View', 'woo-vipps') . "]</a> "; |
| 618 |
if (!$local) { |
| 619 |
echo " <a href='javascript:void(0)' class='webhook-deleter'>[" . __('Delete', 'woo-vipps') . "]</a>"; |
| 620 |
} else { |
| 621 |
echo " <em>". __('Created for this site', 'woo-vipps') . "</em>"; |
| 622 |
} |
| 623 |
echo "</td>"; |
| 624 |
echo "</tr>"; |
| 625 |
} |
| 626 |
echo "</tbody>"; |
| 627 |
echo "</table>"; |
| 628 |
echo "</div>"; |
| 629 |
echo "<hr>"; |
| 630 |
} |
| 631 |
|
| 632 |
$epayment_events = [__('Created', 'woo-vipps') => 'epayments.payment.created.v1', |
| 633 |
__('Aborted', 'woo-vipps') => 'epayments.payment.aborted.v1', |
| 634 |
__('Expired', 'woo-vipps') => 'epayments.payment.expired.v1', |
| 635 |
__('Cancelled', 'woo-vipps') => 'epayments.payment.cancelled.v1', |
| 636 |
__('Captured', 'woo-vipps') => 'epayments.payment.captured.v1', |
| 637 |
__('Refunded', 'woo-vipps') => 'epayments.payment.refunded.v1', |
| 638 |
__('Authorized', 'woo-vipps') => 'epayments.payment.authorized.v1', |
| 639 |
__('Terminated', 'woo-vipps') => 'epayments.payment.terminated.v1']; |
| 640 |
|
| 641 |
$recurring_events = [ __('Agreement accepted', 'woo-vipps') =>'recurring.agreement-activated.v1', |
| 642 |
__('Agreement rejected', 'woo-vipps') =>'recurring.agreement-rejected.v1', |
| 643 |
__('Agreement stopped', 'woo-vipps') =>'recurring.agreement-stopped.v1', |
| 644 |
__('Agreement expired', 'woo-vipps') =>'recurring.agreement-expired.v1', |
| 645 |
__('Charge reserved', 'woo-vipps') =>'recurring.charge-reserved.v1', |
| 646 |
__('Charge captured', 'woo-vipps') =>'recurring.charge-captured.v1', |
| 647 |
__('Charge cancelled', 'woo-vipps') =>'recurring.charge-canceled.v1', |
| 648 |
__('Charge failed', 'woo-vipps') =>'recurring.charge-failed.v1']; |
| 649 |
|
| 650 |
$qr_events = [__('User Checked in', 'woo-vipps')=> 'user.checked-in.v1']; |
| 651 |
|
| 652 |
|
| 653 |
$defaultevents = ['epayments.payment.authorized.v1', 'epayments.payment.aborted.v1', 'epayments.payment.expired.v1', 'epayments.payment.terminated.v1']; |
| 654 |
|
| 655 |
|
| 656 |
?> |
| 657 |
|
| 658 |
<dialog id='webhook_view_dialog' style='width:70%'> |
| 659 |
<form method="dialog"> |
| 660 |
<div class='viewdata' style='margin-bottom: 3rem'> |
| 661 |
<label>ID</label><span class='webhook_id'></span> |
| 662 |
<label>URL</label><span class='webhook_url'></span> |
| 663 |
<label>Events</label><div style='width:80%' class='webhook_events'></div> |
| 664 |
</div> |
| 665 |
<button class="button btn button-primary" type="submit" value="OK"><?php _e('OK'); ?></button> |
| 666 |
</form> |
| 667 |
</dialog> |
| 668 |
|
| 669 |
|
| 670 |
<dialog id='webhook_add_dialog' style='width: 70%'> |
| 671 |
<form method="dialog"> |
| 672 |
<h3><?php _e('Add a webhook', 'woo-vipps'); ?></h3> |
| 673 |
<label for='dialog_webhook_msn'>MSN</label><input style='width: 50%' id='dialog_webhook_msn' required readonly type="text" name="webhook_msn" placeholder=""> |
| 674 |
<label for='dialog_webhook_url'>URL</label><input style='width: 50%' id='dialog_webhook_url' autofocus required type="url" name="webhook_url" placeholder="https://..."> |
| 675 |
<div class="events" style="margin-bottom: 2rem"> |
| 676 |
<h3>Epayment</h3> |
| 677 |
<?php foreach($epayment_events as $label=>$event): ?> |
| 678 |
<label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?> |
| 679 |
<input <?php if (in_array($event, $defaultevents)) echo " checked " ?> |
| 680 |
type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'> |
| 681 |
</label> |
| 682 |
<?php endforeach; ?> |
| 683 |
<h3>Recurring</h3> |
| 684 |
<?php foreach($recurring_events as $label=>$event): ?> |
| 685 |
<label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?> |
| 686 |
<input <?php if (in_array($event, $defaultevents)) echo " checked " ?> |
| 687 |
type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'> |
| 688 |
</label> |
| 689 |
<?php endforeach; ?> |
| 690 |
<h3>QR</h3> |
| 691 |
<?php foreach($qr_events as $label=>$event): ?> |
| 692 |
<label for='<?php echo esc_attr($event); ?>'><?php echo esc_html($label);?> |
| 693 |
<input <?php if (in_array($event, $defaultevents)) echo " checked " ?> |
| 694 |
type='checkbox' name='webhook_event' value='<?php echo esc_attr($event); ?>'> |
| 695 |
</label> |
| 696 |
<?php endforeach; ?> |
| 697 |
|
| 698 |
</div> |
| 699 |
<div class='buttonholder'> |
| 700 |
<button class="button btn button-primary" type="submit" value="OK"><?php _e('Add this URL as a webhook', 'woo-vipps'); ?></button> |
| 701 |
<button class="button btn" type="submit" formnovalidate value="NO"><?php _e('No, forget it', 'woo-vipps'); ?></button> |
| 702 |
</div> |
| 703 |
</form> |
| 704 |
</dialog> |
| 705 |
|
| 706 |
<style> |
| 707 |
dialog#webhook_add_dialog::backdrop { |
| 708 |
background-color: rgba(0.9,0.9,0.9,0.7); |
| 709 |
} |
| 710 |
</style> |
| 711 |
|
| 712 |
<script> |
| 713 |
let dialog = document.getElementById('webhook_add_dialog'); |
| 714 |
let viewdialog = document.getElementById('webhook_view_dialog'); |
| 715 |
dialog.addEventListener('close', function () { |
| 716 |
if (dialog.returnValue =='OK') { |
| 717 |
let msn = dialog.querySelector('input[name="webhook_msn"]').value; |
| 718 |
let url = dialog.querySelector('input[name="webhook_url"]').value; |
| 719 |
dialog.querySelector('input[name="webhook_url"]').value = ""; |
| 720 |
dialog.querySelector('input[name="webhook_msn"]').value = ""; |
| 721 |
|
| 722 |
let events = dialog.querySelectorAll('input[name="webhook_event"]:checked'); |
| 723 |
let eventlist = []; |
| 724 |
let eventstring = ''; |
| 725 |
for (const ev of events.values()) { |
| 726 |
eventlist.push(ev.value); |
| 727 |
} |
| 728 |
eventstring = eventlist.join(','); |
| 729 |
|
| 730 |
|
| 731 |
if (msn && url && eventstring) { |
| 732 |
jQuery('#webhook_msn').val(msn); |
| 733 |
jQuery('#webhook_post_action').val('vipps_add_webhook'); |
| 734 |
jQuery('#webhook_url').val(url); |
| 735 |
jQuery('#webhook_events').val(eventstring); |
| 736 |
let f = jQuery('#webhook_action_form'); |
| 737 |
f.submit(); |
| 738 |
} |
| 739 |
} |
| 740 |
dialog.querySelector('input[name="webhook_url"]').value = ""; |
| 741 |
dialog.querySelector('input[name="webhook_msn"]').value = ""; |
| 742 |
}); |
| 743 |
|
| 744 |
let data = ""; |
| 745 |
jQuery('a.webhook-viewer').click(function (e) { |
| 746 |
e.preventDefault(); |
| 747 |
let row= jQuery(this).closest('tr'); |
| 748 |
data = row.data('hookdata'); |
| 749 |
viewdialog.querySelector('.viewdata').querySelector('.webhook_id').innerHTML= data['id']; |
| 750 |
viewdialog.querySelector('.viewdata').querySelector('.webhook_url').innerHTML= data['url']; |
| 751 |
viewdialog.querySelector('.viewdata').querySelector('.webhook_events').innerHTML= data['events'].join(" "); |
| 752 |
viewdialog.showModal(); |
| 753 |
}); |
| 754 |
|
| 755 |
|
| 756 |
jQuery('a.webhook-deleter').click(function (e) { |
| 757 |
e.preventDefault(); |
| 758 |
let row = jQuery(this).closest('tr'); |
| 759 |
let wh = row.data('webhook-id'); |
| 760 |
let msn = row.data('msn'); |
| 761 |
let f = jQuery('#webhook_action_form'); |
| 762 |
jQuery('#webhook_id').val(wh); |
| 763 |
jQuery('#webhook_msn').val(msn); |
| 764 |
jQuery('#webhook_post_action').val('vipps_delete_webhook'); |
| 765 |
f.submit(); |
| 766 |
}); |
| 767 |
|
| 768 |
jQuery('a.webhook-adder').click(function (e) { |
| 769 |
e.preventDefault(); |
| 770 |
let msn = jQuery(this).data('msn'); |
| 771 |
dialog.querySelector('input[name="webhook_url"]').value = ""; |
| 772 |
dialog.querySelector('input[name="webhook_msn"]').value = msn; |
| 773 |
dialog.showModal(); |
| 774 |
}); |
| 775 |
|
| 776 |
</script> |
| 777 |
|
| 778 |
<?php |
| 779 |
|
| 780 |
|
| 781 |
echo "</div>"; |
| 782 |
} |
| 783 |
|
| 784 |
// To be called in admin-post.php |
| 785 |
public function vipps_delete_webhook() { |
| 786 |
static::set_locale_if_in_header(); |
| 787 |
$ok = wp_verify_nonce($_REQUEST['webhook_nonce'],'webhook_nonce'); |
| 788 |
if (!$ok) { |
| 789 |
wp_die("Wrong nonce"); |
| 790 |
} |
| 791 |
if (!current_user_can('manage_woocommerce')) { |
| 792 |
wp_die(__('You don\'t have sufficient rights', 'woo-vipps')); |
| 793 |
} |
| 794 |
|
| 795 |
$msn = sanitize_title($_REQUEST['webhook_msn']); |
| 796 |
$id = sanitize_title($_REQUEST['webhook_id']); |
| 797 |
|
| 798 |
if ($msn && $id) { |
| 799 |
$this->gateway()->api->delete_webhook($msn, $id); |
| 800 |
} |
| 801 |
|
| 802 |
wp_safe_redirect(admin_url("admin.php?page=vipps_webhook_menu")); |
| 803 |
exit(); |
| 804 |
} |
| 805 |
|
| 806 |
// To be called in admin-post.php |
| 807 |
public function vipps_add_webhook() { |
| 808 |
static::set_locale_if_in_header(); |
| 809 |
$ok = wp_verify_nonce($_REQUEST['webhook_nonce'],'webhook_nonce'); |
| 810 |
if (!$ok) { |
| 811 |
wp_die("Wrong nonce"); |
| 812 |
} |
| 813 |
if (!current_user_can('manage_woocommerce')) { |
| 814 |
wp_die(__('You don\'t have sufficient rights', 'woo-vipps')); |
| 815 |
} |
| 816 |
|
| 817 |
$msn = sanitize_title($_REQUEST['webhook_msn']); |
| 818 |
$url = sanitize_url($_REQUEST['webhook_url']); |
| 819 |
$events = []; |
| 820 |
foreach(explode(",", $_REQUEST['webhook_events']) as $event) { |
| 821 |
$events[] = $event; |
| 822 |
} |
| 823 |
if (!empty($events) && $msn && $url) { |
| 824 |
$this->gateway()->api->register_webhook($msn, $url, $events); |
| 825 |
} |
| 826 |
|
| 827 |
wp_safe_redirect(admin_url("admin.php?page=vipps_webhook_menu")); |
| 828 |
exit(); |
| 829 |
} |
| 830 |
|
| 831 |
public function badge_menu_page () { |
| 832 |
if (!current_user_can('manage_woocommerce')) { |
| 833 |
wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps')); |
| 834 |
} |
| 835 |
wp_enqueue_script('vipps-onsite-messageing'); |
| 836 |
|
| 837 |
$badge_options = get_option('vipps_badge_options'); |
| 838 |
|
| 839 |
// Get current brand and language |
| 840 |
$current_brand = strtolower($this->get_payment_method_name()); |
| 841 |
$current_language = $this->get_customer_language(); |
| 842 |
if ('se' === $current_language) $current_language = 'sv'; |
| 843 |
// Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13 |
| 844 |
if ('dk' === $current_language) $current_language = 'da'; |
| 845 |
|
| 846 |
$variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'), |
| 847 |
'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'), |
| 848 |
'purple'=> __('Purple', 'woo-vipps')]; |
| 849 |
|
| 850 |
?> |
| 851 |
<div class='wrap vipps-badge-settings'> |
| 852 |
|
| 853 |
<h1><?php echo sprintf(__('%1$s On-Site Messaging', 'woo-vipps'), Vipps::CompanyName()); ?></h1> |
| 854 |
|
| 855 |
<h3><?php echo sprintf(__('%1$s On-Site Messaging contains <em>badges</em> in different variants that can be used to let your customers know that %1$s payment is accepted.', 'woo-vipps'), Vipps::CompanyName()); ?></h3> |
| 856 |
|
| 857 |
<p> |
| 858 |
<?php _e('You can configure these badges on this page, turning them on in all or some products and configure their default setup. You can also add a badge using a shortcode or a Block', 'woo-vipps'); ?> |
| 859 |
</p> |
| 860 |
|
| 861 |
<h2> <?php _e('Settings', 'woo-vipps'); ?></h2> |
| 862 |
<form class="vipps-badge-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST"> |
| 863 |
<input type="hidden" name="action" value="update_vipps_badge_settings" /> |
| 864 |
<?php wp_nonce_field( 'badgeaction', 'badgenonce'); ?> |
| 865 |
<div> |
| 866 |
<label for="badgeon"><?php echo sprintf(__('Turn on support for %1$s On-site Messaging badges', 'woo-vipps'), Vipps::CompanyName()); ?></label> |
| 867 |
<input type="hidden" name="badgeon" value="0" /> |
| 868 |
<input <?php if (@$badge_options['badgeon']) echo " checked "; ?> value="1" type="checkbox" id="badgeon" name="badgeon" /> |
| 869 |
</div> |
| 870 |
|
| 871 |
<div> |
| 872 |
<label for="defaultall"><?php _e('Add badge to all products by default', 'woo-vipps'); ?></label> |
| 873 |
<input type="hidden" name="defaultall" value="0" /> |
| 874 |
<input <?php if (@$badge_options['defaultall']) echo " checked "; ?> value="1" type="checkbox" id="defaultall" name="defaultall" /> |
| 875 |
<p><?php echo sprintf(__("If selected, all products will get a badge, but you can override this on the %1\$s tab on the product data page. If not, it's the other way around. You can also choose a particular variant on that page", 'woo-vipps'), Vipps::CompanyName()); ?></p> |
| 876 |
</div> |
| 877 |
<p id="badgeholder" style="font-size:1.5rem"> |
| 878 |
<vipps-mobilepay-badge id="vipps-badge-demo" |
| 879 |
brand="<?php echo esc_attr($current_brand); ?>" |
| 880 |
language="<?php echo esc_attr($current_language); ?>" |
| 881 |
<?php if (@$badge_options['variant']) echo ' variant="' . esc_attr($badge_options['variant']) . '" ' ?> |
| 882 |
></vipps-mobilepay-badge> |
| 883 |
</p> |
| 884 |
|
| 885 |
<div> |
| 886 |
<label for="vippsBadgeVariant"><?php _e('Variant', 'woo-vipps'); ?></label> |
| 887 |
|
| 888 |
<select id=vippsBadgeVariant name="variant" onChange='changeVariant()'> |
| 889 |
<option value=""><?php _e('Choose color variant:', 'woo-vipps'); ?></option> |
| 890 |
<?php foreach($variants as $key=>$name): ?> |
| 891 |
<option value="<?php echo $key; ?>" <?php if (@$badge_options['variant'] == $key) echo " selected "; ?> > |
| 892 |
<?php echo $name ; ?> |
| 893 |
</option> |
| 894 |
<?php endforeach; ?> |
| 895 |
</select> |
| 896 |
|
| 897 |
<div> |
| 898 |
<input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" /> |
| 899 |
</div> |
| 900 |
|
| 901 |
</form> |
| 902 |
|
| 903 |
<h2><?php _e('The Gutenberg Block', 'woo-vipps'); ?></h2> |
| 904 |
<p><?php echo sprintf(__('If you use Gutenberg, you should be able to add a %1$s Badge block wherever you need it. It is called %1$s On-Site Messaging Badge Block.', 'woo-vipps'), Vipps::CompanyName()); ?> |
| 905 |
|
| 906 |
<h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2> |
| 907 |
<p><?php echo sprintf(__('If you need to add a %1$s badge on a specific page, footer, header and so on, and you cannot use the Gutenberg Block provided for this, you can either add the %1$s Badge manually (as <a href="%2$s" nofollow rel=nofollow target=_blank>documented here</a>) or you can use the shortcode.', 'woo-vipps'), Vipps::CompanyName(), "https://developer.vippsmobilepay.com/docs/knowledge-base/design-guidelines/on-site-messaging/"); ?></p> |
| 908 |
<br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br> |
| 909 |
<pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br> |
| 910 |
<?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br> |
| 911 |
<?php _e("The brand will be automatically applied.", 'woo-vipps'); ?> |
| 912 |
</p> |
| 913 |
|
| 914 |
</div> |
| 915 |
<script> |
| 916 |
function changeVariant() { |
| 917 |
const badge = document.getElementById('vipps-badge-demo'); |
| 918 |
const variantSelector = document.getElementById('vippsBadgeVariant'); |
| 919 |
const variant = variantSelector.options[variantSelector.selectedIndex].value; |
| 920 |
|
| 921 |
// Just update the variant attribute, preserving brand and language |
| 922 |
badge.setAttribute('variant', variant); |
| 923 |
} |
| 924 |
</script> |
| 925 |
<?php |
| 926 |
} |
| 927 |
|
| 928 |
public function update_button_settings () { |
| 929 |
$ok = wp_verify_nonce($_REQUEST['buttonnonce'],'buttonaction'); |
| 930 |
if (!$ok) { |
| 931 |
wp_die("Wrong nonce"); |
| 932 |
} |
| 933 |
if (!current_user_can('manage_woocommerce')) { |
| 934 |
echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps'))); |
| 935 |
wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps')); |
| 936 |
} |
| 937 |
|
| 938 |
$old = get_option('vipps_button_options2', []); |
| 939 |
$new = $old; |
| 940 |
if (isset($_POST['express']['configs'])) { |
| 941 |
foreach ($_POST['express']['configs'] as $ctx => $config) { |
| 942 |
$sanitized_ctx = sanitize_title($ctx); |
| 943 |
$sanitized_config = map_deep($config, 'sanitize_title'); |
| 944 |
|
| 945 |
// If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25 |
| 946 |
if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) { |
| 947 |
$new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true]; |
| 948 |
} else { |
| 949 |
$new['express']['configs'][$sanitized_ctx] = $sanitized_config; |
| 950 |
} |
| 951 |
} |
| 952 |
} |
| 953 |
update_option('vipps_button_options2', $new); |
| 954 |
wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu")); |
| 955 |
exit(); |
| 956 |
} |
| 957 |
|
| 958 |
public function update_badge_settings () { |
| 959 |
static::set_locale_if_in_header(); |
| 960 |
$ok = wp_verify_nonce($_REQUEST['badgenonce'],'badgeaction'); |
| 961 |
if (!$ok) { |
| 962 |
wp_die("Wrong nonce"); |
| 963 |
} |
| 964 |
if (!current_user_can('manage_woocommerce')) { |
| 965 |
echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps'))); |
| 966 |
wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps')); |
| 967 |
} |
| 968 |
|
| 969 |
$current = get_option('vipps_badge_options'); |
| 970 |
if (isset($_POST['badgeon'])) { |
| 971 |
$current['badgeon'] = intval($_POST['badgeon']); |
| 972 |
} |
| 973 |
if (isset($_POST['defaultall'])) { |
| 974 |
$current['defaultall'] = intval($_POST['defaultall']); |
| 975 |
} |
| 976 |
if (isset($_POST['variant'])) { |
| 977 |
$current['variant'] = sanitize_title($_POST['variant']); |
| 978 |
} |
| 979 |
|
| 980 |
update_option('vipps_badge_options', $current); |
| 981 |
wp_safe_redirect(admin_url("admin.php?page=vipps_badge_menu")); |
| 982 |
exit(); |
| 983 |
} |
| 984 |
|
| 985 |
public function vipps_mobilepay_badge_shortcode($atts) { |
| 986 |
$args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts ); |
| 987 |
|
| 988 |
$variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : ""; |
| 989 |
$language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language(); |
| 990 |
if ('se' === $language) $language = 'sv'; |
| 991 |
// Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11 |
| 992 |
if ('dk' === $language) $language = 'da'; |
| 993 |
$id = sanitize_title($args['id']); |
| 994 |
$class = sanitize_text_field($args['class']); |
| 995 |
|
| 996 |
$attributes = []; |
| 997 |
$attributes['brand'] = strtolower($this->get_payment_method_name()); |
| 998 |
if ($variant) $attributes['variant'] = $variant; |
| 999 |
if ($language) $attributes['language'] = $language; |
| 1000 |
if ($id) $attributes['id'] = $id; |
| 1001 |
if ($class) $attributes['class'] = $class; |
| 1002 |
|
| 1003 |
$badgeatts = ""; |
| 1004 |
foreach($attributes as $key=>$value) $badgeatts .= " $key=\"" . esc_attr($value) . '"'; |
| 1005 |
|
| 1006 |
return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>"; |
| 1007 |
} |
| 1008 |
|
| 1009 |
// legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024 |
| 1010 |
// Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11 |
| 1011 |
public function vipps_badge_shortcode($atts) { |
| 1012 |
$args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts ); |
| 1013 |
|
| 1014 |
$variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : ""; |
| 1015 |
$language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language(); |
| 1016 |
if ('se' === $language) $language = 'sv'; |
| 1017 |
// Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11 |
| 1018 |
if ('dk' === $language) $language = 'da'; |
| 1019 |
|
| 1020 |
$id = sanitize_title($args['id']); |
| 1021 |
$class = sanitize_text_field($args['class']); |
| 1022 |
|
| 1023 |
$attributes = []; |
| 1024 |
if ($variant) $attributes['variant'] = $variant; |
| 1025 |
if ($language) $attributes['language'] = $language; |
| 1026 |
if ($id) $attributes['id'] = $id; |
| 1027 |
if ($class) $attributes['class'] = $class; |
| 1028 |
|
| 1029 |
$badgeatts = ""; |
| 1030 |
foreach($attributes as $key=>$value) $badgeatts .= " $key=\"" . esc_attr($value) . '"'; |
| 1031 |
|
| 1032 |
return "<vipps-badge $badgeatts></vipps-badge>"; |
| 1033 |
} |
| 1034 |
|
| 1035 |
public function get_html_button_default_attrs() { |
| 1036 |
return [ |
| 1037 |
'language' => 'store', |
| 1038 |
'variant' => 'primary', |
| 1039 |
'rounded' => 'false', |
| 1040 |
'verb' => 'buy', |
| 1041 |
'stretched' => 'false', |
| 1042 |
'compact' => 'false', |
| 1043 |
'brand' => strtolower($this->get_payment_method_name()), // NB: if setting wp option, you need to remember to unset this value so it's dynamic. LP 2026-07-01 |
| 1044 |
]; |
| 1045 |
} |
| 1046 |
|
| 1047 |
public function get_html_button_attrs_for_context($context = 'global') { |
| 1048 |
$options = get_option('vipps_button_options2', []); |
| 1049 |
if (!is_string($context)) $context = 'global'; |
| 1050 |
$config = $options['express']['configs'][$context] ?? []; |
| 1051 |
if (!$config || ($config['use-global-config'] ?? false)) { |
| 1052 |
$config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs(); |
| 1053 |
} |
| 1054 |
return $config; |
| 1055 |
} |
| 1056 |
|
| 1057 |
public function get_html_button_for_context($context = 'global') { |
| 1058 |
return $this->get_html_button($this->get_html_button_attrs_for_context($context)); |
| 1059 |
} |
| 1060 |
|
| 1061 |
// Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24 |
| 1062 |
// See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/ |
| 1063 |
public function get_html_button($attrs = []) { |
| 1064 |
$payment_method = $this->get_payment_method_name(); |
| 1065 |
$attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs()); |
| 1066 |
$attrs['brand'] = strtolower($payment_method); |
| 1067 |
$attrs['type'] = 'button'; // static |
| 1068 |
|
| 1069 |
// Support using store language |
| 1070 |
if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language(); |
| 1071 |
// Don't support these login verbs. LP 2026-06-04 |
| 1072 |
if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy'; |
| 1073 |
// Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11 |
| 1074 |
if ('dk' === $attrs['language']) $attrs['language'] = 'da'; |
| 1075 |
|
| 1076 |
$escaped_attrs = []; |
| 1077 |
foreach($attrs as $k => $v) { |
| 1078 |
$escaped_attrs[$k] = esc_attr($v); |
| 1079 |
} |
| 1080 |
|
| 1081 |
// id attribute |
| 1082 |
$id = $escaped_attrs['id'] ?? ''; |
| 1083 |
$id_str = $id ? "id='$id'" : ''; |
| 1084 |
|
| 1085 |
// class attribute |
| 1086 |
$class_str = ''; |
| 1087 |
if (isset($attrs['class'])) { |
| 1088 |
if (is_array($attrs['class'])) { |
| 1089 |
$class_str = implode(' ', $attrs['class']); |
| 1090 |
} else if (is_string($attrs['class'])) { |
| 1091 |
$class_str = $attrs['class']; |
| 1092 |
} |
| 1093 |
} |
| 1094 |
|
| 1095 |
// The html |
| 1096 |
$html = <<<EOF |
| 1097 |
<vipps-mobilepay-button |
| 1098 |
$id_str |
| 1099 |
$class_str |
| 1100 |
type="{$escaped_attrs['type']}" |
| 1101 |
brand="{$escaped_attrs['brand']}" |
| 1102 |
language="{$escaped_attrs['language']}" |
| 1103 |
variant="{$escaped_attrs['variant']}" |
| 1104 |
rounded="{$escaped_attrs['rounded']}" |
| 1105 |
verb="{$escaped_attrs['verb']}" |
| 1106 |
stretched="{$escaped_attrs['stretched']}" |
| 1107 |
compact="{$escaped_attrs['compact']}" |
| 1108 |
></vipps-mobilepay-button> |
| 1109 |
EOF; |
| 1110 |
return apply_filters('woo_vipps_html_button', $html, $attrs); |
| 1111 |
} |
| 1112 |
|
| 1113 |
public function button_menu_page() { |
| 1114 |
if (!current_user_can('manage_woocommerce')) { |
| 1115 |
wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps')); |
| 1116 |
} |
| 1117 |
wp_enqueue_script('vipps-button-webcomponent'); |
| 1118 |
?> |
| 1119 |
<div class='wrap vipps-button-settings'> |
| 1120 |
<h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1> |
| 1121 |
<span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span> |
| 1122 |
<form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST"> |
| 1123 |
<input type="hidden" name="action" value="update_vipps_button_settings" /> |
| 1124 |
<?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?> |
| 1125 |
|
| 1126 |
<!-- Express section --> |
| 1127 |
<?php $this->button_menu_express_section(); ?> |
| 1128 |
|
| 1129 |
<!-- submit button --> |
| 1130 |
<div id="vipps-button-settings-save"> |
| 1131 |
<input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" /> |
| 1132 |
</div> |
| 1133 |
</form> |
| 1134 |
</div> |
| 1135 |
<?php |
| 1136 |
} |
| 1137 |
|
| 1138 |
private function button_menu_express_section() { |
| 1139 |
$options = get_option('vipps_button_options2', []); |
| 1140 |
$express = $options['express'] ?? []; |
| 1141 |
$configs = $express['configs'] ?? []; |
| 1142 |
$contexts = [ |
| 1143 |
'global' => __('Global', 'woo-vipps'), |
| 1144 |
'product' => __('Product', 'woo-vipps'), |
| 1145 |
'catalog' => __('Catalog', 'woo-vipps'), |
| 1146 |
'cart' => __('Cart', 'woo-vipps'), |
| 1147 |
'minicart' => __('Mini cart', 'woo-vipps'), |
| 1148 |
'checkout' => __('Checkout', 'woo-vipps'), |
| 1149 |
]; |
| 1150 |
$init_context = 'global'; |
| 1151 |
$init_config = $configs[$init_context] ?? []; |
| 1152 |
|
| 1153 |
// html button args |
| 1154 |
$init_args = $init_config; |
| 1155 |
$init_args['id'] = 'vipps-button-express-preview'; |
| 1156 |
|
| 1157 |
?> |
| 1158 |
<div class="vipps-button-settings-section" id="vipps-button-settings-express-container"> |
| 1159 |
<h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2> |
| 1160 |
|
| 1161 |
<!-- Context dropdown --> |
| 1162 |
<div id="vipps-button-settings-express-context"> |
| 1163 |
<label> |
| 1164 |
<?php _e('Config context', 'woo-vipps'); ?> |
| 1165 |
</label> |
| 1166 |
<select id="context" onChange='updateContext()'> |
| 1167 |
<?php foreach($contexts as $key => $label): ?> |
| 1168 |
<option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> > |
| 1169 |
<?php echo $label ; ?> |
| 1170 |
</option> |
| 1171 |
<?php endforeach; ?> |
| 1172 |
</select> |
| 1173 |
<label class="hidden" id="use-global-config-container"><input onchange="updateContext()" type="checkbox" name="express[tmpConfig][use-global-config]" checked><?php _e('Use global config', 'woo-vipps'); ?></label> |
| 1174 |
</div> |
| 1175 |
|
| 1176 |
|
| 1177 |
<!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily. |
| 1178 |
On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 --> |
| 1179 |
<div class="vipps-button-settings-section" id="vipps-button-settings-express-args"> |
| 1180 |
<fieldset> |
| 1181 |
<label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label> |
| 1182 |
<label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label> |
| 1183 |
<label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label> |
| 1184 |
</fieldset> |
| 1185 |
<fieldset> |
| 1186 |
<legend><?php _e('Language', 'woo-vipps'); ?></legend> |
| 1187 |
<label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label> |
| 1188 |
<label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label> |
| 1189 |
<label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label> |
| 1190 |
<label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label> |
| 1191 |
<label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label> |
| 1192 |
<?php if ($this->get_payment_method_name() === 'MobilePay'): ?> |
| 1193 |
<label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label> |
| 1194 |
<?php endif; ?> |
| 1195 |
</fieldset> |
| 1196 |
|
| 1197 |
<?php if ($this->get_payment_method_name() !== 'MobilePay'): ?> |
| 1198 |
<p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p> |
| 1199 |
<?php endif; ?> |
| 1200 |
|
| 1201 |
<fieldset> |
| 1202 |
<legend><?php _e('Verb', 'woo-vipps'); ?></legend> |
| 1203 |
<label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label> |
| 1204 |
<label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label> |
| 1205 |
<label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label> |
| 1206 |
<label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label> |
| 1207 |
<label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label> |
| 1208 |
<label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label> |
| 1209 |
</fieldset> |
| 1210 |
<fieldset> |
| 1211 |
<legend><?php _e('Variant', 'woo-vipps'); ?></legend> |
| 1212 |
<label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label> |
| 1213 |
<label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label> |
| 1214 |
<label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label> |
| 1215 |
</fieldset> |
| 1216 |
</div> |
| 1217 |
|
| 1218 |
<!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 --> |
| 1219 |
<?php echo $this->get_html_button($init_args); ?> |
| 1220 |
</div> |
| 1221 |
|
| 1222 |
<script> |
| 1223 |
// When inputs change, update the preview args. LP 2026-06-24 |
| 1224 |
jQuery('#vipps-button-settings-express-args input').on('click', updatePreview); |
| 1225 |
|
| 1226 |
let currentContext = '<?php echo $init_context; ?>'; |
| 1227 |
let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24 |
| 1228 |
|
| 1229 |
// Updates the actual html inputs from given config. LP 2026-07-01 |
| 1230 |
function setInputsFromConfig(context, config) { |
| 1231 |
const useGlobalConfig = Boolean(config?.["use-global-config"]); |
| 1232 |
const isGlobal = "global" === context; |
| 1233 |
|
| 1234 |
// Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26 |
| 1235 |
jQuery('#use-global-config-container').toggleClass('hidden', isGlobal); |
| 1236 |
|
| 1237 |
// Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26 |
| 1238 |
if (!config || (!isGlobal && useGlobalConfig)) { |
| 1239 |
config = contextConfigs["global"]; |
| 1240 |
|
| 1241 |
jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true); |
| 1242 |
|
| 1243 |
// When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26 |
| 1244 |
jQuery('#vipps-button-settings-express-args input').prop("disabled", true); |
| 1245 |
} else { |
| 1246 |
jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false); |
| 1247 |
jQuery('#vipps-button-settings-express-args input').prop("disabled", false); |
| 1248 |
} |
| 1249 |
|
| 1250 |
Object.entries(config).forEach(([key, val]) => { |
| 1251 |
if ("use-global-config" === key) return; |
| 1252 |
const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`); |
| 1253 |
const type = inputs.prop('type'); |
| 1254 |
switch (type) { |
| 1255 |
case "checkbox": |
| 1256 |
inputs.prop('checked', typeof val === "boolean" ? val : "true" === val); |
| 1257 |
break; |
| 1258 |
case "radio": |
| 1259 |
inputs.filter(`[value="${val}"]`).prop('checked', true); |
| 1260 |
break; |
| 1261 |
default: |
| 1262 |
console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`); |
| 1263 |
} |
| 1264 |
}); |
| 1265 |
|
| 1266 |
updatePreview(); |
| 1267 |
} |
| 1268 |
// init the starting config from option. LP 2026-06-25 |
| 1269 |
setInputsFromConfig(currentContext, contextConfigs[currentContext]); |
| 1270 |
|
| 1271 |
// Update the preview web component's attributes. LP 2026-06-24 |
| 1272 |
function updatePreview(event) { |
| 1273 |
const args = getPreviewArgs(); |
| 1274 |
// FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02 |
| 1275 |
// if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>'; |
| 1276 |
if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>'; |
| 1277 |
const button = jQuery('#vipps-button-express-preview'); |
| 1278 |
button.attr(args); |
| 1279 |
} |
| 1280 |
|
| 1281 |
function getPreviewArgs() { |
| 1282 |
const args = {}; |
| 1283 |
jQuery('#vipps-button-settings-express-args input').each(function () { |
| 1284 |
// inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24 |
| 1285 |
const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)]; |
| 1286 |
const attr = matches.length ? matches[matches.length - 1][1] : null; |
| 1287 |
if (!attr) { |
| 1288 |
console.error("woo-vipps: Could not extract attribute name for button preview:", this); |
| 1289 |
return; |
| 1290 |
} |
| 1291 |
if (this.type === 'checkbox') { |
| 1292 |
args[attr] = this.checked; |
| 1293 |
} else if (this.checked) { |
| 1294 |
args[attr] = this.value; |
| 1295 |
} |
| 1296 |
}); |
| 1297 |
|
| 1298 |
return args; |
| 1299 |
} |
| 1300 |
|
| 1301 |
// Stores selected config for context and switches to another (if changed). LP 2026-07-01 |
| 1302 |
function updateContext() { |
| 1303 |
const wasGlobal = "global" === currentContext; |
| 1304 |
const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked"); |
| 1305 |
|
| 1306 |
// Store config to global, unless its a non-global context that uses global config. LP 2026-06-25 |
| 1307 |
if (wasGlobal || !useGlobalConfig) { |
| 1308 |
contextConfigs[currentContext] = getPreviewArgs(); |
| 1309 |
} else { |
| 1310 |
contextConfigs[currentContext] = {'use-global-config': true}; |
| 1311 |
} |
| 1312 |
|
| 1313 |
// Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25 |
| 1314 |
const newContext = jQuery("#context").val(); |
| 1315 |
const newConfig = contextConfigs[newContext]; |
| 1316 |
setInputsFromConfig(newContext, newConfig); |
| 1317 |
currentContext = newContext; |
| 1318 |
} |
| 1319 |
|
| 1320 |
// Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24 |
| 1321 |
jQuery('#vipps-button-settings-form').on('formdata', e => { |
| 1322 |
const formData = e?.originalEvent?.formData; |
| 1323 |
if (!formData) return; |
| 1324 |
|
| 1325 |
// run this to store current context config before posting. LP 2026-06-24 |
| 1326 |
updateContext(); |
| 1327 |
|
| 1328 |
// now we can delete the current temporary config from post data. LP 2026-06-24 |
| 1329 |
const keysToDelete = []; |
| 1330 |
for (const [key] of formData.entries()) { |
| 1331 |
if (key.startsWith("express[tmpConfig][")) { |
| 1332 |
keysToDelete.push(key); |
| 1333 |
} |
| 1334 |
} |
| 1335 |
keysToDelete.forEach(key => formData.delete(key)); |
| 1336 |
|
| 1337 |
// Now add the actual post data from the stored global contextConfigs. LP 2026-06-24 |
| 1338 |
Object.entries(contextConfigs).forEach(([context, config]) => { |
| 1339 |
Object.entries(config).forEach(([key, val]) => { |
| 1340 |
formData.append(`express[configs][${context}][${key}]`, val); |
| 1341 |
}); |
| 1342 |
}); |
| 1343 |
}); |
| 1344 |
</script> |
| 1345 |
<?php |
| 1346 |
} |
| 1347 |
|
| 1348 |
|
| 1349 |
public function admin_menu_page () { |
| 1350 |
$flavour = sanitize_title($this->get_payment_method_name()); |
| 1351 |
|
| 1352 |
// The function which is hooked in to handle the output of the page must check that the user has the required capability as well. (manage_woocommerce) |
| 1353 |
if (!current_user_can('manage_woocommerce')) { |
| 1354 |
wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps')); |
| 1355 |
} |
| 1356 |
|
| 1357 |
$recurringsettings = admin_url('/admin.php?page=wc-settings&tab=checkout§ion=vipps_recurring'); |
| 1358 |
$checkoutsettings = admin_url('/admin.php?page=vipps_settings_menu'); |
| 1359 |
$loginsettings = admin_url('options-general.php?page=vipps_login_settings'); |
| 1360 |
|
| 1361 |
$logininstall = admin_url('/plugin-install.php?s=login-with-vipps&tab=search&type=term'); |
| 1362 |
$subscriptioninstall = 'https://woocommerce.com/products/woocommerce-subscriptions/'; |
| 1363 |
|
| 1364 |
$logspage = admin_url('/admin.php?page=wc-status&tab=logs'); |
| 1365 |
$forumpage = 'https://wordpress.org/support/plugin/woo-vipps/'; |
| 1366 |
|
| 1367 |
$portalurl = 'https://portal.vippsmobilepay.com'; |
| 1368 |
|
| 1369 |
$installed = get_plugins(); |
| 1370 |
|
| 1371 |
$recurringinstalled = array_key_exists('vipps-recurring-payments-gateway-for-woocommerce/woo-vipps-recurring.php',$installed); |
| 1372 |
$recurringactive = class_exists('WC_Vipps_Recurring'); |
| 1373 |
$recurringstandalone = $recurringactive && !(defined('WC_VIPPS_RECURRING_INTEGRATED') && WC_VIPPS_RECURRING_INTEGRATED); |
| 1374 |
$deactivatelink = admin_url("plugins.php?s=vipps-recurring-payments-gateway-for-woocommerce"); |
| 1375 |
|
| 1376 |
$logininstalled = array_key_exists('login-with-vipps/login-with-vipps.php', $installed); |
| 1377 |
$loginactive = class_exists('ContinueWithVipps'); |
| 1378 |
$slogan = __('- very, very simple', 'woo-vipps'); |
| 1379 |
|
| 1380 |
|
| 1381 |
$gw = $this->gateway(); |
| 1382 |
$configured = get_option('woo-vipps-configured', false); |
| 1383 |
$isactive = ($gw->enabled == 'yes'); |
| 1384 |
$istestmode = $gw->is_test_mode(); |
| 1385 |
$ischeckout = false; |
| 1386 |
if ($isactive) { |
| 1387 |
$ischeckout = ($gw->get_option('vipps_checkout_enabled') == 'yes'); |
| 1388 |
} |
| 1389 |
|
| 1390 |
if (WC_Gateway_Vipps::instance()->get_payment_method_name() != "Vipps"): |
| 1391 |
?> |
| 1392 |
<style>.notice.notice-vipps.test-mode { display: none; }body.wp-admin.toplevel_page_vipps_admin_menu #wpcontent {background-color: white; }</style> |
| 1393 |
<header class="vipps-admin-page-header <?php echo esc_attr($flavour); ?>" style="padding-top: 3.5rem ; line-height: 30px;"> |
| 1394 |
<h1><?php echo esc_html(Vipps::CompanyName()); ?> <?php echo esc_html($slogan); ?></h1> |
| 1395 |
</header> |
| 1396 |
<div class='wrap vipps-admin-page'> |
| 1397 |
<div id="vipps_page_vipps_banners"><?php echo apply_filters('woo_vipps_vipps_page_banners', ""); ?></div> |
| 1398 |
<h1><?php echo sprintf(__("%1\$s for WordPress and WooCommerce", 'woo-vipps'), Vipps::CompanyName()); ?></h1> |
| 1399 |
<div class="pluginsection woo-vipps"> |
| 1400 |
|
| 1401 |
<p><?php echo sprintf(__("This plugin gives you %1\$s in WooCommerce, either as a fully fledged Checkout, or as a flexible payment method.",'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p> |
| 1402 |
<p><?php echo sprintf(__("With Checkout, you’ll also get access to shipping addresses, shipping selection and other payment options. Currently Checkout supports %1\$s and bank transfer; VISA and MasterCard payments will be added later.",'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p> |
| 1403 |
|
| 1404 |
<p><strong><?php echo sprintf(__("NB! Checkout for MobilePay is currently in beta mode; Bank Transfer has limited availability", 'woo-vipps')); ?></strong></p> |
| 1405 |
|
| 1406 |
<p><?php echo sprintf(__("Configure the plugin on its <a href='%1\$s'>settings page</a> and get your keys from the <a target='_blank' href='%2\$s'>%3\$s portal</a>.",'woo-vipps'), $checkoutsettings, $portalurl, Vipps::CompanyName());?></p> |
| 1407 |
<p><?php echo sprintf(__("If you experience problems or unexpected results, please check the 'fatal-errors' and 'woo-vipps' logs at <a href='%1\$s'>WooCommerce logs page</a>.", 'woo-vipps'), $logspage); ?></p> |
| 1408 |
<p><?php echo sprintf(__("If you need support, please use the <a href='%1\$s'>forum page</a> for the plugin. If you cannot post your question publicly, contact WP-Hosting directly at support@wp-hosting.no.", 'woo-vipps'), $forumpage); ?></p> |
| 1409 |
<div class="pluginstatus vipps_admin_highlighted_section <?php echo esc_attr($flavour); ?>"> |
| 1410 |
<?php if ($istestmode): ?> |
| 1411 |
<p><b> |
| 1412 |
<?php echo sprintf(__('%1$s is currently in test mode - no real transactions will occur.', 'woo-vipps'), Vipps::CompanyName()); ?> |
| 1413 |
</b></p> |
| 1414 |
<?php endif; ?> |
| 1415 |
<p> |
| 1416 |
<?php if ($configured): ?> |
| 1417 |
<?php echo sprintf(__("<a href='%1\$s'>%2\$s configuration</a> is complete.", 'woo-vipps'), $checkoutsettings, Vipps::CompanyName()); ?> |
| 1418 |
<?php else: ?> |
| 1419 |
<?php echo sprintf(__("%1\$s configuration is not yet complete - you must get your keys from the %1\$s portal and enter them on the <a href='%2\$s'>settings page</a>", 'woo-vipps'), Vipps::CompanyName(), $checkoutsettings); ?> |
| 1420 |
<?php endif; ?> |
| 1421 |
</p> |
| 1422 |
<?php if ($isactive): ?> |
| 1423 |
<p> |
| 1424 |
<?php echo sprintf(__("The plugin is <b>active</b> - %1\$s is available as a payment method.", 'woo-vipps'), Vipps::CompanyName()); ?> |
| 1425 |
<?php if ($ischeckout): ?> |
| 1426 |
</p> |
| 1427 |
<p> |
| 1428 |
<?php echo sprintf(__("You are now using <b>%1\$s Checkout</b> instead of the standard WooCommerce Checkout page.", 'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?> |
| 1429 |
<?php endif; ?> |
| 1430 |
</p> |
| 1431 |
<?php else:; ?> |
| 1432 |
<?php endif; ?> |
| 1433 |
</div> |
| 1434 |
|
| 1435 |
</div> |
| 1436 |
</div> |
| 1437 |
|
| 1438 |
<?php else: ?> |
| 1439 |
|
| 1440 |
<style>.notice.notice-vipps.test-mode { display: none; }body.wp-admin.toplevel_page_vipps_admin_menu #wpcontent {background-color: white; }</style> |
| 1441 |
<header class="vipps-admin-page-header <?php echo esc_attr($flavour); ?>" style="padding-top: 3.5rem ; line-height: 30px;"> |
| 1442 |
<h1><?php echo esc_html(Vipps::CompanyName()); ?> <?php echo esc_html($slogan); ?></h1> |
| 1443 |
</header> |
| 1444 |
<div class='wrap vipps-admin-page'> |
| 1445 |
<div id="vipps_page_vipps_banners"><?php echo apply_filters('woo_vipps_vipps_page_banners', ""); ?></div> |
| 1446 |
<h1><?php echo sprintf(__("%1\$s for WordPress and WooCommerce", 'woo-vipps'), Vipps::CompanyName()); ?></h1> |
| 1447 |
<p><?php echo sprintf(__("%1\$s officially supports WordPress and WooCommerce with a family of plugins implementing a payment gateway for WooCommerce, a system for managing QR-codes that link to your products or landing pages, a plugin for recurring payments, and a system for passwordless logins.", 'woo-vipps'), Vipps::CompanyName());?></p> |
| 1448 |
<p><?php echo sprintf(__("To order or configure your %1\$s account that powers these plugins, log onto <a target='_blank' href='%2\$s'>the %1\$s portal</a> and use the keys and data from that to set up your plugins as needed.", 'woo-vipps'), Vipps::CompanyName(), $portalurl); ?></p> |
| 1449 |
|
| 1450 |
<h1><?php echo sprintf(__("The %1\$s plugins", 'woo-vipps'), Vipps::CompanyName()); ?></h1> |
| 1451 |
<div class="pluginsection woo-vipps"> |
| 1452 |
<h2><?php echo sprintf(__('Pay with %1$s for WooCommerce', 'woo-vipps' ), Vipps::CompanyName());?></h2> |
| 1453 |
<p><?php echo sprintf(__("This plugin implements a %1\$s checkout solution for WooCommerce and an alternate %1\$s hosted checkout that supports both %2\$s and credit cards. It also supports %1\$s's QR-api for creating QR-codes to your landing pages or products.", 'woo-vipps'), Vipps::CompanyName(), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?></p> |
| 1454 |
<p><?php echo sprintf(__("Configure the plugin on its <a href='%1\$s'>settings page</a> and get your keys from the <a target='_blank' href='%2\$s'>%3\$s portal</a>.",'woo-vipps'), $checkoutsettings, $portalurl, Vipps::CompanyName());?></p> |
| 1455 |
<p><?php echo sprintf(__("If you experience problems or unexpected results, please check the 'fatal-errors' and 'woo-vipps' logs at <a href='%1\$s'>WooCommerce logs page</a>.", 'woo-vipps'), $logspage); ?></p> |
| 1456 |
<p><?php echo sprintf(__("If you need support, please use the <a href='%1\$s'>forum page</a> for the plugin. If you cannot post your question publicly, contact WP-Hosting directly at support@wp-hosting.no.", 'woo-vipps'), $forumpage); ?></p> |
| 1457 |
<div class="pluginstatus vipps_admin_highlighted_section"> |
| 1458 |
<?php if ($istestmode): ?> |
| 1459 |
<p><b> |
| 1460 |
<?php echo sprintf(__('%1$s is currently in test mode - no real transactions will occur.', 'woo-vipps'), Vipps::CompanyName()); ?> |
| 1461 |
</b></p> |
| 1462 |
<?php endif; ?> |
| 1463 |
<p> |
| 1464 |
<?php if ($configured): ?> |
| 1465 |
<?php echo sprintf(__("<a href='%1\$s'>%2\$s configuration</a> is complete.", 'woo-vipps'), $checkoutsettings, Vipps::CompanyName()); ?> |
| 1466 |
<?php else: ?> |
| 1467 |
<?php echo sprintf(__("%1\$s configuration is not yet complete - you must get your keys from the %1\$s portal and enter them on the <a href='%2\$s'>settings page</a>", 'woo-vipps'), Vipps::CompanyName(), $checkoutsettings); ?> |
| 1468 |
<?php endif; ?> |
| 1469 |
</p> |
| 1470 |
<?php if ($isactive): ?> |
| 1471 |
<p> |
| 1472 |
<?php echo sprintf(__("The plugin is <b>active</b> - %1\$s is available as a payment method.", 'woo-vipps'), Vipps::CompanyName()); ?> |
| 1473 |
<?php if ($ischeckout): ?> |
| 1474 |
</p> |
| 1475 |
<p> |
| 1476 |
<?php echo sprintf(__("You are now using <b>%1\$s Checkout</b> instead of the standard WooCommerce Checkout page.", 'woo-vipps'), WC_Gateway_Vipps::instance()->get_payment_method_name()); ?> |
| 1477 |
<?php endif; ?> |
| 1478 |
</p> |
| 1479 |
<?php else:; ?> |
| 1480 |
<?php endif; ?> |
| 1481 |
</div> |
| 1482 |
|
| 1483 |
</div> |
| 1484 |
|
| 1485 |
<div class="pluginsection vipps-recurring"> |
| 1486 |
<h2><?php echo sprintf(__( 'Recurring Payments with %1$s', 'woo-vipps' ), Vipps::CompanyName());?></h2> |
| 1487 |
<p> |
| 1488 |
<?php echo sprintf(__("%1\$s supports recurring payments through the plugin <a href='%2\$s' target='_blank'>WooCommerce Subscriptions</a>. This support is written and supported by <a href='%3\$s' target='_blank'>Everyday</a>, and is perfect for you if you run a web shop with subscription based services or other products that would benefit from subscriptions.", 'woo-vipps'), Vipps::CompanyName(), 'https://woocommerce.com/products/woocommerce-subscriptions/', Vipps::CompanyName(), 'https://everyday.no/'); ?> |
| 1489 |
<?php do_action('vipps_page_vipps_recurring_payments_section'); ?> |
| 1490 |
<div class="pluginstatus vipps_admin_highlighted_section"> |
| 1491 |
<?php if ($recurringactive): ?> |
| 1492 |
<p> |
| 1493 |
<?php echo sprintf(__("Support for recurring payments with %1\$s is <b>active</b>. You can configure the plugin at its <a href='%2\$s'>settings page</a>.", 'woo-vipps'), Vipps::CompanyName(), $recurringsettings); ?> |
| 1494 |
</p> |
| 1495 |
<?php endif; ?> |
| 1496 |
<?php if (!$subscriptioninstall): ?> |
| 1497 |
<p> |
| 1498 |
<?php echo sprintf(__("This plugins support for recurring payments requires the plugin <a href='%1\$s' target='_blank'>WooCommerce Subscriptions</a>. You need to install and activate this first.", 'woo-vipps'), 'https://woocommerce.com/products/woocommerce-subscriptions/'); ?> |
| 1499 |
</p> |
| 1500 |
<?php endif; ?> |
| 1501 |
<?php if ($recurringactive && $recurringstandalone): ?> |
| 1502 |
<p> |
| 1503 |
<?php echo sprintf(__("Your support for recurring payments with %1\$s uses the legacy stand-alone plugin. This is no longer required, and you should <b><a href='%2\$s'>deactivate</a></b> this plugin, since development on this will soon cease.", 'woo-vipps'), Vipps::CompanyName(), esc_attr($deactivatelink));?> |
| 1504 |
</p> |
| 1505 |
|
| 1506 |
<?php endif; ?> |
| 1507 |
|
| 1508 |
</div> |
| 1509 |
|
| 1510 |
</div> |
| 1511 |
|
| 1512 |
<div class="pluginsection login-with-vipps"> |
| 1513 |
<h2><?php echo sprintf(__( '%1$s', 'woo-vipps' ), Vipps::LoginName());?></h2> |
| 1514 |
<p><?php echo sprintf(__("<a href='%1\$s' target='_blank'>%3\$s</a> is a password-less solution that lets you or your customers to securely log into your site without having to remember passwords - you only need the %2\$s app. The plugin does not require WooCommerce, and it can be customized for many different usecases.", 'woo-vipps'), 'https://www.wordpress.org/plugins/login-with-vipps/',Vipps::CompanyName(), Vipps::LoginName()); ?></p> |
| 1515 |
<p> |
| 1516 |
<?php echo sprintf(__("Remember, you need to set up %3\$s at the <a target='_blank' href='%2\$s'>%1\$s Portal</a>, where you will find the keys you need and where you will have to register the <em>return url</em> you will find on the settings page.", 'woo-vipps'),Vipps::CompanyName(),$portalurl, Vipps::LoginName()); ?> |
| 1517 |
</p> |
| 1518 |
|
| 1519 |
<div class="pluginstatus vipps_admin_highlighted_section"> |
| 1520 |
<?php if ($loginactive): ?> |
| 1521 |
<p> |
| 1522 |
<?php echo sprintf(__("%1\$s is installed and active. You can configure the plugin at its <a href='%2\$s'>settings page</a>", 'woo-vipps'),Vipps::LoginName(), $loginsettings); ?> |
| 1523 |
</p> |
| 1524 |
<?php elseif ($logininstalled): ?> |
| 1525 |
<p> |
| 1526 |
<?php echo sprintf(__("%1\$s is installed, but not active. Activate it on the <a href='%2\$s'>plugins page</a>", 'woo-vipps'), Vipps::LoginName(), admin_url("/plugins.php")); ?> |
| 1527 |
</p> |
| 1528 |
<?php else: ?> |
| 1529 |
<p> |
| 1530 |
<?php echo sprintf(__("%1\$s is not installed. You can install it <a href='%2\$s'>here!</a>", 'woo-vipps'), Vipps::LoginName(), $logininstall); ?> |
| 1531 |
</p> |
| 1532 |
<?php endif; ?> |
| 1533 |
</div> |
| 1534 |
|
| 1535 |
</div> |
| 1536 |
|
| 1537 |
</div> |
| 1538 |
|
| 1539 |
<?php endif; |
| 1540 |
} |
| 1541 |
|
| 1542 |
// Add a link to the settings page from the plugin list |
| 1543 |
public function plugin_action_links ($links) { |
| 1544 |
$link = '<a href="'.esc_attr(admin_url('/admin.php?page=vipps_settings_menu')). '">'.__('Settings', 'woo-vipps').'</a>'; |
| 1545 |
array_unshift( $links, $link); |
| 1546 |
return $links; |
| 1547 |
} |
| 1548 |
|
| 1549 |
|
| 1550 |
// Requested by Vipps: It is a feature of this plugin that a prefix is added to the order number, in order to make it possible to use several different stores |
| 1551 |
// that may use the same ordre number ranges. The prefix used to be just "Woo" by default, but Vipps felt it would be easier to respond to support request by |
| 1552 |
// (trying to) identify the store/site directly in the order prefix. So this does that: It creates a prefix "woo-" + 8 chars derived from the domain of the siteurl. |
| 1553 |
// The result should be "woo-abcdefgh-" which should leave 18 digits for the actual order number. IOK 2020-05-19 |
| 1554 |
public function generate_order_prefix() { |
| 1555 |
$parts = parse_url(site_url()); |
| 1556 |
if (!$parts) return 'Woo'; |
| 1557 |
$domain = explode(".", $parts['host'] ?? ''); |
| 1558 |
if (empty($domain)) return 'Woo'; |
| 1559 |
$first = strtolower($domain[0]); |
| 1560 |
$second = isset($domain[1]) ? $domain[1] : ''; |
| 1561 |
$key = 'Woo'; |
| 1562 |
// Select first part of domain unless that has no content, otherwise second. Default to Woo again. |
| 1563 |
if (in_array($first, array('www','test','dev','vdev')) && !empty($second)) { |
| 1564 |
$key = $second; |
| 1565 |
} else { |
| 1566 |
$key = $first; |
| 1567 |
} |
| 1568 |
// Use only 8 chars for the site. Try to make it so by dropping vowels, if that doesn't succeed, just chop it. |
| 1569 |
$key = $key; |
| 1570 |
$key = sanitize_title($key); |
| 1571 |
$len = strlen($key); |
| 1572 |
if ($len <= 8) return "woo-$key-"; |
| 1573 |
$kzk = preg_replace("/[aeiouæøåüö]/i","",$key); |
| 1574 |
if (strlen($kzk) <= 8) return "woo-$kzk-"; |
| 1575 |
return "woo-" . substr($key,0,8) . "-"; |
| 1576 |
} |
| 1577 |
|
| 1578 |
// Add a backend notice to stand out a bit, using a Vipps logo and the Vipps color for info-level messages. IOK 2020-02-16 |
| 1579 |
public function add_vipps_admin_notice ($text, $type='info',$key='', $extraclasses='') { |
| 1580 |
if ($key) { |
| 1581 |
$dismissed = get_option('_vipps_dismissed_notices'); |
| 1582 |
if (isset($dismissed[$key])) return; |
| 1583 |
} |
| 1584 |
add_action('admin_notices', function() use ($text,$type, $key, $extraclasses) { |
| 1585 |
$logo = plugins_url('img/vmp-logo.png',__FILE__); |
| 1586 |
$message = "<img style='height:40px;float:left;' src='$logo' alt='Vipps-logo'> $text"; |
| 1587 |
echo "<div class='notice notice-vipps notice-$type $extraclasses is-dismissible' data-key='" . esc_attr($key) . "'><p>$message</p></div>"; |
| 1588 |
}); |
| 1589 |
} |
| 1590 |
|
| 1591 |
|
| 1592 |
// This function will delete old orders that were cancelled before the Vipps action was completed. We keep them for |
| 1593 |
// 10 minutes so we can work with them in hooks and callbacks after they are cancelled. IOK 2019-10-22 |
| 1594 |
# protected function delete_old_cancelled_orders() { |
| 1595 |
public function delete_old_cancelled_orders() { |
| 1596 |
$limit = 30; |
| 1597 |
$cutoff = time() - 600; // Ten minutes old orders: Delete them |
| 1598 |
$oldorders = time() - (60*60*24*7); // Very old orders: Ignore them to make this work on sites with enormous order databases |
| 1599 |
$delenda = []; |
| 1600 |
|
| 1601 |
if ($this->useHPOS()) { |
| 1602 |
$args = array( |
| 1603 |
'status' => 'cancelled', |
| 1604 |
'limit' => $limit, |
| 1605 |
'date_modified' => "$oldorders...$cutoff", |
| 1606 |
'meta_query' => [[ 'key' => '_vipps_delendum', 'value' => 1 ]] |
| 1607 |
); |
| 1608 |
$delenda = wc_get_orders($args); |
| 1609 |
} else { |
| 1610 |
// Old-style orders, we'll just use SQL |
| 1611 |
global $wpdb; |
| 1612 |
$sql = $wpdb->prepare("SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm on (pm.post_id = p.ID AND pm.meta_key = '_vipps_delendum') WHERE p.post_type = 'shop_order' AND p.post_status = 'wc-cancelled' AND p.post_modified_gmt >= %s AND p.post_modified_gmt <= %s AND pm.meta_value = 1 LIMIT %d", |
| 1613 |
gmdate( 'Y-m-d H:i:s', $oldorders ), |
| 1614 |
gmdate( 'Y-m-d H:i:s', $cutoff ), |
| 1615 |
$limit |
| 1616 |
); |
| 1617 |
$order_ids = $wpdb->get_col($sql); |
| 1618 |
foreach($order_ids as $did) { |
| 1619 |
$d = wc_get_order($did); |
| 1620 |
if ($d && !is_wp_error($d)) { |
| 1621 |
$delenda[] = $d; |
| 1622 |
} |
| 1623 |
} |
| 1624 |
} |
| 1625 |
|
| 1626 |
foreach ($delenda as $del) { |
| 1627 |
// Delete only if there is no customer info for the order IOK 2022-10-12 |
| 1628 |
if (!$del->get_billing_email()) { |
| 1629 |
$del->delete(true); |
| 1630 |
} else { |
| 1631 |
// If we've gotten a billing email, don't delete this. IOK 2022-10-12 |
| 1632 |
$del->delete_meta_data('_vipps_delendum'); |
| 1633 |
} |
| 1634 |
} |
| 1635 |
} |
| 1636 |
|
| 1637 |
// This is called asynch/nonblocking on payment_complete |
| 1638 |
public function do_order_management() { |
| 1639 |
$orderid = isset($_POST['orderid']) ? $_POST['orderid'] : false; |
| 1640 |
$orderkey = isset($_POST['orderkey']) ? $_POST['orderkey'] : false; |
| 1641 |
if ($orderid && $orderkey) { |
| 1642 |
// This will keep running even if the request ends, and this method is called asynchrounously. |
| 1643 |
add_action('shutdown', function () use ($orderid, $orderkey) { WC_Gateway_Vipps::instance()->payment_complete_at_shutdown ($orderid, $orderkey); }); |
| 1644 |
http_response_code(200); |
| 1645 |
header('Content-Type: application/json; charset=utf-8'); |
| 1646 |
header("Content-length: 1"); |
| 1647 |
print "1"; |
| 1648 |
flush(); |
| 1649 |
} else { |
| 1650 |
http_response_code(403); |
| 1651 |
} |
| 1652 |
} |
| 1653 |
|
| 1654 |
|
| 1655 |
public function admin_head() { |
| 1656 |
// Add some styling to the Vipps product-meta box |
| 1657 |
$smile= plugins_url('img/vmp-logo.png',__FILE__); |
| 1658 |
?> |
| 1659 |
<style> |
| 1660 |
@media only screen and (max-width: 900px) { |
| 1661 |
#woocommerce-product-data ul.wc-tabs li.vipps_tab a:before { |
| 1662 |
background: url(<?php echo $smile ?>) center center no-repeat; |
| 1663 |
content: " " !important; |
| 1664 |
background-size: 20px 20px; |
| 1665 |
} |
| 1666 |
} |
| 1667 |
@media only screen and (min-width: 900px) { |
| 1668 |
#woocommerce-product-data ul.wc-tabs li.vipps_tab a:before { |
| 1669 |
background: url(<?php echo $smile ?>) center center no-repeat; |
| 1670 |
content: " " !important; |
| 1671 |
background-size:100%; |
| 1672 |
width:13px;height:13px;display:inline-block;line-height:1; |
| 1673 |
} |
| 1674 |
} |
| 1675 |
</style> |
| 1676 |
<?php |
| 1677 |
} |
| 1678 |
// Scripts used in the backend |
| 1679 |
public function admin_enqueue_scripts($hook) { |
| 1680 |
// Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02 |
| 1681 |
$this->script_add_vippslocale(); |
| 1682 |
|
| 1683 |
wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all'); |
| 1684 |
wp_enqueue_script('vipps-admin'); |
| 1685 |
|
| 1686 |
wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all'); |
| 1687 |
wp_enqueue_style('vipps-fonts'); |
| 1688 |
wp_enqueue_style('vipps-fonts',plugins_url('css/fonts.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/fonts.css"), 'all'); |
| 1689 |
} |
| 1690 |
|
| 1691 |
|
| 1692 |
public function admin_menu () { |
| 1693 |
// IOK 2023-12-01 replace old Vipps smile in larger contexts |
| 1694 |
// $logo= plugins_url('img/vipps-smile-orange.png',__FILE__); |
| 1695 |
$logo = plugins_url('img/vmp-logo.png', __FILE__); |
| 1696 |
require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php"); |
| 1697 |
$adminSettings = VippsAdminSettings::instance(); |
| 1698 |
|
| 1699 |
add_menu_page(sprintf(__("%1\$s", 'woo-vipps'), Vipps::CompanyName()), sprintf(__("%1\$s", 'woo-vipps'), Vipps::CompanyName()), 'manage_woocommerce', 'vipps_admin_menu', array($this, 'admin_menu_page'), $logo, 58); |
| 1700 |
|
| 1701 |
add_submenu_page( 'vipps_admin_menu', __('Settings', 'woo-vipps'), __('Settings', 'woo-vipps'), 'manage_woocommerce', 'vipps_settings_menu', array($adminSettings, 'init_admin_settings_page_react_ui'), 90); |
| 1702 |
|
| 1703 |
if (class_exists('WC_Vipps_Recurring') && class_exists('WC_Subscriptions_Plugin')) { |
| 1704 |
add_submenu_page( 'vipps_admin_menu', __('Recurring Payments', 'woo-vipps'), __('Recurring Payments', 'woo-vipps'), 'manage_woocommerce', 'vipps_recurring__settings_menu', array($this, 'recurring_settings_page'), 95); |
| 1705 |
} |
| 1706 |
|
| 1707 |
add_submenu_page( 'vipps_admin_menu', __('Badges', 'woo-vipps'), __('Badges', 'woo-vipps'), 'manage_woocommerce', 'vipps_badge_menu', array($this, 'badge_menu_page'), 90); |
| 1708 |
add_submenu_page( 'vipps_admin_menu', __('Buttons', 'woo-vipps'), __('Buttons', 'woo-vipps'), 'manage_woocommerce', 'vipps_button_menu', array($this, 'button_menu_page'), 80); |
| 1709 |
add_submenu_page( 'vipps_admin_menu', __('Webhooks', 'woo-vipps'), __('Webhooks', 'woo-vipps'), 'manage_woocommerce', 'vipps_webhook_menu', array($this, 'webhook_menu_page'), 10); |
| 1710 |
} |
| 1711 |
|
| 1712 |
// Just a redirect to the recurring payment settings for the time being. IOK 2025-01-08 |
| 1713 |
public function recurring_settings_page () { |
| 1714 |
if (class_exists('WC_Vipps_Recurring') && class_exists('WC_Subscriptions_Plugin')) { |
| 1715 |
wp_safe_redirect(admin_url('/admin.php?page=wc-settings&tab=checkout§ion=vipps_recurring'), 302); |
| 1716 |
} else { |
| 1717 |
wp_safe_redirect(admin_url('/admin.php?page=vipps_admin_menu'), 302); |
| 1718 |
} |
| 1719 |
exit(); |
| 1720 |
} |
| 1721 |
|
| 1722 |
public function add_meta_boxes () { |
| 1723 |
$screen = 'shop_order'; |
| 1724 |
$useHPOS = $this->useHPOS(); |
| 1725 |
|
| 1726 |
if ($useHPOS && function_exists('wc_get_page_screen_id')) { |
| 1727 |
$screen = wc_get_page_screen_id('shop-order'); |
| 1728 |
} |
| 1729 |
|
| 1730 |
$vippsorder = false; |
| 1731 |
$order = null; |
| 1732 |
global $post; |
| 1733 |
if ($post && $post->post_type == 'shop_order') { |
| 1734 |
$order = wc_get_order($post); |
| 1735 |
} else { |
| 1736 |
// New style HPOS order table doesn't let us inspect the order, so we must fetch it from query args |
| 1737 |
$screen = get_current_screen(); |
| 1738 |
if ($screen && $screen->id == 'woocommerce_page_wc-orders') { |
| 1739 |
$orderid = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0; |
| 1740 |
$order = wc_get_order($orderid); |
| 1741 |
} |
| 1742 |
} |
| 1743 |
if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) { |
| 1744 |
$vippsorder = true; |
| 1745 |
} |
| 1746 |
|
| 1747 |
if ($vippsorder) { |
| 1748 |
add_meta_box( 'vippsdata', sprintf(__('%1$s','woo-vipps'), $this->get_payment_method_name()), array($this,'add_vipps_metabox'), $screen, 'side', 'core' ); |
| 1749 |
} |
| 1750 |
} |
| 1751 |
|
| 1752 |
public function wp_register_scripts () { |
| 1753 |
// We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues |
| 1754 |
// (which it only is if Gutenberg is active) or not provided at all, add it now. |
| 1755 |
if (!wp_script_is( 'wp-hooks', 'registered')) { |
| 1756 |
wp_register_script('wp-hooks', plugins_url('/compat/hooks.min.js', __FILE__)); |
| 1757 |
} |
| 1758 |
wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true'); |
| 1759 |
|
| 1760 |
// Badges - web components provided by Vipps MobilePay to display payment options in-store. |
| 1761 |
wp_register_script('vipps-onsite-messageing', |
| 1762 |
plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE), |
| 1763 |
array(), |
| 1764 |
filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'), |
| 1765 |
[ |
| 1766 |
'in_footer' => true, |
| 1767 |
'strategy' => 'async', |
| 1768 |
], |
| 1769 |
); |
| 1770 |
|
| 1771 |
// Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24 |
| 1772 |
wp_register_script('vipps-button-webcomponent', |
| 1773 |
plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE), |
| 1774 |
array(), |
| 1775 |
filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'), |
| 1776 |
[ |
| 1777 |
'in_footer' => false |
| 1778 |
], |
| 1779 |
); |
| 1780 |
} |
| 1781 |
|
| 1782 |
// Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02 |
| 1783 |
public function script_add_vippslocale () { |
| 1784 |
// This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13 |
| 1785 |
$strings = array( |
| 1786 |
'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()), |
| 1787 |
'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()), |
| 1788 |
'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()), |
| 1789 |
); |
| 1790 |
wp_localize_script('vipps-gw', 'VippsLocale', $strings); |
| 1791 |
} |
| 1792 |
|
| 1793 |
public function wp_enqueue_scripts() { |
| 1794 |
wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig); |
| 1795 |
// Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02 |
| 1796 |
$this->script_add_vippslocale(); |
| 1797 |
|
| 1798 |
wp_enqueue_script('vipps-gw'); |
| 1799 |
wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css")); |
| 1800 |
wp_enqueue_script('vipps-button-webcomponent'); |
| 1801 |
} |
| 1802 |
|
| 1803 |
|
| 1804 |
public function add_shortcodes() { |
| 1805 |
add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode')); |
| 1806 |
add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode')); |
| 1807 |
add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode')); |
| 1808 |
|
| 1809 |
// Badges, if using shortcodes |
| 1810 |
// New vipps-mobilepay-badge shortcode. LP 19.11.2024 |
| 1811 |
add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode')); |
| 1812 |
// Legacy vipps-badge shortcode. LP 19.11.2024 |
| 1813 |
add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode')); |
| 1814 |
|
| 1815 |
// special page handling, previously a fake page. LP 2026-08-25 |
| 1816 |
add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode')); |
| 1817 |
} |
| 1818 |
|
| 1819 |
|
| 1820 |
public function log ($what,$type='info') { |
| 1821 |
$logger = function_exists('wc_get_logger') ? wc_get_logger() : false; |
| 1822 |
if ($logger) { |
| 1823 |
$context = array('source'=>'woo-vipps'); |
| 1824 |
$logger->log($type,$what,$context); |
| 1825 |
} else { |
| 1826 |
error_log("woo-vipps ($type): $what"); |
| 1827 |
} |
| 1828 |
} |
| 1829 |
|
| 1830 |
|
| 1831 |
// If we have admin-notices that we haven't gotten a chance to show because of |
| 1832 |
// a redirect, this method will fetch and show them IOK 2018-05-07 |
| 1833 |
public function stored_admin_notices() { |
| 1834 |
$stored = get_transient('_vipps_save_admin_notices'); |
| 1835 |
if ($stored) { |
| 1836 |
delete_transient('_vipps_save_admin_notices'); |
| 1837 |
print $stored; |
| 1838 |
} |
| 1839 |
do_action('vipps_admin_notices'); |
| 1840 |
} |
| 1841 |
|
| 1842 |
// Show express button option on checkout form. LP 2026-03-23 |
| 1843 |
public function checkout_before_customer_details_express () { |
| 1844 |
$gw = $this->gateway(); |
| 1845 |
if (!$gw->show_express_checkout()) return; |
| 1846 |
$this->express_checkout_section_html(); |
| 1847 |
} |
| 1848 |
|
| 1849 |
public function express_checkout_section_html() { |
| 1850 |
$payment_method = $this->get_payment_method_name(); |
| 1851 |
$header_text = __('Express Checkout', 'woo-vipps'); |
| 1852 |
$header = "<legend class='express-header'>$header_text</legend>"; |
| 1853 |
$div_classes = "legacy-checkout vipps-express-checkout $payment_method"; |
| 1854 |
echo "<fieldset class='$div_classes'>$header"; |
| 1855 |
$this->checkout_express_checkout_button_html(); |
| 1856 |
echo '</fieldset>'; |
| 1857 |
} |
| 1858 |
|
| 1859 |
public function express_checkout_banner() { |
| 1860 |
$gw = $this->gateway(); |
| 1861 |
if (!$gw->show_express_checkout()) return; |
| 1862 |
return $this->express_checkout_banner_html(); |
| 1863 |
} |
| 1864 |
|
| 1865 |
public function express_checkout_banner_html() { |
| 1866 |
$url = $this->express_checkout_url(); |
| 1867 |
$url = wp_nonce_url($url,'express','sec'); |
| 1868 |
$text = __('Skip entering your address and just checkout using', 'woo-vipps'); |
| 1869 |
$linktext = 'Express'; // dont translate. LP 2025-09-03 |
| 1870 |
$logo = $this->get_express_banner_logo(); |
| 1871 |
$payment_method = $this->get_payment_method_name(); |
| 1872 |
|
| 1873 |
$img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo'; |
| 1874 |
$div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info'; |
| 1875 |
$a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link'; |
| 1876 |
|
| 1877 |
$message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>"; |
| 1878 |
$message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method); |
| 1879 |
?> |
| 1880 |
<div class="<?php echo $div_classes;?>"><?php echo $message;?></div> |
| 1881 |
<?php |
| 1882 |
} |
| 1883 |
|
| 1884 |
public function checkout_express_checkout_button() { |
| 1885 |
$gw = $this->gateway(); |
| 1886 |
|
| 1887 |
if ($gw->show_express_checkout()){ |
| 1888 |
return $this->checkout_express_checkout_button_html(); |
| 1889 |
} |
| 1890 |
} |
| 1891 |
|
| 1892 |
public function checkout_express_checkout_button_html() { |
| 1893 |
$url = $this->express_checkout_url(); |
| 1894 |
$url = wp_nonce_url($url,'express','sec'); |
| 1895 |
$button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout')); |
| 1896 |
$method = $this->get_payment_method_name(); |
| 1897 |
$title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method); |
| 1898 |
$html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>"; |
| 1899 |
$html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url); |
| 1900 |
echo $html; |
| 1901 |
} |
| 1902 |
|
| 1903 |
// Show the express button if reasonable to do so |
| 1904 |
public function cart_express_checkout_button() { |
| 1905 |
$gw = $this->gateway(); |
| 1906 |
|
| 1907 |
if ($gw->show_express_checkout()){ |
| 1908 |
return $this->cart_express_checkout_button_html(); |
| 1909 |
} |
| 1910 |
} |
| 1911 |
|
| 1912 |
public function minicart_express_checkout_button() { |
| 1913 |
$gw = $this->gateway(); |
| 1914 |
|
| 1915 |
if ($gw->show_express_checkout()){ |
| 1916 |
return $this->cart_express_checkout_button_html(true); |
| 1917 |
} |
| 1918 |
} |
| 1919 |
|
| 1920 |
public function cart_express_checkout_button_html($minicart = false) { |
| 1921 |
$url = $this->express_checkout_url(); |
| 1922 |
$url = wp_nonce_url($url,'express','sec'); |
| 1923 |
$context = $minicart ? 'minicart' : 'cart'; |
| 1924 |
$button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context)); |
| 1925 |
$method = $this->get_payment_method_name(); |
| 1926 |
$title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method); |
| 1927 |
$html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>"; |
| 1928 |
$html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url); |
| 1929 |
echo $html; |
| 1930 |
} |
| 1931 |
|
| 1932 |
// A shortcode for a single buy now button. Express checkout must be active; but I don't check for this here, as this button may be |
| 1933 |
// cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02 |
| 1934 |
public function buy_now_button_shortcode ($atts) { |
| 1935 |
// The new web component button args. LP 2026-07-02 |
| 1936 |
$button_args = $this->get_html_button_default_attrs(); |
| 1937 |
unset($button_args['brand']); |
| 1938 |
|
| 1939 |
// Variant exists for the product variant. LP 2026-07-02 |
| 1940 |
if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant']; |
| 1941 |
unset($button_args['variant']); |
| 1942 |
|
| 1943 |
$args = shortcode_atts( |
| 1944 |
array(...$button_args, |
| 1945 |
'id' => '','variant'=> '','sku' => '', |
| 1946 |
), |
| 1947 |
$atts, |
| 1948 |
); |
| 1949 |
|
| 1950 |
// Variant exists for the product variant. LP 2026-07-02 |
| 1951 |
$button_args = $args; |
| 1952 |
if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant']; |
| 1953 |
unset($button_args['button_variant']); |
| 1954 |
unset($button_args['sku']); |
| 1955 |
unset($button_args['id']); |
| 1956 |
// NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/ |
| 1957 |
// "Note also that there is a bug in the library, and it currently only renders one language per page." |
| 1958 |
// it seems like get_html_button() runs once before this shortcode code (whic gets default attrs including language), so I think this is why language bug appears. LP 2026-07-02 |
| 1959 |
|
| 1960 |
return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>"; |
| 1961 |
} |
| 1962 |
|
| 1963 |
// The express checkout shortcode implementation. It does not need to check if we are to show the button, obviously, but needs to see if the cart works |
| 1964 |
public function express_checkout_button_shortcode() { |
| 1965 |
$gw = $this->gateway(); |
| 1966 |
if (!$gw->cart_supports_express_checkout()) return; |
| 1967 |
ob_start(); |
| 1968 |
$this->cart_express_checkout_button_html('shortcode'); |
| 1969 |
return ob_get_clean(); |
| 1970 |
} |
| 1971 |
// Show a banner normally shown for non-logged-in-users at the checkout page. It does not need to check if we are to show the button, obviously, but needs to see if the cart works |
| 1972 |
public function express_checkout_banner_shortcode() { |
| 1973 |
$gw = $this->gateway(); |
| 1974 |
if (!$gw->cart_supports_express_checkout()) return; |
| 1975 |
ob_start(); |
| 1976 |
$this->express_checkout_banner_html(); |
| 1977 |
return ob_get_clean(); |
| 1978 |
} |
| 1979 |
|
| 1980 |
// Manage the various product meta fields |
| 1981 |
public function process_product_meta ($id, $post) { |
| 1982 |
// This is for the 'buy now' button |
| 1983 |
if (isset($_POST['woo_vipps_add_buy_now_button'])) { |
| 1984 |
update_post_meta($id, '_vipps_buy_now_button', sanitize_text_field($_POST['woo_vipps_add_buy_now_button'])); |
| 1985 |
} |
| 1986 |
// This is for overriding Vipps Badge settings |
| 1987 |
if (isset($_POST['woo_vipps_show_badge'])) { |
| 1988 |
update_post_meta($id, '_vipps_show_badge', sanitize_text_field($_POST['woo_vipps_show_badge'])); |
| 1989 |
} |
| 1990 |
|
| 1991 |
// This is for the shareable links. |
| 1992 |
if (isset($_POST['woo_vipps_shareable_delenda'])) { |
| 1993 |
$delenda = array_map('sanitize_text_field',$_POST['woo_vipps_shareable_delenda']); |
| 1994 |
foreach($delenda as $delendum) { |
| 1995 |
// This will delete the actual link |
| 1996 |
delete_post_meta($post->ID, '_vipps_shareable_link_'.$delendum); |
| 1997 |
} |
| 1998 |
// Delete all legacy "shareable links" collections. IOK 2024-06-19 |
| 1999 |
delete_post_meta($post->ID, '_vipps_shareable_links'); |
| 2000 |
} |
| 2001 |
} |
| 2002 |
|
| 2003 |
// An extra product meta tab for Vipps |
| 2004 |
public function woocommerce_product_data_tabs ($tabs) { |
| 2005 |
$img = plugins_url('img/vipps_logo.png',__FILE__); |
| 2006 |
$tabs['vipps'] = array( 'label' => sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()), 'priority'=>100, 'target'=>'woo-vipps', 'class'=>array()); |
| 2007 |
return $tabs; |
| 2008 |
} |
| 2009 |
public function woocommerce_product_data_panels() { |
| 2010 |
global $post; |
| 2011 |
echo "<div id='woo-vipps' class='panel woocommerce_options_panel'>"; |
| 2012 |
// IOK 2024-01-17 Temporary: Only Vipps supports express checkout, shareable links (express checkout) and badges |
| 2013 |
// IOK 2025-09-01 Now available for all |
| 2014 |
$this->product_options_vipps(); |
| 2015 |
$this->product_options_vipps_badges(); |
| 2016 |
$this->product_options_vipps_shareable_link(); |
| 2017 |
echo "</div>"; |
| 2018 |
} |
| 2019 |
// Product data specific to Vipps - mostly the use of the 'Buy now!' button |
| 2020 |
public function product_options_vipps() { |
| 2021 |
$gw = $this->gateway(); |
| 2022 |
$choice = $gw->get_option('singleproductexpress'); |
| 2023 |
echo '<div class="options_group">'; |
| 2024 |
echo "<div class='blurb' style='margin-left:13px'><h4>"; |
| 2025 |
echo __("Buy-now button", 'woo-vipps') ; |
| 2026 |
echo "<h4></div>"; |
| 2027 |
if ($choice == 'some') { |
| 2028 |
$button = sanitize_text_field(get_post_meta( get_the_ID(), '_vipps_buy_now_button', true)); |
| 2029 |
echo "<input type='hidden' name='woo_vipps_add_buy_now_button' value='no' />"; |
| 2030 |
woocommerce_wp_checkbox( array( |
| 2031 |
'id' => 'woo_vipps_add_buy_now_button', |
| 2032 |
'value' => $button, |
| 2033 |
'label' => sprintf(__('Add \'Buy now with %1$s\' button', 'woo-vipps'), $this->get_payment_method_name()), |
| 2034 |
'desc_tip' => true, |
| 2035 |
'description' => sprintf(__('Add a \'Buy now with %1$s\'-button to this product','woo-vipps'), $this->get_payment_method_name()) |
| 2036 |
) ); |
| 2037 |
} else if ($choice == "all") { |
| 2038 |
$prod = wc_get_product(get_the_ID()); |
| 2039 |
$canbebought = false; |
| 2040 |
if (is_a($prod, 'WC_Product')) { |
| 2041 |
$canbebought = $gw->product_supports_express_checkout(wc_get_product(get_the_ID())); |
| 2042 |
} |
| 2043 |
|
| 2044 |
echo "<p>"; |
| 2045 |
echo sprintf(__("The %1\$s settings are currently set up so all products that can be bought with Express Checkout will have a Buy Now button.", 'woo-vipps'), Vipps::CompanyName()); |
| 2046 |
echo " "; |
| 2047 |
if ($canbebought) { |
| 2048 |
echo __("This product supports express checkout, and so will have a Buy Now button." , 'woo-vipps'); |
| 2049 |
} else { |
| 2050 |
echo __("This product does <b>not</b> support express checkout, and so will <b>not</b> have a Buy Now button." , 'woo-vipps'); |
| 2051 |
} |
| 2052 |
echo "</p>"; |
| 2053 |
} else { |
| 2054 |
$settings = esc_attr(admin_url('/admin.php?page=vipps_settings_menu')); |
| 2055 |
echo "<p>"; |
| 2056 |
echo sprintf(__("The %1\$s settings</a> are configured so that no products will have a Buy Now button - including this.", 'woo-vipps'), Vipps::CompanyName()); |
| 2057 |
echo "</p>"; |
| 2058 |
} |
| 2059 |
echo '</div>'; |
| 2060 |
} |
| 2061 |
|
| 2062 |
public function product_options_vipps_badges() { |
| 2063 |
$current = get_option('vipps_badge_options'); |
| 2064 |
if (!$current || !($current['badgeon'] ?? false)) return; |
| 2065 |
echo '<div class="options_group">'; |
| 2066 |
echo "<div class='blurb' style='margin-left:13px'><h4>"; |
| 2067 |
echo __("On-site messaging badge", 'woo-vipps') ; |
| 2068 |
echo "<h4></div>"; |
| 2069 |
$showbadge = sanitize_text_field(get_post_meta( get_the_ID(), '_vipps_show_badge', true)); |
| 2070 |
|
| 2071 |
woocommerce_wp_select( |
| 2072 |
array( |
| 2073 |
'id' => 'woo_vipps_show_badge', |
| 2074 |
'label' => __( 'Override default settings', 'woo-vipps' ), |
| 2075 |
'options' => array( |
| 2076 |
'' => __('Default setting', 'woo-vipps'), |
| 2077 |
'none' => __('No badge', 'woo-vipps'), |
| 2078 |
'white' => __('White', 'woo-vipps'), |
| 2079 |
'grey' => __('Grey', 'woo-vipps'), |
| 2080 |
'filled' => __('Filled', 'woo-vipps'), |
| 2081 |
'light' => __('Light', 'woo-vipps'), |
| 2082 |
'purple' => __('Purple', 'woo-vipps'), |
| 2083 |
), |
| 2084 |
'value' => $showbadge |
| 2085 |
) |
| 2086 |
); |
| 2087 |
echo "</div>"; |
| 2088 |
|
| 2089 |
} |
| 2090 |
|
| 2091 |
public function product_options_vipps_shareable_link() { |
| 2092 |
global $post; |
| 2093 |
global $wpdb; |
| 2094 |
$product = wc_get_product($post->ID); |
| 2095 |
$variable = ($product->get_type() == 'variable'); |
| 2096 |
|
| 2097 |
$buy_url = $this->buy_product_url(); |
| 2098 |
$q = $wpdb->prepare("SELECT meta_key, meta_value FROM `{$wpdb->postmeta}` WHERE post_id = %d AND meta_key LIKE '_vipps_shareable_link@_%' escape '@'", $product->get_id()); |
| 2099 |
$res = $wpdb->get_results($q, ARRAY_A); |
| 2100 |
$shareables = []; |
| 2101 |
if ($res) { |
| 2102 |
foreach($res as $entry) { |
| 2103 |
$shareable = maybe_unserialize($entry['meta_value']); |
| 2104 |
if (!$shareable || empty($shareable['key'])) continue; |
| 2105 |
$url = add_query_arg('pr',$shareable['key'],$this->buy_product_url()); |
| 2106 |
$shareable['url'] = $url; |
| 2107 |
$shareables[] = $shareable; |
| 2108 |
} |
| 2109 |
} |
| 2110 |
|
| 2111 |
$qradmin = admin_url("/edit.php?post_type=vipps_qr_code"); |
| 2112 |
?> |
| 2113 |
<div class="options_group"> |
| 2114 |
<div class='blurb' style='margin-left:13px'> |
| 2115 |
<h4><?php echo __("Shareable links", 'woo-vipps') ?></h4> |
| 2116 |
<p><?php echo sprintf(__('Shareable links are links you can share externally on banners or other places that when followed will start %1$s of this product immediately. Maintain these links here for this product.', 'woo-vipps'), Vipps::ExpressCheckoutName()); ?> </p> |
| 2117 |
<p><?php echo sprintf(__("To create a QR code for your shareable link, we recommend copying the URL and then using the <a href='%2\$s'>%1\$s QR Api</a>", 'woo-vipps'), "Vipps", $qradmin); ?> </p> |
| 2118 |
<input type=hidden id=vipps_sharelink_id value='<?php echo $product->get_id(); ?>'> |
| 2119 |
<?php |
| 2120 |
echo wp_nonce_field('share_link_nonce','vipps_share_sec',1,false); |
| 2121 |
if ($variable): |
| 2122 |
$variations = $product->get_available_variations(); |
| 2123 |
echo "<button id='vipps-share-link' disabled class='button' onclick='return false;'>"; echo __("Create shareable link",'woo-vipps'); echo "</button>"; |
| 2124 |
echo "<select id='vipps_sharelink_variant'><option value=''>"; echo __("Select variant", 'woo-vipps'); echo "</option>"; |
| 2125 |
foreach($variations as $var) { |
| 2126 |
$varid = esc_attr($var['variation_id']); |
| 2127 |
echo "<option value='$varid'>$varid"; |
| 2128 |
echo esc_html($var['sku']); |
| 2129 |
echo "</option>"; |
| 2130 |
} |
| 2131 |
echo "</select>"; |
| 2132 |
else: |
| 2133 |
echo "<button id='vipps-share-link' class='button' onclick='return false;'>"; echo __("Create shareable link", 'woo-vipps'); "</button>"; |
| 2134 |
endif; |
| 2135 |
?> |
| 2136 |
</div> <!-- end blurb --> |
| 2137 |
<div style="display:none;" id='woo_vipps_shareable_link_template'> |
| 2138 |
<a class='shareable' title="<?php echo __('Click to copy', 'woo-vipps'); ?>" href="javascrip:void(0)"></a><input class=deletemarker type=hidden value=''> |
| 2139 |
</div> |
| 2140 |
<div style="display:none;" id='woo_vipps_shareable_command_template'> |
| 2141 |
<a class="copyaction" href='javascript:void(0)'>[<?php echo __("Copy", 'woo-vipps'); ?>]</a> |
| 2142 |
<a class="deleteaction" style="margin-left:13px;" class="deleteaction" href="javascript:void(0)">[<?php echo __('Delete', 'woo-vipps'); ?>]</a> |
| 2143 |
</div> |
| 2144 |
<style> |
| 2145 |
#woo_vipps_shareables a.deleted { |
| 2146 |
text-decoration: line-through; |
| 2147 |
} |
| 2148 |
</style> |
| 2149 |
<div class='blurb' style='margin-left:13px;margin-right:13px'> |
| 2150 |
<div id="message-area" style="min-height:2em"> |
| 2151 |
<div class="vipps-shareable-link-error" style="display:none"><?php echo __('An error occured while creating a shareable link', 'woo-vipps');?> |
| 2152 |
<span id="vipps-shareable-link-error"></span> |
| 2153 |
</div> |
| 2154 |
<div id="vipps-shareable-link-delete-message" style="display:none"><em><?php echo __('Link(s) will be deleted when you save the product', 'woo-vipps');?> </em></div> |
| 2155 |
</div> |
| 2156 |
<table id='woo_vipps_shareables' class='woo-vipps-link-table' style="width:100% <?php if (empty($shareables)) echo ';display:none;'?>"> |
| 2157 |
<thead> |
| 2158 |
<tr> |
| 2159 |
<?php if ($variable): ?><th align=left><?php echo __('Variant','woo-vipps'); ?></th><?php endif; ?> |
| 2160 |
<th align=left><?php echo __('Link','woo-vipps'); ?></th> |
| 2161 |
<th><?php echo __('Action','woo-vipps'); ?></th></tr> |
| 2162 |
</thead> |
| 2163 |
<tbody> |
| 2164 |
<tr> |
| 2165 |
<?php foreach ($shareables as $shareable): ?> |
| 2166 |
<?php if ($variable): ?><td><?php echo esc_html($shareable['variant']); ?></td><?php endif; ?> |
| 2167 |
<td><a class='shareable' title="<?php echo __('Click to copy','woo-vipps'); ?>" href="javascrip:void(0)"><?php echo esc_html($shareable['url']); ?></a><input class="deletemarker" type=hidden value='<?php echo esc_attr($shareable['key']); ?>'></td> |
| 2168 |
<td align=center> |
| 2169 |
<a class="copyaction" title="<?php echo __('Click to copy','woo-vipps'); ?>" href='javascript:void(0)'>[<?php echo __("Copy", 'woo-vipps'); ?>]</a> |
| 2170 |
<a class="deleteaction" title="<?php echo __('Mark this link for deletion', 'woo-vipps'); ?>" style="margin-left:13px;" class="deleteaction" href="javascript:void(0)">[<?php echo __('Delete', 'woo-vipps'); ?>]</a> |
| 2171 |
</td> |
| 2172 |
</tr> |
| 2173 |
<?php endforeach; ?> |
| 2174 |
</tbody> |
| 2175 |
</table> |
| 2176 |
</div> <!-- end blurb --> |
| 2177 |
</div> <!-- end options-group --> |
| 2178 |
<?php |
| 2179 |
} |
| 2180 |
|
| 2181 |
|
| 2182 |
// This creates and stores a shareable link that when followed will allow external buyers to buy the specified product direclty. |
| 2183 |
// Only products with these links can be bought like this; both to avoid having to create spurious orders from griefers and to ensure |
| 2184 |
// that a link can be retracted if it has been printed or shared in emails with a specific price. IOK 2018-10-03 |
| 2185 |
public function ajax_vipps_create_shareable_link() { |
| 2186 |
check_ajax_referer('share_link_nonce','vipps_share_sec'); |
| 2187 |
if (!current_user_can('manage_woocommerce')) { |
| 2188 |
echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps'))); |
| 2189 |
wp_die(); |
| 2190 |
} |
| 2191 |
static::set_locale_if_in_header(); |
| 2192 |
$prodid = intval($_POST['prodid']); |
| 2193 |
$varid = intval($_POST['varid']); |
| 2194 |
|
| 2195 |
$product = ''; |
| 2196 |
$variant = ''; |
| 2197 |
$varname = ''; |
| 2198 |
try { |
| 2199 |
$product = wc_get_product($prodid); |
| 2200 |
$variant = $varid ? wc_get_product($varid) : null; |
| 2201 |
$varname = $variant ? $variant->get_id() : ''; |
| 2202 |
if ($variant && $variant->get_sku()) { |
| 2203 |
$varname .= ":" . sanitize_text_field($variant->get_sku()); |
| 2204 |
} |
| 2205 |
} catch (Exception $e) { |
| 2206 |
echo json_encode(array('ok'=>0,'msg'=>$e->getMessage())); |
| 2207 |
wp_die(); |
| 2208 |
} |
| 2209 |
if (!$product) { |
| 2210 |
echo json_encode(array('ok'=>0,'msg'=>__('The product doesn\'t exist', 'woo-vipps'))); |
| 2211 |
wp_die(); |
| 2212 |
} |
| 2213 |
|
| 2214 |
// Find a free shareable link by generating a hash and testing it. Normally there won't be any collisions at all. |
| 2215 |
$key = ''; |
| 2216 |
while (!$key) { |
| 2217 |
global $wpdb; |
| 2218 |
$key = substr(sha1(mt_rand() . ":" . $prodid . ":" . $varid),0,8); |
| 2219 |
$existing = $wpdb->get_row("SELECT post_id from {$wpdb->prefix}postmeta where meta_key='_vipps_shareable_link_$key' limit 1",'ARRAY_A'); |
| 2220 |
if (!empty($existing)) $key = ''; |
| 2221 |
} |
| 2222 |
|
| 2223 |
$url = add_query_arg('pr',$key,$this->buy_product_url()); |
| 2224 |
$payload = array('product_id'=>$prodid,'variation_id'=>$varid,'key'=>$key, 'url'=>$url, 'variant'=>$varname); |
| 2225 |
|
| 2226 |
// This is used to find the link itself |
| 2227 |
update_post_meta($prodid,'_vipps_shareable_link_'.$key, array('product_id'=>$prodid,'variation_id'=>$varid,'key'=>$key)); |
| 2228 |
|
| 2229 |
echo json_encode(array('ok'=>1,'msg'=>'ok', 'url'=>$url, 'variant'=> $varname, 'key'=>$key)); |
| 2230 |
wp_die(); |
| 2231 |
} |
| 2232 |
|
| 2233 |
// A metabox for showing Vipps information about the order. IOK 2018-05-07 |
| 2234 |
public function add_vipps_metabox ($post_or_order_object) { |
| 2235 |
$order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object; |
| 2236 |
$order = wc_get_order($post_or_order_object); |
| 2237 |
$pm = $order->get_payment_method(); |
| 2238 |
if (!self::is_vipps_order($pm)) return; |
| 2239 |
$orderid=$order->get_id(); |
| 2240 |
|
| 2241 |
$init = intval($order->get_meta('_vipps_init_timestamp')); |
| 2242 |
$callback = intval($order->get_meta('_vipps_callback_timestamp')); |
| 2243 |
$capture = intval($order->get_meta('_vipps_capture_timestamp')); |
| 2244 |
$refund = intval($order->get_meta('_vipps_refund_timestamp')); |
| 2245 |
$cancel = intval($order->get_meta('_vipps_cancel_timestamp')); |
| 2246 |
|
| 2247 |
$status = $order->get_meta('_vipps_status'); |
| 2248 |
$total = intval($order->get_meta('_vipps_amount')); |
| 2249 |
$captured = intval($order->get_meta('_vipps_captured')); |
| 2250 |
$refunded = intval($order->get_meta('_vipps_refunded')); |
| 2251 |
$cancelled = intval($order->get_meta('_vipps_cancelled')); |
| 2252 |
|
| 2253 |
$capremain = intval($order->get_meta('_vipps_capture_remaining')); |
| 2254 |
$refundremain = intval($order->get_meta('_vipps_refund_remaining')); |
| 2255 |
|
| 2256 |
$paymentdetailsnonce=wp_create_nonce('paymentdetails'); |
| 2257 |
|
| 2258 |
$failures = intval($order->get_meta('_vipps_capture_failures')); |
| 2259 |
|
| 2260 |
$currency = $order->get_currency(); |
| 2261 |
|
| 2262 |
print "<table border=0><thead></thead><tbody>"; |
| 2263 |
print "<tr><td colspan=2>"; print $order->get_payment_method_title();print "</td></tr>"; |
| 2264 |
print "<tr><td>Status</td>"; |
| 2265 |
print "<td align=right>" . htmlspecialchars($status);print "</td></tr>"; |
| 2266 |
print "<tr><td>Amount</td><td align=right>" . sprintf("%0.2f ",$total/100); print $currency; print "</td></tr>"; |
| 2267 |
print "<tr><td>Captured</td><td align=right>" . sprintf("%0.2f ",$captured/100); print $currency; print "</td></tr>"; |
| 2268 |
print "<tr><td>Refunded</td><td align=right>" . sprintf("%0.2f ",$refunded/100); print $currency; print "</td></tr>"; |
| 2269 |
print "<tr><td>Cancelled</td><td align=right>" . sprintf("%0.2f ",$cancelled/100); print $currency; print "</td></tr>"; |
| 2270 |
|
| 2271 |
if ($failures) { |
| 2272 |
print("<tr><td>Capture attempts</td><td align=right>$failures</td></tr>"); |
| 2273 |
} |
| 2274 |
|
| 2275 |
print "<tr><td>Vipps initiated</td><td align=right>";if ($init) print date('Y-m-d H:i:s',$init); print "</td></tr>"; |
| 2276 |
print "<tr><td>Vipps response </td><td align=right>";if ($callback) print date('Y-m-d H:i:s',$callback); print "</td></tr>"; |
| 2277 |
print "<tr><td>Vipps capture </td><td align=right>";if ($capture) print date('Y-m-d H:i:s',$capture); print "</td></tr>"; |
| 2278 |
print "<tr><td>Vipps refund</td><td align=right>";if ($refund) print date('Y-m-d H:i:s',$refund); print "</td></tr>"; |
| 2279 |
print "<tr><td>Vipps cancelled</td><td align=right>";if ($cancel) print date('Y-m-d H:i:s',$cancel); print "</td></tr>"; |
| 2280 |
print "</tbody></table>"; |
| 2281 |
print "<a href='javascript:VippsGetPaymentDetails($orderid,\"$paymentdetailsnonce\");' class='button'>" . __('Show complete transaction details','woo-vipps') . "</a>"; |
| 2282 |
} |
| 2283 |
|
| 2284 |
|
| 2285 |
// Vipps' requirement for phone numbers is very strict, and payments initiated with |
| 2286 |
// numbers in any other format will fail. Therefore we must try to convert to MSISDN before that. |
| 2287 |
public static function normalizePhoneNumber($phone, $country='') { |
| 2288 |
$phonenr = preg_replace("![^0-9]!", "", strval($phone)); |
| 2289 |
$phonenr = preg_replace("!^0+!", "", $phonenr); |
| 2290 |
|
| 2291 |
// Try to reconstruct phone numbers from information provided |
| 2292 |
switch ($country) { |
| 2293 |
case 'DK': |
| 2294 |
if (8 === strlen($phonenr)) { |
| 2295 |
$phonenr = "45$phonenr"; |
| 2296 |
} |
| 2297 |
break; |
| 2298 |
case 'SE': // 10 digits, but we stripped the leading zero above, https://www.sent.dm/resources/se. LP 2026-02-09 |
| 2299 |
if (9 === strlen($phonenr)) { |
| 2300 |
$phonenr = "46$phonenr"; |
| 2301 |
} |
| 2302 |
break; |
| 2303 |
case 'NO': |
| 2304 |
if (8 === strlen($phonenr)) { |
| 2305 |
$phonenr = "47$phonenr"; |
| 2306 |
} |
| 2307 |
break; |
| 2308 |
case 'FI': // https://en.wikipedia.org/wiki/Telephone_numbers_in_Finland and https://kielitoimistonohjepankki.fi/ohje/puhelinnumerot/ |
| 2309 |
if (9 === strlen($phonenr) // 04x 123 45 67 and 050 123 45 67 (but we removed leading zero already) |
| 2310 |
|| 10 === strlen($phonenr) // 0457 123 45 67 (but we removed leading zero already) |
| 2311 |
) { |
| 2312 |
$phonenr = "358$phonenr"; |
| 2313 |
} |
| 2314 |
break; |
| 2315 |
} |
| 2316 |
|
| 2317 |
if (!preg_match("/^\d{10,15}$/", $phonenr)) { |
| 2318 |
$phonenr = false; |
| 2319 |
} |
| 2320 |
return $phonenr; |
| 2321 |
} |
| 2322 |
|
| 2323 |
|
| 2324 |
// This is for debugging and ensuring we have excact details correct for a transaction. |
| 2325 |
public function ajax_vipps_payment_details() { |
| 2326 |
check_ajax_referer('paymentdetails','vipps_paymentdetails_sec'); |
| 2327 |
static::set_locale_if_in_header(); |
| 2328 |
$orderid = intval($_REQUEST['orderid']); |
| 2329 |
$gw = $this->gateway(); |
| 2330 |
$order = wc_get_order($orderid); |
| 2331 |
if (!$order) { |
| 2332 |
print "<p>" . __("Unknown order", 'woo-vipps') . "</p>"; |
| 2333 |
exit(); |
| 2334 |
} |
| 2335 |
$pm = $order->get_payment_method(); |
| 2336 |
if (!self::is_vipps_order($pm)) { |
| 2337 |
print "<p>" . sprintf(__("The order is not a %1\$s order", 'woo-vipps'), $this->get_payment_method_name()) . "</p>"; |
| 2338 |
exit(); |
| 2339 |
} |
| 2340 |
|
| 2341 |
$gw = $this->gateway(); |
| 2342 |
try { |
| 2343 |
$details = $gw->get_payment_details($order); |
| 2344 |
|
| 2345 |
if ($details) { |
| 2346 |
try { |
| 2347 |
$details['epaymentLog'] = $gw->api->epayment_get_payment_log ($order); |
| 2348 |
} catch (Exception $e) { |
| 2349 |
$this->log("Could not get transaction log for " . $order->get_id() . " : " . $e->getMessage(), 'error'); |
| 2350 |
} |
| 2351 |
} |
| 2352 |
$order->update_meta_data('_vipps_capture_failures', 0); // Reset this if getting full data |
| 2353 |
$order = $gw->update_vipps_payment_details($order, $details); |
| 2354 |
} catch (Exception $e) { |
| 2355 |
print "<p>"; |
| 2356 |
print __('Transaction details not retrievable: ','woo-vipps') . $e->getMessage(); |
| 2357 |
print "</p>"; |
| 2358 |
exit(); |
| 2359 |
} |
| 2360 |
|
| 2361 |
print "<h2>" . __('Transaction details','woo-vipps') . "</h2>"; |
| 2362 |
print "<p>"; |
| 2363 |
print __('Order id', 'woo-vipps') . ": " . @$details['orderId'] . "<br>"; |
| 2364 |
print __('Order status', 'woo-vipps') . ": " .@$details['status'] . "<br>"; |
| 2365 |
if (isset($details['paymentMethod'])) { |
| 2366 |
$method = (is_array($details['paymentMethod'])) ? $details['paymentMethod']['type'] : ""; |
| 2367 |
print __("Payment method", 'woo-vipps') . ":" . $method . "<br>"; |
| 2368 |
} else { |
| 2369 |
print __("Payment method", 'woo-vipps') . ": Vipps <br>"; |
| 2370 |
} |
| 2371 |
print __("API", 'woo-vipps') .": " . esc_html($order->get_meta('_vipps_api')) . "</br>"; |
| 2372 |
|
| 2373 |
if (!empty(@$details['transactionSummary'])) { |
| 2374 |
$ts = $details['transactionSummary']; |
| 2375 |
print "<h3>" . __('Transaction summary', 'woo-vipps') . "</h3>"; |
| 2376 |
print __('Capured amount', 'woo-vipps') . ":" . @$ts['capturedAmount'] . "<br>"; |
| 2377 |
print __('Remaining amount to capture', 'woo-vipps') . ":" . @$ts['remainingAmountToCapture'] . "<br>"; |
| 2378 |
print __('Refunded amount', 'woo-vipps') . ":" . @$ts['refundedAmount'] . "<br>"; |
| 2379 |
print __('Remaining amount to refund', 'woo-vipps') . ":" . @$ts['remainingAmountToRefund'] . "<br>"; |
| 2380 |
if (isset($ts['cancelledAmount'])) { |
| 2381 |
print __('Cancelled amount', 'woo-vipps') . ":" . @$ts['cancelledAmount'] . "<br>"; |
| 2382 |
print __('Remaining amount to cancel', 'woo-vipps') . ":" . @$ts['remainingAmountToCancel'] . "<br>"; |
| 2383 |
} |
| 2384 |
} |
| 2385 |
if (!empty(@$details['shippingDetails'])) { |
| 2386 |
$ss = $details['shippingDetails']; |
| 2387 |
$addr = isset($ss['address']) ? $ss['address'] : array(); |
| 2388 |
print "<h3>" . __('Shipping details', 'woo-vipps') . "</h3>"; |
| 2389 |
print __('Address', 'woo-vipps') . ": " . htmlspecialchars(join(', ', array_filter(array_values($addr), 'is_scalar'))) . "<br>"; |
| 2390 |
if (@$ss['shippingMethod']) print __('Shipping method', 'woo-vipps') . ": " . htmlspecialchars(@$ss['shippingMethod']) . "<br>"; |
| 2391 |
if (@$ss['shippingCost']) print __('Shipping cost', 'woo-vipps') . ": " . @$ss['shippingCost'] . "<br>"; |
| 2392 |
print __('Shipping method ID', 'woo-vipps') . ": " . htmlspecialchars(@$ss['shippingMethodId']) . "<br>"; |
| 2393 |
if (isset($ss['pickupPoint'])) { |
| 2394 |
$pp = $ss['pickupPoint']; |
| 2395 |
print "<h3>" . __('Pickup Point', 'woo-vipps') . "</h3>"; |
| 2396 |
print $pp['name'] . "<br>"; |
| 2397 |
print $pp['address'] . "<br>"; |
| 2398 |
print $pp['postalCode'] . " "; |
| 2399 |
print $pp['city'] . "<br>"; |
| 2400 |
print $pp['country'] . "<br>"; |
| 2401 |
} |
| 2402 |
} |
| 2403 |
if (!empty(@$details['billingDetails'])) { |
| 2404 |
$us = $details['billingDetails']; |
| 2405 |
print "<h3>" . __('Billing details', 'woo-vipps') . "</h3>"; |
| 2406 |
print __('First Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['firstName']) . "<br>"; |
| 2407 |
print __('Last Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['lastName']) . "<br>"; |
| 2408 |
print __('Mobile Number', 'woo-vipps') . ": " . htmlspecialchars(@$us['phoneNumber']) . "<br>"; |
| 2409 |
print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>"; |
| 2410 |
} |
| 2411 |
// Checkout v3: No userDetails, but Vipps email may be present |
| 2412 |
if (!empty(@$details['userInfo'])) { |
| 2413 |
$us = $details['userInfo']; |
| 2414 |
print "<h3>" . __('User details', 'woo-vipps') . "</h3>"; |
| 2415 |
print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>"; |
| 2416 |
} else if (!empty(@$details['userDetails'])) { |
| 2417 |
// Older versions of the api, as well as express checkout has "userDetails" |
| 2418 |
$us = $details['userDetails']; |
| 2419 |
print "<h3>" . __('User details', 'woo-vipps') . "</h3>"; |
| 2420 |
print __('User ID', 'woo-vipps') . ": " . htmlspecialchars(@$us['userId']) . "<br>"; |
| 2421 |
print __('First Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['firstName']) . "<br>"; |
| 2422 |
print __('Last Name', 'woo-vipps') . ": " . htmlspecialchars(@$us['lastName']) . "<br>"; |
| 2423 |
print __('Mobile Number', 'woo-vipps') . ": " . htmlspecialchars(@$us['mobileNumber']) . "<br>"; |
| 2424 |
print __('Email', 'woo-vipps') . ": " . htmlspecialchars(@$us['email']) . "<br>"; |
| 2425 |
} |
| 2426 |
if (!empty(@$details['epaymentLog']) && is_array($details['epaymentLog'])) { |
| 2427 |
print "<h3>" . __('Transaction Log', 'woo-vipps') . "</h3>"; |
| 2428 |
$i = count($details['epaymentLog'])+1; |
| 2429 |
$reversed = array_reverse($details['epaymentLog']); |
| 2430 |
foreach ($reversed as $td) { |
| 2431 |
print "<br>"; |
| 2432 |
print __('Operation','woo-vipps') . ": " . htmlspecialchars(@$td['name']) . "<br>"; |
| 2433 |
$value = intval(@$td['amount']['value'])/100; |
| 2434 |
$curr = $td['amount']['currency']; |
| 2435 |
|
| 2436 |
print __('Amount','woo-vipps') . ": " . esc_html($value) . " " . esc_html($curr) . "<br>"; |
| 2437 |
print __('Success','woo-vipps') . ": " . @$td['success'] . "<br>"; |
| 2438 |
print __('Timestamp','woo-vipps') . ": " . htmlspecialchars(@$td['timestamp']) . "<br>"; |
| 2439 |
print __('Transaction ID','woo-vipps') . ": " . htmlspecialchars(@$td['pspReference']) . "<br>"; |
| 2440 |
} |
| 2441 |
} |
| 2442 |
exit(); |
| 2443 |
} |
| 2444 |
|
| 2445 |
// This function will create a file with an obscure filename in the $callbackDirname directory. |
| 2446 |
// When initiating payment, this file will be created with a zero value. When the response is reday, |
| 2447 |
// it will be rewritten with the value 1. |
| 2448 |
// This function can fail if we can't write to the directory in question, in which case, return null and |
| 2449 |
// to the check with admin-ajax instead. IOK 2018-05-04 |
| 2450 |
public function createCallbackSignal($order,$ok=0) { |
| 2451 |
$fname = $this->callbackSignal($order); |
| 2452 |
if (!$fname) return null; |
| 2453 |
if ($ok) { |
| 2454 |
@file_put_contents($fname,"1"); |
| 2455 |
}else { |
| 2456 |
@file_put_contents($fname,"0"); |
| 2457 |
} |
| 2458 |
if (is_file($fname)) return $fname; |
| 2459 |
return null; |
| 2460 |
} |
| 2461 |
|
| 2462 |
//Helper function that produces the signal file name for an order IOK 2018-05-04 |
| 2463 |
public function callbackSignal($order) { |
| 2464 |
$dir = $this->callbackDir(); |
| 2465 |
if (!$dir) return null; |
| 2466 |
$fname = 'vipps-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction')) . ".txt"; |
| 2467 |
return $dir . DIRECTORY_SEPARATOR . $fname; |
| 2468 |
} |
| 2469 |
// URL of the above product thing |
| 2470 |
public function callbackSignalURL($signal) { |
| 2471 |
if (!$signal) return ""; |
| 2472 |
$uploaddir = wp_upload_dir(); |
| 2473 |
return $uploaddir['baseurl'] . '/' . $this->callbackDirname . '/' . basename($signal); |
| 2474 |
} |
| 2475 |
|
| 2476 |
// Clean up old signal files. If there gets to be a lot of them, this may take some time. IOK 2018-05-04. |
| 2477 |
public function cleanupCallbackSignals() { |
| 2478 |
$dir = $this->callbackDir(); |
| 2479 |
if (!is_dir($dir)) return; |
| 2480 |
$signals = scandir($dir); |
| 2481 |
$now = time(); |
| 2482 |
foreach($signals as $signal) { |
| 2483 |
$path = $dir . DIRECTORY_SEPARATOR . $signal; |
| 2484 |
if (is_dir($path)) continue; |
| 2485 |
if (is_file($path)) { |
| 2486 |
$age = @filemtime($path); |
| 2487 |
$halfhour = 30*60; |
| 2488 |
if (($age+$halfhour) < $now) { |
| 2489 |
@unlink($path); |
| 2490 |
} |
| 2491 |
} |
| 2492 |
} |
| 2493 |
} |
| 2494 |
|
| 2495 |
// Returns the name of the callback-directory, or null if it doesn't exist. IOK 2018-05-04 |
| 2496 |
private function callbackDir() { |
| 2497 |
$uploaddir = wp_upload_dir(); |
| 2498 |
$base = $uploaddir['basedir']; |
| 2499 |
$callbackdir = $base . DIRECTORY_SEPARATOR . $this->callbackDirname; |
| 2500 |
if (is_dir($callbackdir)) return $callbackdir; |
| 2501 |
$ok = mkdir($callbackdir, 0755); |
| 2502 |
if ($ok) return $callbackdir; |
| 2503 |
return null; |
| 2504 |
} |
| 2505 |
|
| 2506 |
// Unfortunately, we cannot do any form of portable locking, and we may get callbacks from Vipps arriving at the same moment as we check the status at Vipps, |
| 2507 |
// which in the very worst case, for Express Checkout orders, may lead to a double shipping line. Changing this to a queue system is non-trivial, because some of |
| 2508 |
// the operations done when modifying the order actually requires the customers session to be active. This operation will make conflicts a litte less probable |
| 2509 |
// by implementing something that isn't quite a lock, and the filter may be used to implement proper locking, using e.g. flock, where this can be used |
| 2510 |
// (non-distributed environments using unix on standard filesystems. IOK 2020-05-15 |
| 2511 |
// Returns true if lock succeeds, or false. |
| 2512 |
public function lockOrder($order) { |
| 2513 |
$orderid = $order->get_id(); |
| 2514 |
if (has_filter('woo_vipps_lock_order')) { |
| 2515 |
$ok = apply_filters('woo_vipps_lock_order', $order); |
| 2516 |
if (!$ok) return false; |
| 2517 |
} else { |
| 2518 |
if(get_transient('order_lock_'.$orderid)) return false; |
| 2519 |
$this->lockKey = uniqid(); |
| 2520 |
set_transient('order_lock_' . $orderid, $this->lockKey, 30); |
| 2521 |
} |
| 2522 |
add_action('shutdown', function () use ($order) { global $Vipps; $Vipps->unlockOrder($order); }); |
| 2523 |
return true; |
| 2524 |
} |
| 2525 |
// If the order is locked, it means it is in the process of being finalized, so for instance, we do *not* want to abandon it |
| 2526 |
// in checkout. |
| 2527 |
public function isLocked ($order) { |
| 2528 |
$orderid = $order->get_id(); |
| 2529 |
$locked = get_transient('order_lock_'.$orderid); |
| 2530 |
return apply_filters('woo_vipps_order_locked', $locked, $order); |
| 2531 |
} |
| 2532 |
public function unlockOrder($order) { |
| 2533 |
$orderid = $order->get_id(); |
| 2534 |
if (has_action('woo_vipps_unlock_order')) { |
| 2535 |
do_action('woo_vipps_unlock_order', $order); |
| 2536 |
} else { |
| 2537 |
if(get_transient('order_lock_'.$orderid) == $this->lockKey) { |
| 2538 |
delete_transient('order_lock_'.$orderid); |
| 2539 |
} |
| 2540 |
} |
| 2541 |
} |
| 2542 |
|
| 2543 |
// Functions using flock() and files to lock orders. This is only guaranteed to work on certain setups, ie, non-distributed setups |
| 2544 |
// using Unix with normal filesystems (not NFS). |
| 2545 |
public function flock_lock_order($order) { |
| 2546 |
global $_orderlocks; |
| 2547 |
if (!$_orderlocks) $_orderlocks = array(); |
| 2548 |
$dir = $this->callbackDir(); |
| 2549 |
if (!$dir) { |
| 2550 |
$this->log(__("Cannot use flock() to lock orders: cannot create or write to directory", "woo-vipps"), 'error'); |
| 2551 |
return true; |
| 2552 |
} |
| 2553 |
$fname = '.ht-vipps-lock-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction')); |
| 2554 |
$path = $dir . DIRECTORY_SEPARATOR . $fname; |
| 2555 |
touch($path); |
| 2556 |
if (!is_writable($path)) { |
| 2557 |
$this->log(__("Cannot use flock() to lock orders: cannot create lockfiles ", "woo-vipps"), 'error'); |
| 2558 |
return true; |
| 2559 |
} |
| 2560 |
$handle = fopen($path, 'w+'); |
| 2561 |
if (flock($handle, LOCK_EX | LOCK_NB)) { |
| 2562 |
$_orderlocks[$order->get_id()] = array($handle,$path); |
| 2563 |
return true; |
| 2564 |
} |
| 2565 |
return false; |
| 2566 |
} |
| 2567 |
public function flock_unlock_order($order) { |
| 2568 |
$orderid=$order->get_id(); |
| 2569 |
global $_orderlocks; |
| 2570 |
if (!$_orderlocks) return; |
| 2571 |
if (!isset($_orderlocks[$orderid])) return; |
| 2572 |
list($handle, $path) = $_orderlocks[$orderid]; |
| 2573 |
unset($_orderlocks[$orderid]); |
| 2574 |
flock($handle, LOCK_UN); |
| 2575 |
fclose($handle); |
| 2576 |
@unlink($path); |
| 2577 |
} |
| 2578 |
|
| 2579 |
|
| 2580 |
// Because the prefix used to create the Vipps order id is editable |
| 2581 |
// by the user, we will store that as a meta and use this for callbacks etc. |
| 2582 |
// IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with |
| 2583 |
// debug filters and similar. |
| 2584 |
// IOK 2026-05-27 rewritten to avoid wc_get_orders for pre-HPOS. Still not used. |
| 2585 |
public function getOrderIdByVippsOrderId($vippsorderid) { |
| 2586 |
$result = false; |
| 2587 |
if ($this->useHPOS()) { |
| 2588 |
$result = wc_get_orders( array( |
| 2589 |
'limit' => 1, |
| 2590 |
'return' => 'ids', |
| 2591 |
'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]] |
| 2592 |
)); |
| 2593 |
if ($result && is_array($result)) return $result[0]; |
| 2594 |
} else { |
| 2595 |
// Pre-HPOS did not support meta_query, so we're doing it with direct access to the database. IOK 2026-05-27 |
| 2596 |
global $wpdb; |
| 2597 |
$q = $wpdb->prepare("SELECT p.ID from `{$wpdb->posts}` p JOIN `{$wpdb->postmeta}` m ON (m.post_id = p.ID and m.meta_key = '_vipps_orderid') WHERE p.post_type = 'shop_order' AND m.meta_value = %s LIMIT 1", $vippsorderid); |
| 2598 |
$res = $wpdb->get_results($q, ARRAY_A); |
| 2599 |
if (empty($res)) return 0; |
| 2600 |
return $res[0]['ID']; |
| 2601 |
} |
| 2602 |
return 0; |
| 2603 |
} |
| 2604 |
|
| 2605 |
// This is like getOrderByVipsOrderId, but only fetches pending orders. |
| 2606 |
// This is used for the webhooks, where there is no way to add our own order info. IOK 2023-12-19 |
| 2607 |
private function get_pending_vipps_order($vippsorderid) { |
| 2608 |
if ($this->useHPOS()) { |
| 2609 |
$sevendaysago = time() - (60*60*24*7); |
| 2610 |
$result = wc_get_orders( array( |
| 2611 |
'limit' => 1, |
| 2612 |
'status' => 'wc-pending', |
| 2613 |
'type' => 'shop_order', |
| 2614 |
'date_created' => '>' . $sevendaysago, |
| 2615 |
'return' => 'objects', |
| 2616 |
'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]] |
| 2617 |
)); |
| 2618 |
if (!empty($result) && is_a($result[0], 'WC_Order')) return $result[0]; |
| 2619 |
return null; |
| 2620 |
} else { |
| 2621 |
global $wpdb; |
| 2622 |
$q = $wpdb->prepare("SELECT p.ID from `{$wpdb->posts}` p JOIN `{$wpdb->postmeta}` m ON (m.post_id = p.ID and m.meta_key = '_vipps_orderid') WHERE p.post_type = 'shop_order' && p.post_status = 'wc-pending' AND m.meta_value = %s LIMIT 1", $vippsorderid); |
| 2623 |
$res = $wpdb->get_results($q, ARRAY_A); |
| 2624 |
if (empty($res)) return null; |
| 2625 |
$o = wc_get_order($res[0]['ID']); |
| 2626 |
if (is_a($o, 'WC_Order')) return $o; |
| 2627 |
return null; |
| 2628 |
} |
| 2629 |
} |
| 2630 |
|
| 2631 |
// Special pages, and some callbacks. IOK 2018-05-18 |
| 2632 |
public function template_redirect() { |
| 2633 |
|
| 2634 |
// Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11 |
| 2635 |
// We redirect these to the new location. |
| 2636 |
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); |
| 2637 |
if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) { |
| 2638 |
$url = static::get_special_page_url(); |
| 2639 |
$_GET['action'] = 'buy_product'; |
| 2640 |
$q = build_query($_GET); |
| 2641 |
wp_redirect($url . "?" . $q, 302); |
| 2642 |
exit(); |
| 2643 |
} |
| 2644 |
|
| 2645 |
if (static::is_special_page()) { |
| 2646 |
// Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11 |
| 2647 |
remove_filter('template_redirect', 'redirect_canonical', 10); |
| 2648 |
// dont cache special page. LP 2026-08-25 |
| 2649 |
$this->nocache(); |
| 2650 |
// Do the custom pre-load actions for these pages IOK 2026-09-11 |
| 2651 |
do_action('woo_vipps_before_handling_special_page', $_GET['action']); |
| 2652 |
} |
| 2653 |
} |
| 2654 |
|
| 2655 |
// Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02 |
| 2656 |
public function vipps_special_page_endpoint_title($title, $postid = 0) { |
| 2657 |
global $wp_query; |
| 2658 |
// Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02 |
| 2659 |
|
| 2660 |
// In block themes the whole template (header, footer, content) renders inside the main |
| 2661 |
// loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a |
| 2662 |
// server-rendered mini-cart) - not just the page's own heading. Only replace the title of |
| 2663 |
// the queried page so an earlier title doesn't consume this one-shot filter. |
| 2664 |
if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) { |
| 2665 |
switch ($_GET['action'] ?? '') { |
| 2666 |
case 'wait_for_payment': |
| 2667 |
$title = __('Processing order', 'woo-vipps'); |
| 2668 |
break; |
| 2669 |
case 'do_express_checkout': |
| 2670 |
case 'buy_product': |
| 2671 |
$title = __('Express Checkout', 'woo-vipps'); |
| 2672 |
break; |
| 2673 |
} |
| 2674 |
} |
| 2675 |
return $title; |
| 2676 |
} |
| 2677 |
|
| 2678 |
// Template handling for special pages. IOK 2018-11-21 |
| 2679 |
// This is legacy - the special page is now a real page, so it can have a special template using standard WP methods. IOK 2026-09-11 |
| 2680 |
public function template_include($template) { |
| 2681 |
if (static::is_special_page()) { |
| 2682 |
// Get any special template override from the options IOK 2020-02-18 |
| 2683 |
$specific = $this->gateway()->get_option('vippsspecialpagetemplate'); |
| 2684 |
$found = locate_template($specific,false,false); |
| 2685 |
if ($found) $template=$found; |
| 2686 |
|
| 2687 |
return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? ''); |
| 2688 |
} |
| 2689 |
return $template; |
| 2690 |
} |
| 2691 |
|
| 2692 |
// On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in |
| 2693 |
// the user if using Express Checkout IOK 2020-10-09 |
| 2694 |
public function woocommerce_before_thankyou ($orderid) { |
| 2695 |
$order = wc_get_order($orderid); |
| 2696 |
if ($order) { |
| 2697 |
// Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09 |
| 2698 |
// -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04 |
| 2699 |
$this->maybe_log_in_user($order); |
| 2700 |
$order->delete_meta_data('_vipps_limited_session'); |
| 2701 |
$order->save(); |
| 2702 |
|
| 2703 |
// Now if this was express checkout and we are a guest, ensure we have the correct email in the session->customer array IOK 2023-07-17 |
| 2704 |
if (! is_user_logged_in() ) { |
| 2705 |
$this->maybe_set_session_customer_email($order); |
| 2706 |
} |
| 2707 |
} |
| 2708 |
$this->maybe_restore_cart($orderid); |
| 2709 |
|
| 2710 |
WC()->session->set('current_vipps_session', false); |
| 2711 |
WC()->session->set('vipps_checkout_current_pending',false); |
| 2712 |
WC()->session->set('vipps_address_hash', false); |
| 2713 |
do_action('woo_vipps_before_thankyou', $orderid, $order); |
| 2714 |
} |
| 2715 |
public function woocommerce_loaded() { |
| 2716 |
// Ended buy-now product block support for allproducts block. LP 29.11.2024 |
| 2717 |
|
| 2718 |
/* This is for the other product blocks - here we only have a single HTML filter unfortunately */ |
| 2719 |
add_filter('woocommerce_blocks_product_grid_item_html', function ($html, $data, $product) { |
| 2720 |
if (!$this->loop_single_product_is_express_checkout_purchasable($product)) return $html; |
| 2721 |
$stripped = preg_replace("!</li>$!", "", $html); |
| 2722 |
$pid = $product->get_id(); |
| 2723 |
$button = '<div class="wp-block-button wc-block-components-product-button wc-block-button-vipps">'; |
| 2724 |
$button .= $this->get_buy_now_button($pid,false, null, false, '', 'catalog'); |
| 2725 |
$button .= '</div>'; |
| 2726 |
return $stripped . $button . "</li>"; |
| 2727 |
}, 10, 3); |
| 2728 |
|
| 2729 |
// If local pickup has been added to express/checkout by filters, add this to emails/confirmation pages. IOK 2025-08-15 |
| 2730 |
add_filter('woocommerce_order_shipping_to_display', function($shipping, $order, $tax_display) { |
| 2731 |
if (!is_a($order, 'WC_Order')) return $shipping; |
| 2732 |
if (! self::is_vipps_order($order)) return $shipping; |
| 2733 |
$shipping_method = current( $order->get_shipping_methods() ); |
| 2734 |
|
| 2735 |
if (empty($shipping_method)) return $shipping; |
| 2736 |
|
| 2737 |
// Handled by Woo Central IOK 2025-08-15 |
| 2738 |
if ('pickup_location' == $shipping_method->get_method_id()) { |
| 2739 |
return $shipping; |
| 2740 |
} |
| 2741 |
|
| 2742 |
|
| 2743 |
$details = trim($shipping_method->get_meta( 'pickup_details' )); |
| 2744 |
$location = trim($shipping_method->get_meta( 'pickup_location' )); |
| 2745 |
$address = trim($shipping_method->get_meta( 'pickup_address' )); |
| 2746 |
|
| 2747 |
if (!empty($location) || !empty($address)) { |
| 2748 |
$shipping .= "<br><strong>" . __( 'Pickup location', 'woocommerce' ) . ":</strong>"; |
| 2749 |
} |
| 2750 |
if (!empty($location)) $shipping .= esc_html($location); |
| 2751 |
if (!empty($address)) $shipping .= "<br>" . esc_html($address); |
| 2752 |
if (!empty($details)) $shipping .= "<br><small>" . esc_html($details) . "</small>"; |
| 2753 |
|
| 2754 |
return $shipping; |
| 2755 |
}, 10, 3); |
| 2756 |
|
| 2757 |
|
| 2758 |
// Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter |
| 2759 |
// IOK 2025-11-19 |
| 2760 |
add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4); |
| 2761 |
} |
| 2762 |
|
| 2763 |
public function get_payment_method_name() { |
| 2764 |
return $this->gateway()->get_option('payment_method_name'); |
| 2765 |
} |
| 2766 |
|
| 2767 |
public function plugins_loaded() { |
| 2768 |
/* The gateway is added at 'plugins_loaded' and instantiated by Woo itself. IOK 2018-02-07 */ |
| 2769 |
add_filter( 'woocommerce_payment_gateways', array($this,'woocommerce_payment_gateways' )); |
| 2770 |
/* Try to get a list of all installed gateways *before* we instantiate our own IOK 2024-05-27 */ |
| 2771 |
add_filter( 'woocommerce_payment_gateways', function ($gws) { |
| 2772 |
if (!empty(Vipps::$installed_gateways)) return Vipps::$installed_gateways; |
| 2773 |
Vipps::$installed_gateways = $gws; |
| 2774 |
return $gws; |
| 2775 |
}, 99999); |
| 2776 |
} |
| 2777 |
|
| 2778 |
public function after_setup_theme() { |
| 2779 |
// To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest. |
| 2780 |
$ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages"); |
| 2781 |
|
| 2782 |
// Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist |
| 2783 |
// Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this |
| 2784 |
// is important. |
| 2785 |
add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1); |
| 2786 |
|
| 2787 |
// Callbacks use the Woo API IOK 2018-05-18 |
| 2788 |
add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback')); |
| 2789 |
add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback')); |
| 2790 |
|
| 2791 |
// Currently this sets Vipps as default payment method if hooked. IOK 2018-06-06 |
| 2792 |
add_action( 'woocommerce_cart_updated', array($this,'woocommerce_cart_updated')); |
| 2793 |
|
| 2794 |
// Template integrations |
| 2795 |
add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button')); |
| 2796 |
add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30); |
| 2797 |
|
| 2798 |
// Previously we added an express html banner to the action 'woocommerce_before_checkout_form.', |
| 2799 |
// replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23 |
| 2800 |
add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5); |
| 2801 |
|
| 2802 |
add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button')); |
| 2803 |
add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20); |
| 2804 |
|
| 2805 |
|
| 2806 |
// Special pages and callbacks handled by template_redirect. IOK 2023-02-22 |
| 2807 |
add_action('template_redirect', array($this,'template_redirect'),1); |
| 2808 |
|
| 2809 |
// Allow overriding their templates |
| 2810 |
add_filter('template_include', array($this,'template_include'), 10, 1); |
| 2811 |
|
| 2812 |
// Ajax endpoints for checking the order status while waiting for confirmation |
| 2813 |
add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status')); |
| 2814 |
add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status')); |
| 2815 |
|
| 2816 |
|
| 2817 |
// Buying a single product directly using express checkout IOK 2018-09-28 |
| 2818 |
add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product')); |
| 2819 |
add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product')); |
| 2820 |
|
| 2821 |
// This is for express checkout which we will also do asynchronously IOK 2018-05-28 |
| 2822 |
add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout')); |
| 2823 |
add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout')); |
| 2824 |
|
| 2825 |
// Same thing, but for single products IOK 2018-05-28 |
| 2826 |
add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout')); |
| 2827 |
add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout')); |
| 2828 |
|
| 2829 |
// Handle the cancel unpaid order action when the "hold stock" times out. |
| 2830 |
// For *normal* vipps orders, we run another cronjob every 5. minute which checks order status, |
| 2831 |
// therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return. |
| 2832 |
// For Checkout the rules are different though. |
| 2833 |
add_filter('woocommerce_cancel_unpaid_order', function ($cancel, $order) { |
| 2834 |
|
| 2835 |
// If we can't cancel for some other reason, don't. |
| 2836 |
if (!$cancel) return $cancel; |
| 2837 |
|
| 2838 |
// Only check Vipps orders |
| 2839 |
if (! self::is_vipps_order($order)) return $cancel; |
| 2840 |
|
| 2841 |
// For Vipps, all unpaid orders must be pending. |
| 2842 |
if ($order->get_status() != 'pending' && $order->get_status() != 'failed') return $cancel; |
| 2843 |
|
| 2844 |
// Handle this separately, in the Checkout class. IOK 2025-10-08 |
| 2845 |
$checkout_session = $order->get_meta('_vipps_checkout_session'); |
| 2846 |
if ($checkout_session) { |
| 2847 |
$exception = null; |
| 2848 |
try { |
| 2849 |
$polldata = $this->gateway()->api->checkout_get_session_info($order); |
| 2850 |
$sessionState = (!empty($polldata) && is_array($polldata) && isset($polldata['sessionState'])) ? $polldata['sessionState'] : ""; |
| 2851 |
// We can cancel the order iff we haven't started payment yet. |
| 2852 |
if ($sessionState == 'PaymentSuccessful' || $sessionState == 'PaymentInitiated') return false; |
| 2853 |
return true; |
| 2854 |
} catch (VippsAPIException $e) { |
| 2855 |
$resp = intval($e->responsecode); |
| 2856 |
if ($resp == 402 || $resp == 404) { |
| 2857 |
// We don't know about this transaction, so allow cancel IOK 2026-04-29 |
| 2858 |
return true; |
| 2859 |
} |
| 2860 |
$exception = $e; // Unknown exception, handle below |
| 2861 |
} catch (Exception $e) { |
| 2862 |
$exception = $e; // Unknown exception, handle below |
| 2863 |
} |
| 2864 |
if ($exception) { |
| 2865 |
// If Vipps is unreachable, be safe and don't delete |
| 2866 |
$this->log("Checkout: " . sprintf(__("Cannot get status of %1\$d at %2\$s in woocommerce_cancel_unpaid_order, not allowing deletion: %3\$s", 'woo-vipps'), $order->get_id(), Vipps::CompanyName(), $exception->getMessage())); |
| 2867 |
return false; |
| 2868 |
} |
| 2869 |
return false; |
| 2870 |
} |
| 2871 |
|
| 2872 |
// Epayment/non-checkout IOK 2026-04-29 |
| 2873 |
// Keep in mind, checkout will fall through to here if the checkout session initialization failed. LP 2026-04-29 |
| 2874 |
try { |
| 2875 |
$exception = null; |
| 2876 |
$result = $this->gateway()->api->epayment_get_payment($order); |
| 2877 |
} catch (VippsAPIException $e) { |
| 2878 |
$resp = intval($e->responsecode); |
| 2879 |
if ($resp == 402 || $resp == 404) { |
| 2880 |
// We don't know about this transaction, so allow cancel IOK 2026-04-29 |
| 2881 |
return true; |
| 2882 |
} |
| 2883 |
$exception = $e; // Unknown exception, handle below |
| 2884 |
} catch (Exception $e) { |
| 2885 |
$exception = $e; // Unknown exception, handle below |
| 2886 |
} |
| 2887 |
|
| 2888 |
if ($exception) { |
| 2889 |
// If Vipps is unreachable, be safe and don't delete |
| 2890 |
$this->log(sprintf(__("Cannot get status of %1\$d at %2\$s in woocommerce_cancel_unpaid_order, not allowing deletion: %3\$s", 'woo-vipps'), $order->get_id(), Vipps::CompanyName(), $exception->getMessage())); |
| 2891 |
return false; |
| 2892 |
} |
| 2893 |
|
| 2894 |
// We should now have an object with the 'state' in one of the Vipps states. We'll translate all of them to |
| 2895 |
// cancelled or nah, and if cancelled, we allow deletion. IOK 2025-10-07 |
| 2896 |
if (empty($result)) return true; |
| 2897 |
$state = $this->gateway()->interpret_vipps_order_status($result['state'] ?? 'CANCEL'); |
| 2898 |
if (empty($state) || $state == 'cancelled') return true; |
| 2899 |
|
| 2900 |
return false; |
| 2901 |
|
| 2902 |
}, 20, 2); |
| 2903 |
|
| 2904 |
// Used both in admin and non-admin-scripts, load as quick as possible IOK 2020-09-03 |
| 2905 |
$this->vippsJSConfig = array(); |
| 2906 |
$this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php'); |
| 2907 |
$this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps'); |
| 2908 |
$this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name()); |
| 2909 |
$this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__); |
| 2910 |
$this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__); |
| 2911 |
$this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name()); |
| 2912 |
$this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name()); |
| 2913 |
$this->vippsJSConfig['vippslanguage'] = $this->get_customer_language(); |
| 2914 |
$this->vippsJSConfig['vippslocale'] = get_locale(); |
| 2915 |
$this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name(); |
| 2916 |
|
| 2917 |
|
| 2918 |
// If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10 |
| 2919 |
if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) { |
| 2920 |
// Ensure gateways are loaded at this point IOK 2026-05-27 |
| 2921 |
require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php'); |
| 2922 |
require_once(dirname(__FILE__) . '/WC_Gateway_Vipps.class.php'); |
| 2923 |
|
| 2924 |
// Then the payment blocks |
| 2925 |
require_once(dirname(__FILE__) . "/Blocks/Payment/Vipps.class.php"); |
| 2926 |
require_once(dirname(__FILE__) . "/Blocks/Payment/VippsCard.class.php"); |
| 2927 |
Automattic\WooCommerce\Blocks\Payments\Integrations\Vipps::register(); |
| 2928 |
Automattic\WooCommerce\Blocks\Payments\Integrations\VippsCard::register(); |
| 2929 |
} |
| 2930 |
|
| 2931 |
// Used for e.g. labels of product/shipping metadata. IOK 2025-05-07 |
| 2932 |
add_filter('woocommerce_attribute_label', function ($label, $name, $product) { |
| 2933 |
if ( $product ) { |
| 2934 |
return $label; |
| 2935 |
} |
| 2936 |
switch ( $name ) { |
| 2937 |
case 'brand': // This is for shipping IOK 2025-05-07 |
| 2938 |
return __('Company', 'woo-vipps'); |
| 2939 |
case 'type': |
| 2940 |
return __('Type', 'woo-vipps'); |
| 2941 |
case 'vipps_delivery_timeslot': |
| 2942 |
return __('Timeslot', 'woo-vipps'); |
| 2943 |
case 'vipps_delivery_timeslot_id': |
| 2944 |
return __('Timeslot ID', 'woo-vipps'); |
| 2945 |
} |
| 2946 |
return $label; |
| 2947 |
}, 9, 3); |
| 2948 |
|
| 2949 |
|
| 2950 |
} |
| 2951 |
|
| 2952 |
// IOK 2021-12-09 try to get the current language in the format Vipps wants, one of 'en' and 'no' |
| 2953 |
// IOK 2025-09-03 stop trying to get the logged-in users language - it does not seem to work especially well in newer woos. |
| 2954 |
public function get_customer_language() { |
| 2955 |
global $TRP_LANGUAGE; // TranslatePress IOK 2025-11-06 |
| 2956 |
|
| 2957 |
$language = substr(get_bloginfo('language'),0,2); |
| 2958 |
if (function_exists('pll_current_language')) { |
| 2959 |
$pll_language = pll_current_language('slug'); |
| 2960 |
if ($pll_language) $language = $pll_language; |
| 2961 |
} elseif (has_filter('wpml_current_language')){ |
| 2962 |
$language=apply_filters('wpml_current_language',null); |
| 2963 |
} elseif (!empty($TRP_LANGUAGE)) { |
| 2964 |
$language = sanitize_title($TRP_LANGUAGE); |
| 2965 |
} |
| 2966 |
// Just to be sure. |
| 2967 |
$language = strtolower($language); |
| 2968 |
|
| 2969 |
// Allow others to override in case they have some unorthodox setups IOK 2025-11-12 |
| 2970 |
$language = apply_filters('woo_vipps_customer_language', $language); |
| 2971 |
|
| 2972 |
if ($language == 'nb' || $language == 'nn') $language = 'no'; |
| 2973 |
if ($language == 'da') $language = 'dk'; |
| 2974 |
if ($language == 'sv') $language = 'se'; |
| 2975 |
if (! in_array($language, ['en', 'no', 'dk', 'fi', 'se'])) $language = 'en'; |
| 2976 |
return $language; |
| 2977 |
} |
| 2978 |
|
| 2979 |
// Called by ajax on the order page; redirects back to same page. IOK 2022-11-02 |
| 2980 |
public function order_handle_vipps_action () { |
| 2981 |
check_ajax_referer('vippssecnonce','vipps_sec'); |
| 2982 |
static::set_locale_if_in_header(); |
| 2983 |
$order = wc_get_order(intval($_REQUEST['orderid'])); |
| 2984 |
if (!is_a($order, 'WC_Order')) return; |
| 2985 |
$pm = $order->get_payment_method(); |
| 2986 |
if (!self::is_vipps_order($pm)) return; |
| 2987 |
|
| 2988 |
$action = isset($_REQUEST['do']) ? sanitize_title($_REQUEST['do']) : 'none'; |
| 2989 |
|
| 2990 |
if ($action == 'do_capture') { |
| 2991 |
$gw = $this->gateway(); |
| 2992 |
$ok = $gw->maybe_capture_payment($order->get_id()); |
| 2993 |
} |
| 2994 |
print "1"; |
| 2995 |
} |
| 2996 |
|
| 2997 |
// Rest route: returns wc products, but only those purchasable by VMP express checkout. LP 2026-01-22 |
| 2998 |
// Called by the buy-now express block. LP 2026-01-22 |
| 2999 |
public function rest_express_checkout_products($request) { |
| 3000 |
static::set_locale_if_in_header(); |
| 3001 |
|
| 3002 |
// Redirect product fetch to WC rest api. LP 2026-01-23 |
| 3003 |
$wc_request = new WP_REST_Request('GET', '/wc/store/v1/products'); |
| 3004 |
$wc_request->set_query_params($request->get_query_params()); |
| 3005 |
$response = rest_do_request($wc_request); |
| 3006 |
if ($response->is_error()) { |
| 3007 |
return $response; |
| 3008 |
} |
| 3009 |
$products = $response->get_data(); |
| 3010 |
|
| 3011 |
// Extract variant products out from the parent product, so we can support these. LP 2026-01-22 |
| 3012 |
foreach($products as &$product) { |
| 3013 |
if (!(isset($product['variations']) && is_array($product['variations']) && $product['variations'])) continue; |
| 3014 |
|
| 3015 |
foreach($product['variations'] as $variation) { |
| 3016 |
$v = wc_get_product($variation->id); |
| 3017 |
if (!is_a($v, 'WC_Product')) continue; |
| 3018 |
$products[] = [ |
| 3019 |
'is_variation' => true, |
| 3020 |
'parent' => $product['id'], |
| 3021 |
'id' => $v->get_id(), |
| 3022 |
'sku' => $v->get_sku(), |
| 3023 |
'type' => $v->get_type(), |
| 3024 |
'slug' => $v->get_slug(), |
| 3025 |
'name' => $v->get_name() |
| 3026 |
]; |
| 3027 |
} |
| 3028 |
} |
| 3029 |
|
| 3030 |
// Filter only Express-purchaseable products, variant parents should also be removed here. LP 2026-01-22 |
| 3031 |
$filtered_products = array_filter($products, fn($p) => $this->loop_single_product_is_express_checkout_purchasable(wc_get_product($p['id']))); |
| 3032 |
// Reindex array to fix output. LP 2026-01-22 |
| 3033 |
$filtered_products = array_values($filtered_products); |
| 3034 |
$response->set_data($filtered_products); |
| 3035 |
return $response; |
| 3036 |
|
| 3037 |
} |
| 3038 |
|
| 3039 |
// Make admin-notices persistent so we can provide error messages whenever possible. IOK 2018-05-11 |
| 3040 |
public function store_admin_notices() { |
| 3041 |
// WooCommerce will (now) call this function in the inject_before_notices method. If it does not exist, |
| 3042 |
// we get a crash. If there is no "current screen", then we cannot provide these. |
| 3043 |
if (!function_exists('get_current_screen')) return false; |
| 3044 |
ob_start(); |
| 3045 |
do_action('vipps_admin_notices'); |
| 3046 |
$notices = ob_get_clean(); |
| 3047 |
set_transient('_vipps_save_admin_notices',$notices, 5*60); |
| 3048 |
} |
| 3049 |
|
| 3050 |
|
| 3051 |
public function order_item_add_action_buttons ($order) { |
| 3052 |
$this->order_item_add_capture_button($order); |
| 3053 |
} |
| 3054 |
|
| 3055 |
public function order_item_add_capture_button ($order) { |
| 3056 |
$pm = $order->get_payment_method(); |
| 3057 |
if (!self::is_vipps_order($pm)) return; |
| 3058 |
$status = $order->get_status(); |
| 3059 |
|
| 3060 |
$show_capture_button = ($status == 'on-hold' || $status == 'processing'); |
| 3061 |
if (!apply_filters('woo_vipps_show_capture_button', $show_capture_button, $order)) { |
| 3062 |
return; |
| 3063 |
} |
| 3064 |
|
| 3065 |
$captured = intval($order->get_meta('_vipps_captured')); |
| 3066 |
// noncapturable should never be greater than capture remaining, so this *should* not be negative. LP 2026-06-12 |
| 3067 |
$capremain = intval($order->get_meta('_vipps_capture_remaining')) - intval($order->get_meta('_vipps_noncapturable')); |
| 3068 |
if ($captured && (!$capremain || $capremain < 2)) { |
| 3069 |
print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>"; |
| 3070 |
return; |
| 3071 |
} |
| 3072 |
|
| 3073 |
$logo = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__); |
| 3074 |
|
| 3075 |
print '<button type="button" class="button vippsbutton generate-items vipps-action" |
| 3076 |
data-orderid="' . $order->get_id() . '" data-action="do_capture" |
| 3077 |
style="background-color:#ff5b24;border-color:#ff5b24;color:#ffffff" > |
| 3078 |
<img border=0 style="display:inline;height:2ex;vertical-align:text-bottom" class="inline" alt=0 src="'.$logo.'"/> ' . __('Capture payment','woo-vipps') . '</button>'; |
| 3079 |
|
| 3080 |
} |
| 3081 |
|
| 3082 |
|
| 3083 |
// This is the main callback from Vipps when payments are returned. IOK 2018-04-20 |
| 3084 |
public function vipps_callback() { |
| 3085 |
$this->log("Callback received"); |
| 3086 |
|
| 3087 |
Vipps::nocache(); |
| 3088 |
// Required for Checkout, we send this early as error recovery here will be tricky anyhow. |
| 3089 |
status_header(202, "Accepted"); |
| 3090 |
|
| 3091 |
|
| 3092 |
$raw_post = @file_get_contents( 'php://input' ); |
| 3093 |
$result = @json_decode($raw_post,true); |
| 3094 |
|
| 3095 |
// This handler handles both Checkout and Vipps ECom IOK 2021-09-02 |
| 3096 |
// .. and the epayment webhooks 2023-12-19 |
| 3097 |
$ischeckout = false; |
| 3098 |
$iswebhook = false; |
| 3099 |
$callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : ""; |
| 3100 |
// For Checkout v3 and onwards, we control the callback so the type is just this field |
| 3101 |
if ($callback == 'checkout') { |
| 3102 |
$ischeckout = true; |
| 3103 |
} |
| 3104 |
// For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19 |
| 3105 |
if ($callback == 'webhook' || (!$ischeckout && ($result['pspReference'] ?? false))) { |
| 3106 |
$iswebhook = true; |
| 3107 |
} |
| 3108 |
|
| 3109 |
$vippsorderid = ($result && isset($result['orderId'])) ? $result['orderId'] : ""; |
| 3110 |
// For checkout, the orderId has been renamed to "reference" IOK 2022-02-11 |
| 3111 |
// We set the orderId here very early so old filters and hooks will continue working - mostly used for debugging. |
| 3112 |
if (!$vippsorderid && $result && isset($result['reference'])) { |
| 3113 |
$vippsorderid = $result['reference']; |
| 3114 |
$result['orderId'] = $result['reference']; |
| 3115 |
} |
| 3116 |
|
| 3117 |
do_action('woo_vipps_vipps_callback', $result,$raw_post); |
| 3118 |
|
| 3119 |
if (!$result) { |
| 3120 |
$error = json_last_error_msg(); |
| 3121 |
$this->log(sprintf(__("Did not understand callback from %1\$s:",'woo-vipps'), $this->get_payment_method_name()) . " " . $raw_post, 'error'); |
| 3122 |
$this->log(sprintf(__("Error was: %1\$s",'woo-vipps'), $error)); |
| 3123 |
return false; |
| 3124 |
} |
| 3125 |
|
| 3126 |
// For testing sites that appear not to receive callbacks |
| 3127 |
if (isset($result['testing_callback'])) { |
| 3128 |
$this->log(__("Received a test callback, exiting" , 'woo-vipps'), 'debug'); |
| 3129 |
print '{"status": 1, "msg": "Test ok"}'; |
| 3130 |
exit(); |
| 3131 |
} |
| 3132 |
|
| 3133 |
// If this is a webhook call, we need to verify it, check that it is one of the 'callback' webhooks, check that we still have a pending |
| 3134 |
// order for it, normalize the callback data and then handle the callback. IOK 2023-12-21 |
| 3135 |
if ($iswebhook) { |
| 3136 |
// The webhook payloads spell the msn differently. IOK 2023-12-21 |
| 3137 |
$msn = ($result['msn'] ?? '') ? $result['msn'] : ($result['merchantSerialNumber'] ?? ''); |
| 3138 |
if ($msn) { |
| 3139 |
$result['msn'] = $msn; |
| 3140 |
$result['merchantSerialNumber'] = $msn; |
| 3141 |
} |
| 3142 |
$hookdata = $this->gateway()->get_local_webhook($msn); |
| 3143 |
$secret = $hookdata ? ($hookdata['secret'] ?? false) : false; |
| 3144 |
if (!$secret) { |
| 3145 |
$this->log(sprintf(__('Cannot verify webhook callback for order %1$s - this shop does not know the secret. You should delete all unwanted webhooks. If you are using the same MSN on several shops, this callback is probably for one of the others.', 'woo-vipps'), $vippsorderid), 'debug'); |
| 3146 |
return false; |
| 3147 |
} |
| 3148 |
$verified = $this->verify_webhook($raw_post, $secret); |
| 3149 |
if (!$verified) { |
| 3150 |
$this->log(sprintf(__('Cannot verify webhook callback for order %1$s - signature does not match. This may be an attempt to forge callbacks', 'woo-vipps'), $vippsorderid), 'debug'); |
| 3151 |
return; |
| 3152 |
} |
| 3153 |
|
| 3154 |
// We need to check if this is a payment event, or if not, and if it is, if it is one of the ones we are prepared to handle. IOK 2023-12-21 |
| 3155 |
$event = $result['name'] ?? ''; |
| 3156 |
$payment_events = ["CREATED", "ABORTED", "EXPIRED", "CANCELLED", "CAPTURED", "REFUNDED", "AUTHORIZED", "TERMINATED"]; |
| 3157 |
$callback_events = ["ABORTED","EXPIRED", "AUTHORIZED", "TERMINATED"]; |
| 3158 |
|
| 3159 |
// If this is a payment event, we should have an order too so try to retrieve it. IOK 2023-12-21 |
| 3160 |
$order = null; |
| 3161 |
$pending = false; |
| 3162 |
if ($vippsorderid && $msn && in_array($event, $payment_events)) { |
| 3163 |
// Then check if the reference/vippsorderid is a pending order |
| 3164 |
$order = $this->get_pending_vipps_order($vippsorderid); |
| 3165 |
if ($order) { |
| 3166 |
$pending = true; |
| 3167 |
} else { |
| 3168 |
// If it isn't, but it is a payment event, get the order id from the epayment metadata. IOK 2023-12-21 |
| 3169 |
try { |
| 3170 |
$polldata = $this->gateway()->api->epayment_get_payment($vippsorderid, $msn); |
| 3171 |
if ($polldata && isset($polldata['metadata'])) { |
| 3172 |
$orderid = $polldata['metadata']['orderid']; |
| 3173 |
if ($orderid) { |
| 3174 |
$order = wc_get_order($orderid); |
| 3175 |
if (!$order || $vippsorderid != $order->get_meta('_vipps_orderid')) { |
| 3176 |
$this->log( |
| 3177 |
sprintf(__('The reference %1$s and order id %2$s does not match in webhook event %3$s - callback is invalid for the order.', 'woo-vipps'), |
| 3178 |
$vippsorderid, $orderid, $event), 'debug'); |
| 3179 |
$order = null; |
| 3180 |
return; |
| 3181 |
$order = null; |
| 3182 |
} |
| 3183 |
} |
| 3184 |
} |
| 3185 |
} catch (Exception $e) { |
| 3186 |
$this->log(sprintf(__("Could not get orderid of reference %2\$s from %1\$s: ", 'woo-vipps'), Vipps::CompanyName(), $vippsorderid) . $e->getMessage(), 'debug'); |
| 3187 |
} |
| 3188 |
} |
| 3189 |
} |
| 3190 |
|
| 3191 |
// This will run for all events, not just the one this handler handles IOK 2023-12-21 |
| 3192 |
do_action('woo_vipps_webhook_event', $result, $order); |
| 3193 |
|
| 3194 |
// We are not interested in Checkout orders - they have their own callback systems |
| 3195 |
if ($order && $order->get_meta('_vipps_checkout')) { |
| 3196 |
$this->log(sprintf(__('Received webhook callback for Checkout order %1$d - ignoring since full callback should come', 'woo-vipps'), $order->get_id()), 'debug'); |
| 3197 |
return; |
| 3198 |
} |
| 3199 |
// Now we will handle everything that is a callback event. IOK 2023-12-21 |
| 3200 |
if (!in_array($event, $callback_events)) { |
| 3201 |
return; |
| 3202 |
} |
| 3203 |
|
| 3204 |
if (!$pending) { |
| 3205 |
// If the order is no longer pending, then we can safely ignore it. IOK 2023-12-21 |
| 3206 |
$this->log(sprintf(__('Received webhook callback for order %1$s but this is no longer pending.', 'woo-vipps'), $vippsorderid), 'debug'); |
| 3207 |
return; |
| 3208 |
} |
| 3209 |
do_action('woo_vipps_callback_webhook', $result); |
| 3210 |
|
| 3211 |
$ok = $this->gateway()->handle_callback($result, $order, false, $iswebhook); |
| 3212 |
if ($ok) { |
| 3213 |
// This runs only if the callback actually handled the order, if not, then the order was handled by poll. |
| 3214 |
do_action('woo_vipps_callback_handled_order', $order); |
| 3215 |
} |
| 3216 |
|
| 3217 |
exit(); |
| 3218 |
} |
| 3219 |
|
| 3220 |
// This branch is only for non-webhook callbacks; which currently means Checkout only. IOK 2025-08-13 |
| 3221 |
$orderid = intval(@$_REQUEST['id']); |
| 3222 |
|
| 3223 |
if (!$orderid) { |
| 3224 |
$this->log(sprintf(__("There is no order with this %1\$s orderid, callback fails:",'woo-vipps'), $this->get_payment_method_name()) . " " . $vippsorderid, 'error'); |
| 3225 |
return false; |
| 3226 |
} |
| 3227 |
|
| 3228 |
$order = wc_get_order($orderid); |
| 3229 |
if (!is_a($order, 'WC_Order')) { |
| 3230 |
$this->log(__("There is no order with this order id, callback fails:",'woo-vipps') . " " . $orderid, 'error'); |
| 3231 |
return false; |
| 3232 |
} |
| 3233 |
|
| 3234 |
// a small bit of security |
| 3235 |
if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) { |
| 3236 |
$this->log("Wrong authtoken on Vipps payment details callback", 'error'); |
| 3237 |
exit(); |
| 3238 |
} |
| 3239 |
|
| 3240 |
do_action('woo_vipps_callback_checkout', $result); |
| 3241 |
|
| 3242 |
$gw = $this->gateway(); |
| 3243 |
|
| 3244 |
// If neccessary, the order session will be restored in this method, and if so it will be reset before the exit happens |
| 3245 |
// to reduce issues with users simultaneously returning to the store. IOK 2023-07-18 |
| 3246 |
$ok = $gw->handle_callback($result, $order, $ischeckout); |
| 3247 |
if ($ok) { |
| 3248 |
// This runs only if the callback actually handled the order, if not, then the order was handled by poll. |
| 3249 |
do_action('woo_vipps_callback_handled_order', $order); |
| 3250 |
} |
| 3251 |
|
| 3252 |
exit(); |
| 3253 |
} |
| 3254 |
|
| 3255 |
// Returns true iff we can verify that the webhook we just received is valid and that we know its secret IOK 2023-12-21 |
| 3256 |
public function verify_webhook($serialized, $secret) { |
| 3257 |
// Extract the necessary headers. |
| 3258 |
$expected_auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['HTTP_X_VIPPS_AUTHORIZATION'] ?? ""); |
| 3259 |
$expected_date = $_SERVER['HTTP_X_MS_DATE'] ?? ''; |
| 3260 |
|
| 3261 |
// Check if the date header is present and within an acceptable range (e.g., +/- 5 minutes) NT 2023-12-22 |
| 3262 |
if (!$this->isDateValid($expected_date)) { |
| 3263 |
return false; // Date is not valid or not within the acceptable range |
| 3264 |
} |
| 3265 |
|
| 3266 |
// Prepare the data for signing. |
| 3267 |
$hashed_payload = base64_encode(hash('sha256', $serialized, true)); |
| 3268 |
$path_and_query = $_SERVER['REQUEST_URI']; |
| 3269 |
$host = $_SERVER['HTTP_HOST']; |
| 3270 |
|
| 3271 |
// Construct the string to sign. |
| 3272 |
$toSign = "POST\n{$path_and_query}\n{$expected_date};{$host};{$hashed_payload}"; |
| 3273 |
|
| 3274 |
// Generate the HMAC signature. |
| 3275 |
$signature = base64_encode(hash_hmac('sha256', $toSign, $secret, true)); |
| 3276 |
|
| 3277 |
// Construct the authorization string. |
| 3278 |
$auth = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={$signature}"; |
| 3279 |
|
| 3280 |
// Compare the generated auth string with the expected one. |
| 3281 |
// Hash_equals is used to mitigate timing attacks NT 2023-12-22 |
| 3282 |
return hash_equals($auth, $expected_auth); |
| 3283 |
} |
| 3284 |
|
| 3285 |
// Helper function to validate the date NT 2023-12-22 |
| 3286 |
private function isDateValid($dateHeader) { |
| 3287 |
// Define the acceptable time leeway (e.g., 5 minutes) |
| 3288 |
$leewayInSeconds = 300; |
| 3289 |
|
| 3290 |
// Convert the header date to a Unix timestamp |
| 3291 |
$headerTime = strtotime($dateHeader); |
| 3292 |
|
| 3293 |
// Check if the date is valid |
| 3294 |
if ($headerTime === false) { |
| 3295 |
return false; // Invalid date |
| 3296 |
} |
| 3297 |
|
| 3298 |
// Get the current time |
| 3299 |
$currentTime = time(); |
| 3300 |
|
| 3301 |
// Check if the date is within the acceptable range |
| 3302 |
return abs($currentTime - $headerTime) <= $leewayInSeconds; |
| 3303 |
} |
| 3304 |
|
| 3305 |
|
| 3306 |
// Helper function to get ISO-3166 two-letter country codes from country names as supplied by Vipps |
| 3307 |
// IOK 2021-11-22 Seems as if Vipps is now sending two-letter country codes at least some times |
| 3308 |
public function country_to_code($countryname) { |
| 3309 |
if (!$this->countrymap) $this->countrymap = unserialize(file_get_contents(dirname(__FILE__) . "/lib/countrycodes.php")); |
| 3310 |
$mapped = @$this->countrymap[strtoupper($countryname)]; |
| 3311 |
$code = WC()->countries->get_base_country(); |
| 3312 |
if ($mapped) { |
| 3313 |
$code = $mapped; |
| 3314 |
} else if (strlen($countryname)==2) { |
| 3315 |
$code = strtoupper($countryname); |
| 3316 |
} |
| 3317 |
$code = apply_filters('woo_vipps_country_to_code', $code, $countryname); |
| 3318 |
return $code; |
| 3319 |
} |
| 3320 |
|
| 3321 |
// To be added to the 'woocommerce_session_handler' filter IOK 2021-06-21 |
| 3322 |
public static function getCallbackSessionClass ($handler) { |
| 3323 |
return "VippsCallbackSessionHandler"; |
| 3324 |
} |
| 3325 |
|
| 3326 |
// Go back to the basic woocommerce session handler if we have temporarily restored session from an Vipps order 2021-06-21 |
| 3327 |
// Only to be called by wp-cron, callbacks etc. Will not actually destroy the stored session, just the current session. |
| 3328 |
public function callback_destroy_session () { |
| 3329 |
$this->callbackorder = null; |
| 3330 |
remove_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass')); |
| 3331 |
if (version_compare(WC_VERSION, '3.6.4', '>=')) { |
| 3332 |
// This will replace the old session with this one. IOK 2019-10-22 |
| 3333 |
WC()->initialize_session(); |
| 3334 |
} else { |
| 3335 |
// Do this manually for 3.6.3 and below |
| 3336 |
WC()->session = new WC_Session_Handler(); |
| 3337 |
WC()->session->init(); |
| 3338 |
} |
| 3339 |
} |
| 3340 |
|
| 3341 |
// When we get callbacks from Vipps, we want to restore the Woo session in place for the order. |
| 3342 |
// For many plugins this is strictly neccessary because they don't check to see if there is a session |
| 3343 |
// or not - and for many others, wrong results are produced without the (correct) session. IOK 2019-10-22 |
| 3344 |
public function callback_restore_session ($orderid) { |
| 3345 |
$this->callbackorder = $orderid; |
| 3346 |
require_once(dirname(__FILE__) . "/VippsCallbackSessionHandler.class.php"); |
| 3347 |
add_filter('woocommerce_session_handler', array('Vipps', 'getCallbackSessionClass')); |
| 3348 |
// Support older versions of Woo by inlining initialize session IOK 2019-12-12 |
| 3349 |
if (version_compare(WC_VERSION, '3.6.4', '>=')) { |
| 3350 |
// This will replace the old session with this one. IOK 2019-10-22 |
| 3351 |
WC()->initialize_session(); |
| 3352 |
} else { |
| 3353 |
// Do this manually for 3.6.3 and below |
| 3354 |
$session_class = "VippsCallbackSessionHandler"; |
| 3355 |
WC()->session = new $session_class(); |
| 3356 |
WC()->session->init(); |
| 3357 |
} |
| 3358 |
|
| 3359 |
$customerid= 0; |
| 3360 |
if (WC()->session && is_a(WC()->session, 'WC_Session_Handler')) { |
| 3361 |
$customerid = WC()->session->get('express_customer_id'); |
| 3362 |
} |
| 3363 |
if ($customerid) { |
| 3364 |
WC()->customer = new WC_Customer($customerid); // Reset from session, logged in user |
| 3365 |
} else { |
| 3366 |
WC()->customer = new WC_Customer(); // Reset from session |
| 3367 |
} |
| 3368 |
// This is to provide defaults; real address will come from Vipps in this sitation. IOK 2019-10-25 |
| 3369 |
WC()->customer->set_billing_address_to_base(); |
| 3370 |
WC()->customer->set_shipping_address_to_base(); |
| 3371 |
|
| 3372 |
// The normal "restore cart from session" thing runs on wp_loaded, and only there, and cannot |
| 3373 |
// be called from outside the WC_Cart object. We cannot easily run this on wp_loaded, and it does |
| 3374 |
// do much more than it should for this particular use: |
| 3375 |
// We have already created the order, so we only want this cart for the shipping calculations. |
| 3376 |
// Therefore, we will just recreate the 'data' bit of the contents and set the cart contents directly |
| 3377 |
// from the now restored session. IOK 2020-04-08 |
| 3378 |
// IOK 2022-06-28 Updated to also call the woocommerce_get_cart_item_from_session filters and to correctly handle |
| 3379 |
// coupons. |
| 3380 |
$newcart = array(); |
| 3381 |
if (WC()->session->get('cart', false)) { |
| 3382 |
foreach(WC()->session->get('cart',[]) as $key => $values) { |
| 3383 |
$product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] ); |
| 3384 |
$session_data = array_merge($values, array( 'data' => $product)); |
| 3385 |
$newcart[$key] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key ); |
| 3386 |
} |
| 3387 |
} else { |
| 3388 |
$this->log(sprintf(__("Could not restore cart from session of order %1\$d", 'woo-vipps'), $orderid)); |
| 3389 |
} |
| 3390 |
if (WC()->cart) { |
| 3391 |
|
| 3392 |
// When doing "calculate_totals" on a cart, Woo will now compare "previous shipping methods" with |
| 3393 |
// "current shipping methods" and reset the chosen shipping methods even if it is still available. |
| 3394 |
// This becomes a problem because Woo only loads the pickup location methods in a few places - mostly checkout - |
| 3395 |
// so if we chose a shipping method while these were available, we'd get ourselves reset just by calculating |
| 3396 |
// cart totals. Fix this by saving and restoring this value. IOK 2025-11-05 |
| 3397 |
$all_chosen = WC()->session->get( 'chosen_shipping_methods' ); |
| 3398 |
|
| 3399 |
WC()->cart->set_totals( WC()->session->get( 'cart_totals', null ) ); |
| 3400 |
WC()->cart->set_applied_coupons( WC()->session->get( 'applied_coupons', array() ) ); |
| 3401 |
WC()->cart->set_coupon_discount_totals( WC()->session->get( 'coupon_discount_totals', array() ) ); |
| 3402 |
WC()->cart->set_coupon_discount_tax_totals( WC()->session->get( 'coupon_discount_tax_totals', array() ) ); |
| 3403 |
WC()->cart->set_removed_cart_contents( WC()->session->get( 'removed_cart_contents', array() ) ); |
| 3404 |
WC()->cart->set_cart_contents($newcart); |
| 3405 |
// IOK 2020-07-01 plugins expect this to be called: hopefully they'll not get confused by it happening twice |
| 3406 |
do_action( 'woocommerce_cart_loaded_from_session', WC()->cart); |
| 3407 |
WC()->cart->calculate_totals(); // And if any of them changed anything, recalculate the totals again! |
| 3408 |
// See above: Reset chosen shipping methods to avoid having it be reset by Woo for no good reason. |
| 3409 |
if ($all_chosen) { |
| 3410 |
WC()->session->set('chosen_shipping_methods', $all_chosen); |
| 3411 |
} |
| 3412 |
} else { |
| 3413 |
// Apparently this happens quite a lot, so don't log it or anything. IOK 2021-06-21 |
| 3414 |
} |
| 3415 |
return WC()->session; |
| 3416 |
} |
| 3417 |
|
| 3418 |
|
| 3419 |
|
| 3420 |
// Based on either a logged-in user, or the stores' default address, get the address to use when using |
| 3421 |
// the Express Checkout static shipping feature |
| 3422 |
// This is neccessary because WC()->customer->set_shipping_address_to_base() only sets country and state. |
| 3423 |
// IOK 2020-03-18 |
| 3424 |
public function get_static_shipping_address_data () { |
| 3425 |
// This is the format used by the Vipps callback, we are going to mimic this. |
| 3426 |
// IOK 2025-05-08 now also using the format used by Checkout in addition to Express. -- streetAddress, postalCode, region |
| 3427 |
$defaultdata = array('addressId'=>0, "addressLine1"=>"", "addressLine2"=>"", "streetAddress"=>"", "country"=>"NO", "city"=>"", "postalCode"=>"", "postCode"=>"", "addressType"=>"Home"); |
| 3428 |
// IOK 2025-08-14 previously this used the customers' address if logged in or available, but that I think was a mistake - since this is intended to be static, ensure we use the base address only. |
| 3429 |
$countries=new WC_Countries(); |
| 3430 |
$defaultdata['country'] = $countries->get_base_country(); |
| 3431 |
$defaultdata['city'] = $countries->get_base_city(); |
| 3432 |
$defaultdata['region'] = $countries->get_base_city(); |
| 3433 |
$defaultdata['postalCode'] = $countries->get_base_postcode(); |
| 3434 |
$defaultdata['postCode'] = $countries->get_base_postcode(); |
| 3435 |
$defaultdata['streetAddress'] = $countries->get_base_address(); |
| 3436 |
$defaultdata['addressLine1'] = $countries->get_base_address(); |
| 3437 |
return $defaultdata; |
| 3438 |
} |
| 3439 |
|
| 3440 |
// Getting shipping methods/costs for a given order to Vipps for express checkout |
| 3441 |
public function vipps_shipping_details_callback() { |
| 3442 |
Vipps::nocache(); |
| 3443 |
|
| 3444 |
$raw_post = @file_get_contents( 'php://input' ); |
| 3445 |
$result = @json_decode($raw_post,true); |
| 3446 |
|
| 3447 |
if (!$result) { |
| 3448 |
if (empty(trim($raw_post))) { |
| 3449 |
status_header(400, "Empty address info"); |
| 3450 |
print "No address"; |
| 3451 |
} else { |
| 3452 |
status_header(400, "Invalid JSON"); |
| 3453 |
print "Invalid JSON"; |
| 3454 |
} |
| 3455 |
$error = json_last_error_msg(); |
| 3456 |
$this->log(sprintf(__("Error getting customer data in the %1\$s shipping details callback: %2\$s",'woo-vipps'), $this->get_payment_method_name(), $error)); |
| 3457 |
$this->log(__("Raw input was ", 'woo-vipps')); |
| 3458 |
$this->log($raw_post); |
| 3459 |
exit(); |
| 3460 |
} |
| 3461 |
|
| 3462 |
// IOK 2025-08-15 Express Checkout (now) passes the reference/order-id in the data, but Checkout passes it in the URL, which we |
| 3463 |
// capture in a callback= parameter added at the end. Format is |
| 3464 |
// '/v3/checkout/woodigitalt4780/shippingDetails' |
| 3465 |
$vippsorderid = ""; |
| 3466 |
$callback = sanitize_text_field($_REQUEST['callback'] ?? ""); |
| 3467 |
do_action('woo_vipps_shipping_details_callback', $result,$raw_post,$callback); // This is for debugging. IOK 2025-08-15 |
| 3468 |
|
| 3469 |
if ($callback) { |
| 3470 |
$data = array_reverse(explode("/",$callback)); |
| 3471 |
$vippsorderid = !empty($data) ? ($data[1] ?? "") : ""; // Second element - callback is /v3/checkout/woodigitalt4780/shippingDetails |
| 3472 |
} elseif (isset($result['reference'])) { |
| 3473 |
$vippsorderid = $result['reference']; |
| 3474 |
} |
| 3475 |
|
| 3476 |
$orderid = intval($_REQUEST['id'] ?? 0); |
| 3477 |
if (!$orderid) { |
| 3478 |
status_header(404, "Unknown order"); |
| 3479 |
print "Unknown order"; |
| 3480 |
$this->log(sprintf(__('Could not find %1$s order with id:', 'woo-vipps'), $this->get_payment_method_name()) . " " . $vippsorderid . "\n" . __('Callback was:', 'woo-vipps') . " " . $callback, 'error'); |
| 3481 |
exit(); |
| 3482 |
} |
| 3483 |
|
| 3484 |
// This is for debugging sites where shipping handling fails because of blocks etc IOK 2026-01-15 |
| 3485 |
$this->log(sprintf(__("Received shipping callback for order %d", 'woo-vipps'), $orderid)); |
| 3486 |
|
| 3487 |
do_action('woo_vipps_shipping_details_callback_order', $orderid, $vippsorderid); |
| 3488 |
|
| 3489 |
$order = wc_get_order($orderid); |
| 3490 |
if (!$order) { |
| 3491 |
status_header(404, "Unknown order"); |
| 3492 |
print "Unknown order"; |
| 3493 |
$this->log(__('Could not find Woo order with id:', 'woo-vipps') . " " . $orderid, 'error'); |
| 3494 |
exit(); |
| 3495 |
} |
| 3496 |
if (!self::is_vipps_order($order)) { |
| 3497 |
status_header(400, "Invalid order"); |
| 3498 |
print "Invalid order"; |
| 3499 |
$this->log(__('Invalid order for shipping callback:', 'woo-vipps') . " " . $orderid, 'error'); |
| 3500 |
exit(); |
| 3501 |
} |
| 3502 |
// a small bit of security |
| 3503 |
if (!$order->get_meta('_vipps_authtoken') || (!wp_check_password($_REQUEST['tk'], $order->get_meta('_vipps_authtoken')))) { |
| 3504 |
status_header(403, "Wrong auth"); |
| 3505 |
print "Wrong auth"; |
| 3506 |
$this->log("Wrong authtoken on shipping details callback", 'error'); |
| 3507 |
exit(); |
| 3508 |
} |
| 3509 |
if ($vippsorderid != $order->get_meta('_vipps_orderid')) { |
| 3510 |
status_header(400, "Invalid order id"); |
| 3511 |
print "Invalid order id"; |
| 3512 |
$this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning'); |
| 3513 |
exit(); |
| 3514 |
} |
| 3515 |
|
| 3516 |
// If we are doing this for Checkout after version 3, communicate to any shipping methods with |
| 3517 |
// special support for Checkout that this is in fact happening. IOK 2023-01-19 |
| 3518 |
// This needs to be done before "calculate totals". |
| 3519 |
// Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06 |
| 3520 |
$ischeckout = $order->get_meta('_vipps_checkout'); |
| 3521 |
|
| 3522 |
$this->callback_restore_session($orderid); |
| 3523 |
|
| 3524 |
// If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06 |
| 3525 |
// here we will add support for PickupLocations. Also called for static shipping. |
| 3526 |
// IOK 2025-08-14 now also supported for Express Checkout |
| 3527 |
$this->load_extra_shipping_methods($order, $result, $ischeckout); |
| 3528 |
|
| 3529 |
$return = $this->vipps_shipping_details_callback_handler($order, $result,$vippsorderid, $ischeckout); |
| 3530 |
|
| 3531 |
// Express checkout wants the data wrapped in a object with a 'groups' attribute, Checkout wants thing unwrapped. |
| 3532 |
// Dispatch on the known type. IOK 2025-08-15 |
| 3533 |
if ($ischeckout) { |
| 3534 |
$return = $return['shippingDetails']; |
| 3535 |
} else { |
| 3536 |
// Note that this is of course different from both Checkout and static shipping. |
| 3537 |
$return = [ "groups" => $return ]; |
| 3538 |
} |
| 3539 |
|
| 3540 |
$json = json_encode($return); |
| 3541 |
|
| 3542 |
header("Content-type: application/json; charset=UTF-8"); |
| 3543 |
print $json; |
| 3544 |
// Just to be sure, save any changes made to the session by plugins/hooks IOK 2019-10-22 |
| 3545 |
if (is_a(WC()->session, 'WC_Session_Handler')) WC()->session->save_data(); |
| 3546 |
exit(); |
| 3547 |
} |
| 3548 |
|
| 3549 |
// This function calculates and returns one of two possible JSON representations to Vipps MobilePay, one for Express and one for Checkout. |
| 3550 |
// First, an intermediate representation is created, based on the original Express API. This is kept because users may still have filters |
| 3551 |
// that expects this representation. Later, these are transformed and augmented for the newer APIs. IOK 2025-08-14 |
| 3552 |
// Also used for Static Shipping for both representations. IOK 2025-08-14 |
| 3553 |
public function vipps_shipping_details_callback_handler($order, $vippsdata,$vippsorderid, $ischeckout) { |
| 3554 |
// This filter is used in sub-functions to keep track of what we are calculating for, without having to set globals or pass arguments. IOK 2025-08-14 |
| 3555 |
if ($ischeckout) add_filter('woo_vipps_is_vipps_checkout', '__return_true'); |
| 3556 |
|
| 3557 |
// We may have an address already in the Order, and no *new* address, when recalculating shipping options after modifying the order. |
| 3558 |
// We'll still create a $vippsdata struct so that old filters can do whatever is neccessary. IOK 2025-09-16 |
| 3559 |
$new_address = !empty($vippsdata); |
| 3560 |
if (!$new_address) { |
| 3561 |
$vippsdata['addressLine1'] = $order->get_shipping_address_1(); |
| 3562 |
$vippsdata['addressLine2'] = $order->get_shipping_address_2(); |
| 3563 |
$vippsdata['postCode'] = $order->get_shipping_postcode(); |
| 3564 |
$vippsdata['city'] = $order->get_shipping_city(); |
| 3565 |
$vippsdata['country'] = $order->get_shipping_country(); |
| 3566 |
} |
| 3567 |
|
| 3568 |
// Since we have legacy users that may have filters defined on these values, we will translate newer apis to the older ones. |
| 3569 |
// so filters will continue to work for newer apis/checkout |
| 3570 |
if (isset($vippsdata['streetAddress'])){ |
| 3571 |
$vippsdata['addressLine1'] = $vippsdata['streetAddress']; |
| 3572 |
$vippsdata['addressLine2'] = ""; |
| 3573 |
} |
| 3574 |
if (isset($vippsdata['region'])) { |
| 3575 |
$vippsdata['city'] = $vippsdata['region']; |
| 3576 |
} |
| 3577 |
if (isset($vippsdata['postalCode'])) { |
| 3578 |
$vippsdata['postCode'] = $vippsdata['postalCode']; |
| 3579 |
} |
| 3580 |
// Translations for different versions of the API end |
| 3581 |
|
| 3582 |
$addressid = isset($vippsdata['addressId']) ? $vippsdata['addressId'] : ""; |
| 3583 |
$addressline1 = $vippsdata['addressLine1']; |
| 3584 |
$addressline2 = $vippsdata['addressLine2']; |
| 3585 |
|
| 3586 |
// IOK 2019-08-26 apparently the apps contain a lot of addresses with duplicate lines |
| 3587 |
if ($addressline1 == $addressline2) $addressline2 = ''; |
| 3588 |
if (!$addressline2) $addressline2 = ''; |
| 3589 |
|
| 3590 |
$country = $vippsdata['country']; |
| 3591 |
$city = $vippsdata['city']; |
| 3592 |
$postcode= $vippsdata['postCode']; |
| 3593 |
|
| 3594 |
// Old code here treated "Sofienberggata 12" as a special Vipps pro-forma address; this is no longer necessary. |
| 3595 |
// If we have gotten a new address from Express or Checkout, update the order. IOK 2025-09-16. |
| 3596 |
if ($new_address) { |
| 3597 |
$order->set_billing_address_1($addressline1); |
| 3598 |
$order->set_billing_address_2($addressline2); |
| 3599 |
$order->set_billing_city($city); |
| 3600 |
$order->set_billing_postcode($postcode); |
| 3601 |
$order->set_billing_country($country); |
| 3602 |
$order->set_shipping_address_1($addressline1); |
| 3603 |
$order->set_shipping_address_2($addressline2); |
| 3604 |
$order->set_shipping_city($city); |
| 3605 |
$order->set_shipping_postcode($postcode); |
| 3606 |
$order->set_shipping_country($country); |
| 3607 |
$order->save(); |
| 3608 |
} |
| 3609 |
|
| 3610 |
// This is *essential* to get VAT calculated correctly. That calculation uses the customer, which uses the session.IOK 2019-10-25 |
| 3611 |
// We don't *save* this to the customer, because this may happen in a callback from Checkout where the customers' session is live and |
| 3612 |
// the address info is from Checkout (and not necessarily the customers real address). IOK 2025-09-12 |
| 3613 |
if (WC()->customer) { |
| 3614 |
WC()->customer->set_billing_location($country,'',$postcode,$city); |
| 3615 |
WC()->customer->set_shipping_location($country,'',$postcode,$city); |
| 3616 |
} else { |
| 3617 |
$this->log("No customer! when trying to calculate shipping"); |
| 3618 |
} |
| 3619 |
|
| 3620 |
// If you need to do something before the cart is manipulated, this is where it must be done. |
| 3621 |
// It is possible for a plugin to require a session when manipulating the cart, which could |
| 3622 |
// currently crash the system. This could be used to avoid that. IOK 2019-10-09 |
| 3623 |
do_action('woo_vipps_shipping_details_before_cart_creation', $order, $vippsorderid, $vippsdata); |
| 3624 |
|
| 3625 |
// calculate_totals() overwrites the session chosen_shipping_methods to default if it think it changed, |
| 3626 |
// which will be true if the pickup points are missing from previously. Pickup points only get loaded in woos checkout. |
| 3627 |
// So reset this to what it was before calling calculate_totals(). LP 2025-11-05 |
| 3628 |
// To be more specific if the *list of available methods* change, it will reset the chosen shipping method, |
| 3629 |
// even if the chosen shipping method is actually still available. We need to call calculate_totals on the cart, |
| 3630 |
// so we need to save + restore this. |
| 3631 |
$chosen = null; |
| 3632 |
$all_chosen = null; |
| 3633 |
if (is_a(WC()->session, 'WC_Session_Handler')) { |
| 3634 |
$all_chosen = WC()->session->get( 'chosen_shipping_methods' ); |
| 3635 |
if (!empty($all_chosen)) $chosen= $all_chosen[0]; |
| 3636 |
} |
| 3637 |
|
| 3638 |
// Previously, we would create a shoppingcart at this point, because we would not have access to the 'live' one, |
| 3639 |
// but it turns out this isn't actually possible. Any cart so created will become "the" cart for the Woo front end, |
| 3640 |
// and anyway, some plugins override the class of the cart, so just using WC_Cart will sometimes break. |
| 3641 |
// Now however, the session is stored in the order, and the cart will not have been deleted, so we should |
| 3642 |
// now be able to calculate shipping for the actual cart with no further manipulation. IOK 2020-04-08 |
| 3643 |
|
| 3644 |
// Turns out it is possible for the session - and the cart - to have been deleted at this point, for whatever reason. |
| 3645 |
// Login will do it, probably some other plugins as well. So if we have no cart at this point, we will ressurect the |
| 3646 |
// probable cart based on the order. This is only neccessary because Woo will not let us calculate shipping for an *order*. |
| 3647 |
// IOK 2024-04-09 |
| 3648 |
$cart_is_reconstructed = $this->maybe_reconstruct_cart($order->get_id()); |
| 3649 |
|
| 3650 |
WC()->cart->calculate_totals(); |
| 3651 |
|
| 3652 |
// See above. Restore chosen shipping methods if neccessary. IOK 2025-11-05 |
| 3653 |
if ($all_chosen) { |
| 3654 |
WC()->session->set('chosen_shipping_methods', $all_chosen); |
| 3655 |
} |
| 3656 |
|
| 3657 |
$acart = WC()->cart; |
| 3658 |
|
| 3659 |
$shipping_methods = array(); |
| 3660 |
$shipping_tax_rates = WC_Tax::get_shipping_tax_rates(); |
| 3661 |
|
| 3662 |
|
| 3663 |
// If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20 |
| 3664 |
if (!$acart->needs_shipping()) { |
| 3665 |
$no_shipping_taxes = WC_Tax::calc_shipping_tax('0', $shipping_tax_rates); |
| 3666 |
$shipping_methods['none_required:0'] = new WC_Shipping_Rate('none_required:0',__('No shipping required','woo-vipps'),0,$no_shipping_taxes, 'none_required', 0); |
| 3667 |
} else { |
| 3668 |
// Ensure the shipping packages we use has the current order address IOK 2025-09-12 |
| 3669 |
$destination = [ 'country' => $country, 'state' => '', 'postcode' => $postcode, 'city'=> $city, 'address' => $addressline1, 'address_1' => $addressline1, 'address_2' => $addressline2 ]; |
| 3670 |
add_filter('woocommerce_cart_shipping_packages', function ($packages) use($destination) { |
| 3671 |
$new = []; |
| 3672 |
foreach($packages as $package) { |
| 3673 |
$package['destination'] = $destination; |
| 3674 |
$new[] = $package; |
| 3675 |
} |
| 3676 |
return $new; |
| 3677 |
}); |
| 3678 |
|
| 3679 |
$packages = apply_filters('woo_vipps_shipping_callback_packages', WC()->cart->get_shipping_packages()); |
| 3680 |
$shipping = WC()->shipping->calculate_shipping($packages); |
| 3681 |
|
| 3682 |
$shipping_methods = WC()->shipping->packages[0]['rates']; // the 'rates' of the first package is what we want. |
| 3683 |
} |
| 3684 |
|
| 3685 |
// No exit here, because developers can add more methods using the filter below. IOK 2018-09-20 |
| 3686 |
if (empty($shipping_methods)) { |
| 3687 |
$name = $ischeckout ? Vipps::CheckoutName() : Vipps::ExpressCheckoutName(); |
| 3688 |
$this->log(sprintf(__('Could not find any applicable shipping methods for %1$s - order %2$d will fail', 'woo-vipps', 'warning'), $name, $order->get_id()), 'debug'); |
| 3689 |
$this->log(sprintf(__('Address given for %1$s was %2$s', 'woo-vipps'), $order->get_id(), |
| 3690 |
($addressline1 . " " . $addressline2 . " " . $city . " " . $postcode . " " . $country) |
| 3691 |
), 'debug'); |
| 3692 |
|
| 3693 |
} |
| 3694 |
|
| 3695 |
// Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts |
| 3696 |
// 'dynamic pricing' 2023-01-26 |
| 3697 |
// Which may be deprecated, but anyway, for future use IOK 2025-08-14 |
| 3698 |
$taxrate = 0; |
| 3699 |
if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) { |
| 3700 |
$taxrate = current($shipping_tax_rates)['rate']; |
| 3701 |
} |
| 3702 |
$order->update_meta_data('_vipps_shipping_tax_rates', $taxrate); |
| 3703 |
|
| 3704 |
// Merchant is using the old 'woo_vipps_shipping_methods' filter, and hasn't chosen to disable it. Use legacy methd. |
| 3705 |
// IOK 2025-08-14 I think we should add a deprecation notice to this now. It really should not be used anymore. FIXME |
| 3706 |
if (has_action('woo_vipps_shipping_methods') && $this->gateway()->get_option('newshippingcallback') != 'new') { |
| 3707 |
return $this->legacy_shipping_callback_handler($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart); |
| 3708 |
} |
| 3709 |
|
| 3710 |
// Earlier we sorted shipping methods based on price; currently we just use WooCommerce's order, but we |
| 3711 |
// provide this filter for people who would prefer the old logic. |
| 3712 |
$shipping_methods = apply_filters('woo_vipps_sort_shipping_methods', $shipping_methods, $order); |
| 3713 |
|
| 3714 |
// IOK 2020-02-13 Ok, new method! We are going to provide a list full of metadata for the users to process this time, which we will massage into the final Vipps method list |
| 3715 |
$methods = array(); |
| 3716 |
$i=-1; |
| 3717 |
|
| 3718 |
foreach ($shipping_methods as $key=>$rate) { |
| 3719 |
$i++; |
| 3720 |
$method = array(); |
| 3721 |
$method['priority'] = $i; |
| 3722 |
$method['default'] = false; |
| 3723 |
$method['rate'] = $rate; |
| 3724 |
$methods[$key]= $method; |
| 3725 |
} |
| 3726 |
$chosen = apply_filters('woo_vipps_default_shipping_method', $chosen, $shipping_methods, $order); |
| 3727 |
|
| 3728 |
if ($chosen && !isset($methods[$chosen])) { |
| 3729 |
$chosen = null; // Actually that isn't available |
| 3730 |
$this->log(sprintf(__("Unavailable shipping method set as default in the %1\$s Express Checkout shipping callback - check the 'woo_vipps_default_shipping_method' filter",'debug'), $this->get_payment_method_name())); |
| 3731 |
} |
| 3732 |
|
| 3733 |
if (!$chosen) { |
| 3734 |
// Find first method that isn't 'local_pickup' |
| 3735 |
// or pickup_location. IOK 2025-05-07 |
| 3736 |
foreach($methods as $key=>&$data) { |
| 3737 |
$mid = $data['rate']->get_method_id(); |
| 3738 |
if ($mid != 'local_pickup' && $mid != 'pickup_location') { |
| 3739 |
$chosen = $key; |
| 3740 |
break; |
| 3741 |
} |
| 3742 |
} |
| 3743 |
// Ok, just pick the first |
| 3744 |
if (!$chosen) { |
| 3745 |
foreach($methods as $key=>&$data) { |
| 3746 |
$chosen = $key; |
| 3747 |
break; |
| 3748 |
} |
| 3749 |
|
| 3750 |
} |
| 3751 |
} |
| 3752 |
if (isset($methods[$chosen])) { |
| 3753 |
$methods[$chosen]['default'] = true; |
| 3754 |
} |
| 3755 |
$methods = apply_filters('woo_vipps_express_checkout_shipping_rates', $methods, $order, $acart); |
| 3756 |
|
| 3757 |
// Just to be sure, if the current cart was reconstructed from an order, we will delete it now after |
| 3758 |
// last use of $acart |
| 3759 |
if ($cart_is_reconstructed) { |
| 3760 |
WC()->cart->empty_cart(); |
| 3761 |
} |
| 3762 |
|
| 3763 |
$vippsmethods = array(); |
| 3764 |
|
| 3765 |
// Just a utility from shippingMethodIds to the non-serialized rates, and from the same to the non-serialized |
| 3766 |
// shipping methods - the last stores settings, the first store metadata |
| 3767 |
// The ratemap will be used to store a table in the order from an arbitrary ID key to the calculated shipping rate IOK 2025-08-15 |
| 3768 |
$ratemap = array(); |
| 3769 |
$methodmap = array(); |
| 3770 |
|
| 3771 |
// We need access to the extended settings of the shipping methods. |
| 3772 |
// This is for the 'new' local pickup feature for Woo. IOK 2025-08-14 |
| 3773 |
$methods_classes = WC()->shipping->get_shipping_method_class_names(); |
| 3774 |
$methods_classes['pickup_location'] = 'Automattic\WooCommerce\Blocks\Shipping\PickupLocation'; // Loaded using the "load" hook, after the registered methods, so we need to add it specially. |
| 3775 |
|
| 3776 |
// Store a table of ratemap key => WC_Shipping_Rate id in the session, so we don't have to load and deserialize the rates from the ratemap, |
| 3777 |
// e.g used in the Checkout ajax poll shipping-change event. LP 2026-03-20 |
| 3778 |
$rate_id_map = []; |
| 3779 |
|
| 3780 |
$has_free_shipping = false; |
| 3781 |
foreach($methods as $method) { |
| 3782 |
$rate = $method['rate']; |
| 3783 |
$methodid = $rate->get_method_id(); |
| 3784 |
|
| 3785 |
// Extended settings are stored in these objects |
| 3786 |
$methodclass = $methods_classes[$methodid] ?? null; |
| 3787 |
$shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null; |
| 3788 |
|
| 3789 |
$tax = $rate->get_shipping_tax() ?: 0; |
| 3790 |
$cost = $rate->get_cost() ?: 0; |
| 3791 |
$label = $rate->get_label(); |
| 3792 |
|
| 3793 |
if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) { |
| 3794 |
$has_free_shipping = true; |
| 3795 |
} |
| 3796 |
|
| 3797 |
// We can't just use the method id, because the customer may have different addresses. Just to be sure, hash the entire method and use as a key. |
| 3798 |
// Actually, we probably *can* use the method id, because other addresses are irellevant. But still, add a random factor |
| 3799 |
$rand = md5($methodid . bin2hex(random_bytes(32))); // Random enough, 32 chars |
| 3800 |
// Ensure this never is over 100 chars. Use a dollar sign to indicate 'new method' IOK 2020-02-14 |
| 3801 |
// Reserve 8 chars to contain a : and an option index for Express Checkout IOK 2025-08-15 |
| 3802 |
// IOK 2025-08-14 "new" method is the current system; the legacy system has shipping method ids with different naming conventions. Again, to be deprecated. FIXME. |
| 3803 |
$key = '$' . substr($methodid,0,58) . '$' . $rand; |
| 3804 |
$vippsmethod = array(); |
| 3805 |
$vippsmethod['isDefault'] = @$method['default'] ? 'Y' :'N'; |
| 3806 |
$vippsmethod['priority'] = $method['priority']; |
| 3807 |
|
| 3808 |
$rate_id_map[$key] = $rate->get_id(); |
| 3809 |
|
| 3810 |
// It seems woo actually computes rounding of prices and taxes *separately* when computing |
| 3811 |
// shipping costs, but we can't really assume this (or that all plugins do this, and so on.) |
| 3812 |
// Therefore we compute shipping cost with rounding *both ways* and choose the more expensive one - |
| 3813 |
// this way we should reserve enough money to complete the order in all cases. IOK 2025-09-30 |
| 3814 |
$shippingcostA = sprintf("%.2F",wc_format_decimal($cost+$tax,'')); |
| 3815 |
$shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,'')); |
| 3816 |
$shippingcost = max($shippingcostA, $shippingcostB); |
| 3817 |
|
| 3818 |
$vippsmethod['shippingCost'] = $shippingcost; |
| 3819 |
$vippsmethod['shippingMethod'] = $rate->get_label(); |
| 3820 |
$vippsmethod['shippingMethodId'] = $key; |
| 3821 |
$vippsmethods[]=$vippsmethod; |
| 3822 |
|
| 3823 |
// Metadata and settings stored for later use for Checkout |
| 3824 |
// and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15 |
| 3825 |
// In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to |
| 3826 |
// finalize the order. IOK 2025-08-15 |
| 3827 |
$ratemap[$key]=$rate; |
| 3828 |
$methodmap[$key]=$shipping_method; |
| 3829 |
} |
| 3830 |
|
| 3831 |
if (is_a(WC()->session, 'WC_Session')) { |
| 3832 |
WC()->session->set('vipps_shipping_rate_id_map', $rate_id_map); |
| 3833 |
} else { |
| 3834 |
/* translators: order id */ |
| 3835 |
$this->log(sprintf(__('Could not store shipping rate id map in session for order %1$s, session was not ok', 'woo_vipps'), $order->get_id()), 'error'); |
| 3836 |
} |
| 3837 |
|
| 3838 |
|
| 3839 |
// This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14 |
| 3840 |
$return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods); |
| 3841 |
$return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging |
| 3842 |
|
| 3843 |
// IOK 2021-11-16 Checkout uses a slightly different syntax and format. |
| 3844 |
// IOK 2025-08-15 and new Express yet another slightly different format. |
| 3845 |
// IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them |
| 3846 |
if ($ischeckout) { |
| 3847 |
$return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order); |
| 3848 |
} else { // New express format. LP 2025-05-26 |
| 3849 |
$return = $this->express_format_shipping_methods($return, $ratemap, $methodmap, $order); |
| 3850 |
$return = $this->express_group_shipping_methods($return, $ratemap, $methodmap, $order); |
| 3851 |
$return = apply_filters('woo_vipps_express_json_shipping_methods', $return, $order); // wat |
| 3852 |
} |
| 3853 |
|
| 3854 |
// We need to store the WC_Shipping_Rate objects with all its meta data in the database until return from Vipps. IOK 2020-02-17 |
| 3855 |
$storedmethods = array(); |
| 3856 |
$errormethods = array(); |
| 3857 |
foreach($ratemap as $key => $rate) { |
| 3858 |
$serialized = ''; |
| 3859 |
try { |
| 3860 |
// We use serialize here instead of json_encode because we need the object back. |
| 3861 |
// we base64-encode the serialized object, because it is to be stored in a database in a text field.a IOK 2025-12-12 |
| 3862 |
$raw = @serialize($rate); |
| 3863 |
$serialized = $raw ? @base64_encode($raw) : null; |
| 3864 |
if (!$serialized) { |
| 3865 |
throw new Exception("Could not serialize rate $key"); |
| 3866 |
} |
| 3867 |
// Retrieve these precalculated rates on return from the store IOK 2020-02-14 |
| 3868 |
$storedmethods[$key] = $serialized; |
| 3869 |
} catch (Exception $e) { |
| 3870 |
$errormethods[] = $key; |
| 3871 |
$this->log(sprintf(__("Cannot use shipping method %2\$s in %1\$s Express checkout: the shipping method isn't serializable.", 'woo-vipps'), $this->get_payment_method_name(), $label), 'error'); |
| 3872 |
$this->log($rate, 'error'); |
| 3873 |
continue; |
| 3874 |
} |
| 3875 |
} |
| 3876 |
|
| 3877 |
// Remove any methods from the return that was not serializable |
| 3878 |
if (!empty($errormethods)) { |
| 3879 |
$fixedreturn = []; |
| 3880 |
if ($ischeckout) { |
| 3881 |
foreach($errormethods as $problem) { |
| 3882 |
foreach($return['shippingDetails'] as $method) { |
| 3883 |
$id = preg_replace('!:\d+$!', "", $method['id']); |
| 3884 |
if ($id != $problem) $fixedreturn[] = $method; |
| 3885 |
} |
| 3886 |
} |
| 3887 |
$return['shippingDetails'] = $fixedreturn; |
| 3888 |
} else { |
| 3889 |
foreach($errormethods as $problem) { |
| 3890 |
foreach($return as $method) { |
| 3891 |
$option = $method['options'][0]; |
| 3892 |
$id = preg_replace('!:\d+$!', "", $option['id']); |
| 3893 |
if ($id != $problem) $fixedreturn[] = $method; |
| 3894 |
} |
| 3895 |
} |
| 3896 |
$return = $fixedreturn; |
| 3897 |
} |
| 3898 |
} |
| 3899 |
|
| 3900 |
|
| 3901 |
// We'll also store whether or not this set of rates include free shipping in some way. IOK 2025-09-16 |
| 3902 |
$storedmethods['_meta_has_free_shipping'] = $has_free_shipping; |
| 3903 |
$storedmethods['_is_base64'] = true; |
| 3904 |
|
| 3905 |
$order->update_meta_data('_vipps_express_checkout_shipping_method_table', $storedmethods); |
| 3906 |
$order->save_meta_data(); |
| 3907 |
return $return; |
| 3908 |
} |
| 3909 |
|
| 3910 |
// Translate from the old to the new express format. LP 2025-05-26 |
| 3911 |
public function express_format_shipping_methods ($return, &$ratemap, $methodmap, $order) { |
| 3912 |
$translated = array(); |
| 3913 |
$currency = $order->get_currency(); |
| 3914 |
|
| 3915 |
// First, we'll translate the legacy format originally used by express to the new one (that may |
| 3916 |
// still be in use by filters etc), then add hooks to modify options and other new features. |
| 3917 |
// IOK 2025-11-19 |
| 3918 |
foreach ($return['shippingDetails'] as $m) { |
| 3919 |
$m2 = array(); |
| 3920 |
$options = []; |
| 3921 |
|
| 3922 |
$m2['isDefault'] = ($m['isDefault']=='Y') ? true : false; |
| 3923 |
$m2['priority'] = $m['priority']; |
| 3924 |
$m2['brand'] = 'OTHER'; // the default. This is replaced for certain brands. LP 2025-05-26 |
| 3925 |
$m2['type'] = 'OTHER'; // default, replaced for certain types. LP 2025-05-26 |
| 3926 |
|
| 3927 |
$id = $m['shippingMethodId']; |
| 3928 |
$rate = $ratemap[$id]; |
| 3929 |
$shipping_method = $methodmap[$id]; |
| 3930 |
|
| 3931 |
if ($rate->method_id == 'pickup_location') { |
| 3932 |
$m2['type'] = 'PICKUP_POINT'; |
| 3933 |
} |
| 3934 |
|
| 3935 |
// Each shipping method needs a list of options at this point. |
| 3936 |
$options = []; |
| 3937 |
|
| 3938 |
|
| 3939 |
// A rate can have a delivery time as a string in both Woo and Express |
| 3940 |
$delivery_time = ""; |
| 3941 |
if (version_compare(WC_VERSION, '9.2.0', '>=')) { |
| 3942 |
$delivery_time = $rate->get_delivery_time(); |
| 3943 |
} |
| 3944 |
|
| 3945 |
// And some rates have metadata, such as pickup locations (local_delivery). |
| 3946 |
$meta = $rate->get_meta_data(); |
| 3947 |
// We can also support descriptions, in the "meta" field |
| 3948 |
$description = $rate->get_description(); |
| 3949 |
|
| 3950 |
$option = []; |
| 3951 |
$option['priority'] = $m['priority']; |
| 3952 |
$option['name'] = $m['shippingMethod']; |
| 3953 |
$option['id'] = $id; |
| 3954 |
$option['amount'] = [ 'value' => round(100*$m['shippingCost']), 'currency' => $currency ]; |
| 3955 |
if ($delivery_time) $option['estimatedDelivery'] = $delivery_time; |
| 3956 |
if ($description) $entry['meta'] = $description; |
| 3957 |
$options[] = $option; |
| 3958 |
|
| 3959 |
if (isset($meta['brand'])) { |
| 3960 |
$m2['brand'] = $meta['brand']; |
| 3961 |
} else { |
| 3962 |
// specialcase some known methods so they get brands, and put the label into the description |
| 3963 |
if ($shipping_method && is_a($shipping_method, 'WC_Shipping_Method') && get_class($shipping_method) == 'WC_Shipping_Method_Bring_Pro') { |
| 3964 |
$m2['brand'] = "POSTEN"; |
| 3965 |
} |
| 3966 |
$m2['brand'] = apply_filters('woo_vipps_shipping_method_brand', $m2['brand'],$shipping_method, $rate); |
| 3967 |
} |
| 3968 |
|
| 3969 |
if ($m2['brand'] != "OTHER" && isset($meta['type'])) { |
| 3970 |
$m2['type'] = apply_filters('woo_vipps_shipping_method_type', $meta['type'], $shipping_method, $rate); |
| 3971 |
} |
| 3972 |
$m2['options'] = $options; |
| 3973 |
|
| 3974 |
// Now allow custom code to modify both the rate (adding metadata, mostly) and the Vipps shipping method (probably adding |
| 3975 |
// options, changing the brand etc) IOK 2025-11-19 |
| 3976 |
// For an example, see the express_add_pickup_location_options method. IOK 2025-11-19 |
| 3977 |
list ($rate, $m2) = apply_filters('woo_vipps_modify_express_checkout_rate', [$rate, $m2], $shipping_method, $rate, $order); |
| 3978 |
$ratemap[$id] = $rate; // Modify the ratemaps copy with any new data here - ratemap is passed by reference IOK 2025-11-19 |
| 3979 |
|
| 3980 |
$translated[] = $m2; |
| 3981 |
} |
| 3982 |
|
| 3983 |
return $translated; |
| 3984 |
} |
| 3985 |
|
| 3986 |
// This adds extra options for express checkout shipping rates that implement the 'woo_vipps_shipping_method_pickup_points' filter, |
| 3987 |
// making these into groups with a dropdown for the exact shipping location as separate options. |
| 3988 |
// This will create multiple pointers to the same shipping rate, which will be extended with a metadata field containing the pickup point. |
| 3989 |
// That is, this is *not* for local_pickup, but for legacy local pickup and other shipping methods that have the same rate price, but |
| 3990 |
// allows the user to select a location. IOK 2025-08-15 |
| 3991 |
public function express_add_pickup_location_options ( $data, $shipping_method, $rate, $order) { |
| 3992 |
list ($rate, $m2) = $data; |
| 3993 |
$pickup_points = apply_filters('woo_vipps_shipping_method_pickup_points', [], $rate, $shipping_method, $order); |
| 3994 |
if (empty($pickup_points)) return $data; |
| 3995 |
if (count($m2['options'])>1) return $data; |
| 3996 |
|
| 3997 |
$index = 0; |
| 3998 |
$pickup_point_table = []; |
| 3999 |
$option = $m2['options'][0]; |
| 4000 |
$id = $option['id']; |
| 4001 |
|
| 4002 |
foreach($pickup_points as $point) { |
| 4003 |
$index++; // Start at 1 |
| 4004 |
$entry = $option; // This is a copy in PHP |
| 4005 |
|
| 4006 |
$addr = []; |
| 4007 |
foreach(['name', 'address', 'postalCode', 'city', 'country'] as $key) { |
| 4008 |
$v = trim($point[$key]); |
| 4009 |
if (!empty($v)) $addr[$key] = $v; |
| 4010 |
} |
| 4011 |
// To avoid confusion, force the keys to be strings. IOK 2025-08-15 |
| 4012 |
$pickup_point_table["i".$index] = $addr; |
| 4013 |
|
| 4014 |
// This is for display in the App only IOK 2025-08-15 |
| 4015 |
$description = join(", ", array_values($addr)); |
| 4016 |
$description = trim(apply_filters('woo_vipps_shipping_option_meta', trim($description, " ,"), $rate, $shipping_method, $order)); |
| 4017 |
if ($description) $entry['meta'] = $description; |
| 4018 |
|
| 4019 |
// IOK 2025-06-04 Since we are here mapping several Express rates to a single Woo rate, |
| 4020 |
// we need to add a suffix, which is removed in gw->set_order_shipping_details(). |
| 4021 |
$entry['id'] = $id . ":" . $index; |
| 4022 |
$entry['name'] = $point['name']; |
| 4023 |
$options[] = $entry; |
| 4024 |
} |
| 4025 |
// If we have pickup points added, then store them in a table in the rate itself. We'll strip that value when finalizing the order. IOK 2025-08-15 |
| 4026 |
// This gets stored in the orders ratemap on return. IOK 2025-11-19 |
| 4027 |
if (!empty($pickup_point_table)) { |
| 4028 |
$rate->add_meta_data('_vipps_pickupPoints', $pickup_point_table); |
| 4029 |
} |
| 4030 |
$m2['options'] = $options; |
| 4031 |
$m2['type'] = 'PICKUP_POINT'; |
| 4032 |
|
| 4033 |
return [$rate, $m2]; |
| 4034 |
} |
| 4035 |
|
| 4036 |
|
| 4037 |
// Group certain shipping methods together in the new express format, into a group of options for one method (for example pickup locations). LP 2025-06-04 |
| 4038 |
// $order not used, will keep for now so to have a similar signature to express_format_shipping_methods, also it might be used in future change of this method. LP 2025-08-18 |
| 4039 |
public function express_group_shipping_methods($methods, &$ratemap, $methodmap, $order) { |
| 4040 |
if (!$methods) return $methods; |
| 4041 |
$grouped = []; |
| 4042 |
$maybe_groupable_methods = $methods; |
| 4043 |
while (!empty($maybe_groupable_methods)) { |
| 4044 |
$first = array_shift($maybe_groupable_methods); |
| 4045 |
$first_id = preg_replace("!:.+$!", "", $first['options'][0]['id']); // strip option index from 'augmented' methods. |
| 4046 |
$first_rate = $ratemap[$first_id]; |
| 4047 |
$first_method = $methodmap[$first_id]; |
| 4048 |
|
| 4049 |
$rest = []; |
| 4050 |
foreach ($maybe_groupable_methods as $candidate) { |
| 4051 |
$candidate_id = preg_replace("!:.+$!", "", $candidate['options'][0]['id']); // strip option index from 'augmented' methods. |
| 4052 |
$candidate_rate = $ratemap[$candidate_id]; |
| 4053 |
$candidate_method = $methodmap[$candidate_id]; |
| 4054 |
|
| 4055 |
// By default, we will group all rates that are pickup_location-s. LP 2025-08-18 |
| 4056 |
$is_pickup = $first_rate->method_id === $candidate_rate->method_id && $first_rate->method_id === 'pickup_location'; |
| 4057 |
$should_group = apply_filters('woo_vipps_express_should_group_shipping_methods', $is_pickup, $first_rate, $first_method, $candidate_rate, $candidate_method); |
| 4058 |
|
| 4059 |
if ($should_group) { |
| 4060 |
$first_options = $first['options']; |
| 4061 |
$second_options = $candidate['options']; |
| 4062 |
|
| 4063 |
$first['options'] = array_merge($first_options, $second_options); |
| 4064 |
|
| 4065 |
// Reset default-ness and priority to the highest value from the merged methods. |
| 4066 |
if ($candidate['isDefault']) $first['isDefault'] = true; |
| 4067 |
if ($candidate['priority'] < $first['priority']) $first['priority'] = $candidate['priority']; |
| 4068 |
|
| 4069 |
} else { |
| 4070 |
$rest[] = $candidate; |
| 4071 |
} |
| 4072 |
|
| 4073 |
} |
| 4074 |
$grouped[] = $first; |
| 4075 |
|
| 4076 |
// Start over again with the ones who weren't grouped to the first method of the list. LP 2025-08-18 |
| 4077 |
$maybe_groupable_methods = $rest; |
| 4078 |
} |
| 4079 |
|
| 4080 |
return $grouped; |
| 4081 |
} |
| 4082 |
|
| 4083 |
|
| 4084 |
// In certain situations the session may have no cart, which among other things makes it impossible for us to calculate shipping. |
| 4085 |
// We must therefore reconstruct the cart as close to what it were before calculating shipping; and we must delete it afterwards |
| 4086 |
// because it may not be correct wrt meta values and so forth. Based on cart-sessions "populate_cart_from_order" used in the "order again" path. |
| 4087 |
// Returns "true" if cart is reconstructed from the order, else false. |
| 4088 |
// IOK 2024-04-08 |
| 4089 |
private function maybe_reconstruct_cart($order_id) { |
| 4090 |
if (!WC()->cart->is_empty()) return false; |
| 4091 |
$this->log(sprintf(__("No cart, so will try to calculate shipping based on order contents for order %1\$d", 'woo-vipps'), $order_id), 'error'); |
| 4092 |
try { |
| 4093 |
$order = wc_get_order( $order_id ); |
| 4094 |
$cart = array(); |
| 4095 |
$inital_cart_size = 0; |
| 4096 |
$order_items = $order->get_items(); |
| 4097 |
foreach ( $order_items as $item ) { |
| 4098 |
$product_id = (int) $item->get_product_id(); |
| 4099 |
$quantity = $item->get_quantity(); |
| 4100 |
$variation_id = (int) $item->get_variation_id(); |
| 4101 |
$variations = array(); |
| 4102 |
$cart_item_data = array(); |
| 4103 |
$product = $item->get_product(); |
| 4104 |
if ( ! $product ) { |
| 4105 |
continue; |
| 4106 |
} |
| 4107 |
if ( ! $variation_id && $product->is_type( 'variable' ) ) continue; |
| 4108 |
// We ignore the out-of-stock rule here, it doesn't matter for shipping in this case IOK 2024-04-09 |
| 4109 |
foreach ( $item->get_meta_data() as $meta ) { |
| 4110 |
if ( taxonomy_is_product_attribute( $meta->key ) || meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) { |
| 4111 |
$variations[ $meta->key ] = $meta->value; |
| 4112 |
} |
| 4113 |
} |
| 4114 |
$cart_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data ); |
| 4115 |
$product_data = wc_get_product( $variation_id ? $variation_id : $product_id ); |
| 4116 |
$cart[ $cart_id ] = array_merge( |
| 4117 |
$cart_item_data, |
| 4118 |
array( |
| 4119 |
'key' => $cart_id, |
| 4120 |
'product_id' => $product_id, |
| 4121 |
'variation_id' => $variation_id, |
| 4122 |
'variation' => $variations, |
| 4123 |
'quantity' => $quantity, |
| 4124 |
'data' => $product_data, |
| 4125 |
'data_hash' => wc_get_cart_item_data_hash( $product_data ), |
| 4126 |
) |
| 4127 |
); |
| 4128 |
|
| 4129 |
} |
| 4130 |
WC()->cart->set_cart_contents($cart); |
| 4131 |
WC()->cart->calculate_totals(); |
| 4132 |
WC()->cart->set_session(); |
| 4133 |
return true; |
| 4134 |
} catch (Exception $e) { |
| 4135 |
$this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error'); |
| 4136 |
return false; |
| 4137 |
} |
| 4138 |
} |
| 4139 |
|
| 4140 |
|
| 4141 |
// IOK 2020-02-13 This method implements the *old* style of providing shipping methods to Vipps Express Checkout. |
| 4142 |
// It is 'stateless' in that it doesn't need to serialize shipping methods or anything like that - but precisely because of this, |
| 4143 |
// metadata isn't possible to provide, and it reqires to send VAT separately coded into the shipping method ID which is pretty |
| 4144 |
// clumsy. This method will currently only be used if a merchant has overridden the 'woo_vipps_shipping_methods' filter and hasn't chosen |
| 4145 |
// the setting that overrides this. |
| 4146 |
public function legacy_shipping_callback_handler ($shipping_methods, $chosen, $addressid, $vippsorderid, $order, $acart) { |
| 4147 |
do_action('woo_vipps_legacy_shipping_methods', $order); // This will probably be mostly for debugging. |
| 4148 |
|
| 4149 |
// If no shipping is required (for virtual products, say) ensure we send *something* back IOK 2018-09-20 |
| 4150 |
if (!$acart->needs_shipping()) { |
| 4151 |
$methods = array(array('isDefault'=>'Y','priority'=>'0','shippingCost'=>'0.00','shippingMethod'=>__('No shipping required','woo-vipps'),'shippingMethodId'=>'Free:Free;0')); |
| 4152 |
$return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods); |
| 4153 |
return $return; |
| 4154 |
} |
| 4155 |
|
| 4156 |
$free = 0; |
| 4157 |
$defaultset = 0; |
| 4158 |
$methods = array(); |
| 4159 |
foreach ($shipping_methods as $rate) { |
| 4160 |
$method = array(); |
| 4161 |
$method['priority'] = 0; |
| 4162 |
$tax = $rate->get_shipping_tax() ?: 0; |
| 4163 |
$cost = $rate->get_cost() ?: 0; |
| 4164 |
|
| 4165 |
$method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,'')); |
| 4166 |
$method['shippingMethod'] = $rate->get_label(); |
| 4167 |
// We may not really need the tax stashed here, but just to be sure. |
| 4168 |
$method['shippingMethodId'] = $rate->get_id() . ";" . $tax; |
| 4169 |
$methods[]= $method; |
| 4170 |
|
| 4171 |
// If we qualify for free shipping, make it the default. Thanks to Emely Bakke for reporting. IOK 2019-11-15 |
| 4172 |
if (preg_match("!^free_shipping!",$rate->get_id())) { |
| 4173 |
$free=1; |
| 4174 |
$defaultset=1; |
| 4175 |
$chosen = $rate->get_id(); |
| 4176 |
} |
| 4177 |
} |
| 4178 |
usort($methods, function($method1, $method2) { |
| 4179 |
return $method1['shippingCost'] - $method2['shippingCost']; |
| 4180 |
}); |
| 4181 |
$priority=0; |
| 4182 |
foreach($methods as &$method) { |
| 4183 |
$rateid = explode(";",$method['shippingMethodId'],2); |
| 4184 |
if (!empty($rateid) && $rateid[0] == $chosen) { |
| 4185 |
$defaultset=1; |
| 4186 |
$method['isDefault'] = 'Y'; |
| 4187 |
} else { |
| 4188 |
$method['isDefault'] = 'N'; |
| 4189 |
} |
| 4190 |
$method['priority']=$priority; |
| 4191 |
$priority++; |
| 4192 |
} |
| 4193 |
// If we don't have free shipping, select the first (cheapest) option, unless that is 'local pickup'. IOK 2019-11-26 |
| 4194 |
// Or pickup_location, same thing. IOK 2025-05-07 |
| 4195 |
if(!$defaultset && !empty($methods)) { |
| 4196 |
foreach($methods as &$method) { |
| 4197 |
if (!preg_match("!^(local_pickup|pickup_location)!",$method['shippingMethodId'])) { |
| 4198 |
$defaultset=1; |
| 4199 |
$method['isDefault'] = 'Y'; |
| 4200 |
break; |
| 4201 |
} |
| 4202 |
} |
| 4203 |
} |
| 4204 |
// Or the first if we stil have no default method. |
| 4205 |
if (!$defaultset &&!empty($methods)) { |
| 4206 |
$methods[0]['isDefault'] = 'Y'; |
| 4207 |
} |
| 4208 |
|
| 4209 |
$return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$methods); |
| 4210 |
$return = apply_filters('woo_vipps_shipping_methods', $return,$order,$acart); |
| 4211 |
|
| 4212 |
return $return; |
| 4213 |
} |
| 4214 |
|
| 4215 |
public static function nocache() { |
| 4216 |
wc_nocache_headers(); |
| 4217 |
header("X-Accel-Expires: 0"); |
| 4218 |
} |
| 4219 |
|
| 4220 |
|
| 4221 |
public function woocommerce_payment_gateways($methods) { |
| 4222 |
require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php"); |
| 4223 |
require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php"); |
| 4224 |
// Protect the singleton: Use the object instead of the class name IOK 2025-02-04 |
| 4225 |
$gateway = $this->gateway(); |
| 4226 |
if ($gateway) { |
| 4227 |
$methods[] = $gateway; |
| 4228 |
} else { |
| 4229 |
$methods[] = 'WC_Gateway_Vipps'; |
| 4230 |
} |
| 4231 |
|
| 4232 |
$methods[] = 'WC_Gateway_VippsCard'; |
| 4233 |
|
| 4234 |
return $methods; |
| 4235 |
} |
| 4236 |
|
| 4237 |
// Runs after set_session, so if the session is just created, we'll get called. IOK 2018-06-06 |
| 4238 |
public function woocommerce_cart_updated() { |
| 4239 |
$this->maybe_set_vipps_as_default(); |
| 4240 |
} |
| 4241 |
|
| 4242 |
public function woocommerce_add_to_cart_redirect ($url) { |
| 4243 |
if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) { |
| 4244 |
return $url; |
| 4245 |
} |
| 4246 |
$url = $this->express_checkout_url(); |
| 4247 |
$url = wp_nonce_url($url,'express','sec'); |
| 4248 |
|
| 4249 |
return $url; |
| 4250 |
} |
| 4251 |
|
| 4252 |
// We can't allow a customer to re-call the Vipps Express checkout payment thing twice - |
| 4253 |
// This would happen if a logged-in user tries to re-start the transaction after breaking it. |
| 4254 |
// But for express checkout this breaks because there is no shipping method or address, and of course, |
| 4255 |
// the order id is unique too.. IOK 2018-11-21 |
| 4256 |
public function woocommerce_my_account_my_orders_actions($actions, $order ) { |
| 4257 |
$pm = $order->get_payment_method(); |
| 4258 |
if (!self::is_vipps_order($pm)) return $actions; |
| 4259 |
|
| 4260 |
if (!static::order_is_vipps_retryable($order->get_id())) { |
| 4261 |
unset($actions['pay']); |
| 4262 |
} |
| 4263 |
return $actions; |
| 4264 |
} |
| 4265 |
|
| 4266 |
// This job runs in the wp-cron context, and is intended to clean up signal files and other temporariy data. IOK 2020-04-01 |
| 4267 |
public function cron_cleanup_hook () { |
| 4268 |
$this->cleanupCallbackSignals(); // Remove old callback signals (files in uploads) |
| 4269 |
$this->delete_old_cancelled_orders(); // Remove cancelled express checkout orders if selected |
| 4270 |
} |
| 4271 |
|
| 4272 |
// This job runs in the wp-cron context and checks if there are *old* pending orders with payment method Vipps. If so, it will |
| 4273 |
// check if the status of these orders are now known. This is intended to handle the case where a user does not return |
| 4274 |
// to the store and the Vipps callback fails for whatever reason. IOK 2021-06-21 |
| 4275 |
public function cron_check_for_missing_callbacks() { |
| 4276 |
$eightminutesago = time() - (60*8); |
| 4277 |
$sevendaysago = time() - (60*60*24*7); |
| 4278 |
|
| 4279 |
// This is compatible with both HPOS and old style order management. IOK 2026-05-27 |
| 4280 |
$pending_app = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago )); |
| 4281 |
$pending_cards = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps_card', 'date_created' => '>' . $sevendaysago )); |
| 4282 |
$pending = array_merge($pending_app, $pending_cards); |
| 4283 |
|
| 4284 |
if (empty($pending)) return; |
| 4285 |
foreach ($pending as $o) { |
| 4286 |
$then = $o->get_meta('_vipps_init_timestamp'); |
| 4287 |
if (! $then) continue; # Race condition! We may not have set the timestamp yet. IOK 2022-03-24 |
| 4288 |
if (!$o->get_meta('_vipps_orderid')) continue; # ditto |
| 4289 |
if ($then > $eightminutesago) continue; |
| 4290 |
|
| 4291 |
$vippstatus = $o->get_meta('_vipps_status'); |
| 4292 |
$currentstatus = $this->gateway()->interpret_vipps_order_status($vippstatus); |
| 4293 |
if ($currentstatus != 'initiated') { |
| 4294 |
$this->log(sprintf(__("Order %2\$d is 'pending' but its %1\$s order status is '%3\$s' - this means that the order has been erroneously set to 'pending' after completion or cancellation. Will not process further, please check status of order at %1\$s and set to correct status in WooCommerce", 'woo-vipps'), $this->get_payment_method_name(), $o->get_id(), $currentstatus), 'debug'); |
| 4295 |
return; |
| 4296 |
} |
| 4297 |
$this->check_status_of_pending_order($o, false); |
| 4298 |
} |
| 4299 |
} |
| 4300 |
|
| 4301 |
// Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session - |
| 4302 |
// e.g. wp-cron. IOK 2021-06-21 |
| 4303 |
// Stop restoring session in wp-cron too. IOK 2021-08-23 |
| 4304 |
// Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13 |
| 4305 |
public function check_status_of_pending_order($order, $allow_retry=true) { |
| 4306 |
$gw = $this->gateway(); |
| 4307 |
|
| 4308 |
$order_status = null; |
| 4309 |
try { |
| 4310 |
$order->add_order_note(sprintf(__("Callback from %1\$s delayed or never happened; order status checked by periodic job", 'woo-vipps'), $this->get_payment_method_name())); |
| 4311 |
|
| 4312 |
// Poll status and correct woo status. LP 2026-05-19 |
| 4313 |
$order_data = $gw->get_payment_details($order); |
| 4314 |
|
| 4315 |
// If we already know the order failed, we don't need to process the order further below. LP 2026-05-19 |
| 4316 |
if ('CANCEL' === ($order_data['state'] ?? "")) { |
| 4317 |
/* translators: company name */ |
| 4318 |
$order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName())); |
| 4319 |
return; |
| 4320 |
} |
| 4321 |
|
| 4322 |
$gw->set_order_status_by_payment_details($order, $order_data, $allow_retry); |
| 4323 |
$order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13 |
| 4324 |
$order_status = $order->get_status(); |
| 4325 |
|
| 4326 |
$this->log(sprintf(__("For order %2\$d order status at %1\$s is %3\$s", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id(), $order_status), 'debug'); |
| 4327 |
} catch (Exception $e) { |
| 4328 |
$this->log(sprintf(__("Error getting order status at %1\$s for order %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id()), 'error'); |
| 4329 |
$this->log($e->getMessage() . "\n" . $order->get_id(), 'error'); |
| 4330 |
} |
| 4331 |
return $order_status; |
| 4332 |
} |
| 4333 |
|
| 4334 |
// This will probably be run in activate, but if the plugin is updated in other ways, will also be run on after_setup_theme. IOK 2020-04-01 |
| 4335 |
public static function maybe_add_cron_event() { |
| 4336 |
if (!wp_next_scheduled('vipps_cron_cleanup_hook')) { |
| 4337 |
wp_schedule_event(time(), 'hourly', 'vipps_cron_cleanup_hook'); |
| 4338 |
} |
| 4339 |
if (!wp_next_scheduled('vipps_cron_missing_callback_hook')) { |
| 4340 |
wp_schedule_event(time(), '5min', 'vipps_cron_missing_callback_hook'); |
| 4341 |
} |
| 4342 |
} |
| 4343 |
|
| 4344 |
public function activate () { |
| 4345 |
static::maybe_add_cron_event(); |
| 4346 |
$gw = $this->gateway(); |
| 4347 |
|
| 4348 |
// If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19 |
| 4349 |
if ($gw->get_option('orderprefix') == 'Woo') { |
| 4350 |
$gw->update_option('orderprefix', $this->generate_order_prefix()); |
| 4351 |
} |
| 4352 |
// IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point. |
| 4353 |
$gw->initialize_webhooks(); |
| 4354 |
$this->payment_method_name = $gw->get_option('payment_method_name'); |
| 4355 |
|
| 4356 |
|
| 4357 |
// Check if the special page is noted and actually does exist |
| 4358 |
$special = static::get_special_page_id(); |
| 4359 |
if ($special) { |
| 4360 |
$special_page = get_post($special); |
| 4361 |
if ($special_page && 'trash' !== $special_page->post_status) { |
| 4362 |
// Ensure this page has the necessary shortcode. LP 2026-09-01 |
| 4363 |
if (!has_shortcode($special_page->post_content, 'vipps_special_page')) { |
| 4364 |
$new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->"; |
| 4365 |
wp_update_post([ |
| 4366 |
'ID' => $special, |
| 4367 |
'post_content' => $new_content, |
| 4368 |
]); |
| 4369 |
} |
| 4370 |
} else { |
| 4371 |
delete_option('woocommerce_vipps_special_page_page_id'); |
| 4372 |
} |
| 4373 |
} |
| 4374 |
|
| 4375 |
} |
| 4376 |
|
| 4377 |
// We have added some hooks to wp-cron; remove these. IOK 2020-04-01 |
| 4378 |
public static function deactivate() { |
| 4379 |
$timestamp = wp_next_scheduled('vipps_cron_cleanup_hook'); |
| 4380 |
wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook'); |
| 4381 |
$timestamp = wp_next_scheduled('vipps_cron_missing_callback_hook'); |
| 4382 |
wp_unschedule_event($timestamp, 'vipps_cron_missing_callback_hook'); |
| 4383 |
// IOK 2023-12-20 Delete all webhooks for this instance |
| 4384 |
$gw = WC_Gateway_Vipps::instance(); |
| 4385 |
$gw->delete_all_webhooks(); |
| 4386 |
|
| 4387 |
// Delete all settings if checked in settings menu. LP 2025-10-06 |
| 4388 |
$should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes'; |
| 4389 |
if ($should_delete) { |
| 4390 |
// Delete options. |
| 4391 |
$options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', 'vipps_button_options2', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated']; |
| 4392 |
foreach($options as $option) { |
| 4393 |
delete_option($option); |
| 4394 |
} |
| 4395 |
} |
| 4396 |
|
| 4397 |
// Run deactivation logic for recurring |
| 4398 |
if (class_exists('WC_Vipps_Recurring')) { |
| 4399 |
WC_Vipps_Recurring::get_instance()->deactivate(); |
| 4400 |
} |
| 4401 |
delete_option('woo_vipps_recurring_payments_activation'); |
| 4402 |
|
| 4403 |
} |
| 4404 |
|
| 4405 |
/** Try manually setting locale to locale recieved in AcceptLanguage header. |
| 4406 |
* |
| 4407 |
* This should fix incorrect language recieved from ajax when using translate plugins like polylang, wpml. |
| 4408 |
* E.g. for checkout widgets and product names: We send the correct locale to the frontend when first setting up Checkout, |
| 4409 |
* then we send the locale back in the Accept-Language header to ajax endpoints. LP 2025-12-11 |
| 4410 |
*/ |
| 4411 |
public static function set_locale_if_in_header() { |
| 4412 |
$locales = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; |
| 4413 |
|
| 4414 |
// get first in list, but strip away semicolon and everything after. LP 2025-12-16 |
| 4415 |
$newlocale = trim(preg_replace("!;.*!", "", explode(",", $locales)[0])); |
| 4416 |
if (empty($newlocale)) |
| 4417 |
return false; |
| 4418 |
return switch_to_locale($newlocale); // note: this may fail and return a false. LP 2025-12-11 |
| 4419 |
} |
| 4420 |
|
| 4421 |
|
| 4422 |
public function footer() { |
| 4423 |
// Nothing yet |
| 4424 |
} |
| 4425 |
|
| 4426 |
|
| 4427 |
// If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06 |
| 4428 |
private function maybe_set_vipps_as_default() { |
| 4429 |
if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done. |
| 4430 |
$gw = $this->gateway(); |
| 4431 |
// Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11 |
| 4432 |
if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) { |
| 4433 |
WC()->session->set('chosen_payment_method', $gw->id); |
| 4434 |
} |
| 4435 |
} |
| 4436 |
|
| 4437 |
// Check order status in the database, and if it is pending for a long time, directly at Vipps |
| 4438 |
// IOK 2018-05-04 |
| 4439 |
public function check_order_status($order) { |
| 4440 |
if (!$order) return null; |
| 4441 |
clean_post_cache($order->get_id()); // Get a fresh copy |
| 4442 |
$order = wc_get_order($order->get_id()); |
| 4443 |
$order_status = $order->get_status(); |
| 4444 |
|
| 4445 |
if ($order_status != 'pending') return $order_status; |
| 4446 |
// No callback has occured yet. If this has been going on for a while, check directly with Vipps |
| 4447 |
// We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13 |
| 4448 |
if ($order_status == 'pending') { |
| 4449 |
if (WC()->session) { |
| 4450 |
$now = time(); |
| 4451 |
$then = WC()->session->get('_vipps_check_' . $order->get_id()); |
| 4452 |
if (!$then) { |
| 4453 |
$then = $now; |
| 4454 |
WC()->session->set('_vipps_check_' . $order->get_id(), $then); |
| 4455 |
} |
| 4456 |
if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps |
| 4457 |
return $order_status; |
| 4458 |
} |
| 4459 |
} else { |
| 4460 |
// No session shouldn't be possible, but if it is.. |
| 4461 |
return $order_status; |
| 4462 |
} |
| 4463 |
} |
| 4464 |
$this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info'); |
| 4465 |
return $this->check_status_of_pending_order($order); |
| 4466 |
} |
| 4467 |
|
| 4468 |
// In some situations we have to empty the cart when the user goes to Vipps, so |
| 4469 |
// we store it in the session and restore it if the users cancels. IOK 2018-05-07 |
| 4470 |
// Try to avoid this now 2018-12-10 - only do it for single-product checkouts. IOK 2018-10-12 |
| 4471 |
// Changed to use a serialized cart, which should be more compatible with subclassed carts and cart metadata. |
| 4472 |
// Serialization errors are not yet handled - they can't be fixed but they could be signalled. IOK 2020-04-07 |
| 4473 |
public function save_cart($order,$cart_to_save) { |
| 4474 |
$carts = WC()->session->get('_vipps_carts'); |
| 4475 |
if (!$carts) $carts = array(); |
| 4476 |
$serialized = base64_encode(@serialize($cart_to_save->get_cart_contents())); |
| 4477 |
$carts[$order->get_id()] = $serialized; |
| 4478 |
WC()->session->set('_vipps_carts',$carts); |
| 4479 |
do_action('woo_vipps_cart_saved'); |
| 4480 |
} |
| 4481 |
public function restore_cart($order) { |
| 4482 |
global $woocommerce; |
| 4483 |
$carts = $woocommerce->session->get('_vipps_carts'); |
| 4484 |
if (empty($carts)) return; |
| 4485 |
$cart = null; |
| 4486 |
$cartdata = @$carts[$order->get_id()]; |
| 4487 |
if ($cartdata) { |
| 4488 |
$cart = @unserialize(@base64_decode($cartdata)); |
| 4489 |
} |
| 4490 |
do_action('woo_vipps_restoring_cart',$order,$cart); |
| 4491 |
unset($carts[$order->get_id()]); |
| 4492 |
$woocommerce->session->set('_vipps_carts',$carts); |
| 4493 |
// It will absolutely not work to just use set_cart_contents, because this will not |
| 4494 |
// correctly initialize this 'new' cart. So we *have* to use add_to_cart at least once. IOK 2020-04-07 |
| 4495 |
if (!empty($cart)) { |
| 4496 |
foreach ($cart as $cart_item_key => $values) { |
| 4497 |
$id =$values['product_id']; |
| 4498 |
$quant=$values['quantity']; |
| 4499 |
$varid = @$values['variation_id']; |
| 4500 |
$variation = @$values['variation']; |
| 4501 |
// .. and there may be any number of other attributes, which we need to pass on. |
| 4502 |
$cart_item_data = array(); |
| 4503 |
foreach($values as $key=>$value) { |
| 4504 |
if (in_array($key,array('product_id','quantity','variation_id','variation'))) continue; |
| 4505 |
$cart_item_data[$key] = $value; |
| 4506 |
} |
| 4507 |
$woocommerce->cart->add_to_cart($id,$quant,$varid,$variation,$cart_item_data); |
| 4508 |
} |
| 4509 |
} |
| 4510 |
do_action('woo_vipps_cart_restored'); |
| 4511 |
} |
| 4512 |
|
| 4513 |
// Should only be run when this is an order in our own session, |
| 4514 |
// used in the ajax_check_order_status and vipps_payment methods, where we are |
| 4515 |
// expecting a customer return that may be from Express Checkout. If it is, we may |
| 4516 |
// have no customer email in the current session, which in 7.8.2 will stop the user from |
| 4517 |
// viewing his or her orders. IOK 2023-07-17 |
| 4518 |
function maybe_set_session_customer_email($order) { |
| 4519 |
if ($order->get_meta('_vipps_express_checkout')) { |
| 4520 |
$email = $order->get_billing_email(); |
| 4521 |
if ($email && WC()->customer) { |
| 4522 |
WC()->customer->set_email($email); |
| 4523 |
WC()->customer->set_billing_email($email); |
| 4524 |
WC()->customer->save(); |
| 4525 |
WC()->session->set('tstamp', time()); // Just to ensure it is 'dirty' |
| 4526 |
} else { |
| 4527 |
$this->log(__("Could not get user email from order before thankyou-page", 'woo-vipps')); |
| 4528 |
} |
| 4529 |
} |
| 4530 |
} |
| 4531 |
|
| 4532 |
// Maybe log in user |
| 4533 |
// It is done on the thank-you page of the order, and only for express checkout. |
| 4534 |
function maybe_log_in_user ($order) { |
| 4535 |
if (is_user_logged_in()) return; |
| 4536 |
if (!$order || ! self::is_vipps_order($order)) return; |
| 4537 |
|
| 4538 |
// We *do* want to log in express checkout customers, but not those that |
| 4539 |
// use the Checkout solution - those can change their emails in the |
| 4540 |
// checkout screen. IOK 2021-09-03 |
| 4541 |
$do_login = $order->get_meta('_vipps_express_checkout'); |
| 4542 |
|
| 4543 |
// We will not log in Checkout users unless the option for that is true |
| 4544 |
if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) { |
| 4545 |
$do_login = false; |
| 4546 |
} |
| 4547 |
|
| 4548 |
|
| 4549 |
// Make this filterable because you may want to only log on some users |
| 4550 |
$do_login = apply_filters('woo_vipps_login_user_on_express_checkout', $do_login, $order); |
| 4551 |
if (!$do_login) return; |
| 4552 |
|
| 4553 |
$customer = $this->express_checkout_get_vipps_customer ($order); |
| 4554 |
if( $customer) { |
| 4555 |
$usermeta=get_userdata($customer->get_id()); |
| 4556 |
$iscustomer = (in_array('customer', $usermeta->roles) || in_array('subscriber', $usermeta->roles)); |
| 4557 |
// Ensure we don't have any admins with an additonal customer role logged in like this |
| 4558 |
if($iscustomer && !user_can($customer->get_id(), 'manage_woocommerce') && !user_can($customer->get_id(),'manage_options')) { |
| 4559 |
do_action('express_checkout_before_customer_login', $customer, $order); |
| 4560 |
|
| 4561 |
$user = new WP_User( $customer->get_id()); |
| 4562 |
wp_set_current_user($customer->get_id(), $user->user_login); |
| 4563 |
|
| 4564 |
wp_set_auth_cookie($customer->get_id()); |
| 4565 |
do_action('wp_login', $user->user_login, $user); |
| 4566 |
} |
| 4567 |
} |
| 4568 |
} |
| 4569 |
|
| 4570 |
// Get the customer that corresponds to the current order, maybe creating the customer if it does not exist yet and |
| 4571 |
// the settings allow it. |
| 4572 |
function express_checkout_get_vipps_customer($order) { |
| 4573 |
if (!$order || ! self::is_vipps_order($order)) return null; |
| 4574 |
// specific code for this by netthandelsgruppen if the below function exists |
| 4575 |
if (function_exists('create_assign_user_on_vipps_callback')) return null; |
| 4576 |
|
| 4577 |
// Both Checkout and Express Checkout have the below value set to true |
| 4578 |
if (!$order->get_meta('_vipps_express_checkout')) return; |
| 4579 |
|
| 4580 |
// Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting |
| 4581 |
// IOK 2023-07-27 |
| 4582 |
$ischeckout = $order->get_meta('_vipps_checkout'); |
| 4583 |
if ($ischeckout) { |
| 4584 |
if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null; |
| 4585 |
} else { |
| 4586 |
if ($this->gateway()->get_option('expresscreateuser') != 'yes') return null; |
| 4587 |
} |
| 4588 |
|
| 4589 |
if (is_user_logged_in()) return new WC_Customer(get_current_user_id()); |
| 4590 |
if ($order->get_user_id()) return new WC_Customer($order->get_user_id()); |
| 4591 |
|
| 4592 |
$email = $order->get_billing_email(); |
| 4593 |
|
| 4594 |
// Existing customer, so update the order (and possibly the site if multisite) and return the customer. IOK 2020-10-09 |
| 4595 |
if (email_exists($email)) { |
| 4596 |
$user = get_user_by( 'email', $email); |
| 4597 |
if (!$user) return null; |
| 4598 |
$customerid = $user->ID; |
| 4599 |
$order->set_customer_id( $user->ID ); |
| 4600 |
$order->save(); |
| 4601 |
|
| 4602 |
if (is_multisite() && ! is_user_member_of_blog($customerid, get_current_blog_id())) { |
| 4603 |
add_user_to_blog( get_current_blog_id(), $customerid, 'customer' ); |
| 4604 |
} |
| 4605 |
$customer = new WC_Customer($customerid); |
| 4606 |
return $customer; |
| 4607 |
} |
| 4608 |
|
| 4609 |
// Previously this got the user data from Vipps here as a third argument; this is no longer available after refactoring. |
| 4610 |
$user = []; |
| 4611 |
$maybecreateuser = apply_filters('woo_vipps_create_user_on_express_checkout', true, $order, $user); |
| 4612 |
if (! $maybecreateuser) return; |
| 4613 |
|
| 4614 |
// No customer yet. As we want to create users like this (set in the settings) let's do so. |
| 4615 |
// Username will be created from email, but the settings may stop generating passwords, so we force that to be generated. IOK 2020-10-09 |
| 4616 |
$firstname = $order->get_billing_first_name(); |
| 4617 |
$lastname = $order->get_billing_last_name(); |
| 4618 |
$name = $firstname; |
| 4619 |
$userdata = array('user_nicename'=>$name, 'display_name'=>"$firstname $lastname", 'nickname'=>$firstname, 'first_name'=>$firstname, 'last_name'=>$lastname); |
| 4620 |
|
| 4621 |
// Add filter to allow other ways of creating usernames. |
| 4622 |
$newusername = apply_filters('woo_vipps_express_checkout_new_username', '', $email, $userdata, $order); |
| 4623 |
|
| 4624 |
$customerid = wc_create_new_customer($email, $newusername, wp_generate_password(), $userdata); |
| 4625 |
if ($customerid && !is_wp_error($customerid)) { |
| 4626 |
$order->set_customer_id( $customerid ); |
| 4627 |
$order->save(); |
| 4628 |
|
| 4629 |
// Ensure the standard WP user fields are set too IOK 2020-11-03 |
| 4630 |
wp_update_user(array('ID' => $customerid, 'first_name' => $firstname, 'last_name' => $lastname, 'display_name' => "$firstname $lastname", 'nickname' => $firstname)); |
| 4631 |
|
| 4632 |
update_user_meta( $customerid, 'billing_address_1', $order->get_billing_address_1() ); |
| 4633 |
update_user_meta( $customerid, 'billing_address_2', $order->get_billing_address_2() ); |
| 4634 |
update_user_meta( $customerid, 'billing_city', $order->get_billing_city() ); |
| 4635 |
update_user_meta( $customerid, 'billing_company', $order->get_billing_company() ); |
| 4636 |
update_user_meta( $customerid, 'billing_country', $order->get_billing_country() ); |
| 4637 |
update_user_meta( $customerid, 'billing_email', $order->get_billing_email() ); |
| 4638 |
update_user_meta( $customerid, 'billing_first_name', $order->get_billing_first_name() ); |
| 4639 |
update_user_meta( $customerid, 'billing_last_name', $order->get_billing_last_name() ); |
| 4640 |
update_user_meta( $customerid, 'billing_phone', $order->get_billing_phone() ); |
| 4641 |
update_user_meta( $customerid, 'billing_postcode', $order->get_billing_postcode() ); |
| 4642 |
update_user_meta( $customerid, 'billing_state', $order->get_billing_state() ); |
| 4643 |
update_user_meta( $customerid, 'shipping_address_1', $order->get_shipping_address_1() ); |
| 4644 |
update_user_meta( $customerid, 'shipping_address_2', $order->get_shipping_address_2() ); |
| 4645 |
update_user_meta( $customerid, 'shipping_city', $order->get_shipping_city() ); |
| 4646 |
update_user_meta( $customerid, 'shipping_company', $order->get_shipping_company() ); |
| 4647 |
update_user_meta( $customerid, 'shipping_country', $order->get_shipping_country() ); |
| 4648 |
update_user_meta( $customerid, 'shipping_first_name', $order->get_shipping_first_name() ); |
| 4649 |
update_user_meta( $customerid, 'shipping_last_name', $order->get_shipping_last_name() ); |
| 4650 |
update_user_meta( $customerid, 'shipping_method', $order->get_shipping_method() ); |
| 4651 |
update_user_meta( $customerid, 'shipping_postcode', $order->get_shipping_postcode() ); |
| 4652 |
update_user_meta( $customerid, 'shipping_state', $order->get_shipping_state() ); |
| 4653 |
|
| 4654 |
// Integration with All-in-one WP security - these accounts are created by validated accounts in the app. |
| 4655 |
update_user_meta( $customerid,'aiowps_account_status', 'approved'); |
| 4656 |
|
| 4657 |
$customer = new WC_Customer($customerid); |
| 4658 |
do_action('woo_vipps_express_checkout_new_customer', $customer, $order->get_id()); |
| 4659 |
|
| 4660 |
return $customer; |
| 4661 |
} |
| 4662 |
if (is_wp_error($customerid)) { |
| 4663 |
$this->log(__("Error creating customer in express checkout: ", 'woo-vipps') . $customerid->get_error_message()); |
| 4664 |
} else { |
| 4665 |
$this->log(__("Unknown error customer in express checkout.", 'woo-vipps')); |
| 4666 |
} |
| 4667 |
return null; |
| 4668 |
} |
| 4669 |
|
| 4670 |
// This restores the cart on order complete, but only if the current order was a single product buy with an active cart. |
| 4671 |
public function maybe_restore_cart($orderid,$failed=false) { |
| 4672 |
if (!$orderid) return; |
| 4673 |
$o = null; |
| 4674 |
try { |
| 4675 |
$o = wc_get_order($orderid); |
| 4676 |
} catch (Exception $e) { |
| 4677 |
// Well, we tried. |
| 4678 |
} |
| 4679 |
if (!$o) return; |
| 4680 |
if (!$o->get_meta('_vipps_single_product_express')) return; |
| 4681 |
if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return; |
| 4682 |
if ($failed) WC()->cart->empty_cart(); |
| 4683 |
$this->restore_cart($o); |
| 4684 |
} |
| 4685 |
|
| 4686 |
|
| 4687 |
public function ajax_vipps_buy_single_product () { |
| 4688 |
Vipps::nocache(); |
| 4689 |
static::set_locale_if_in_header(); |
| 4690 |
// We're not checking ajax referer here, because what we do is creating a session and redirecting to the |
| 4691 |
// 'create order' page wherein we'll do the actual work. IOK 2018-09-28 |
| 4692 |
$session = WC()->session; |
| 4693 |
if (!$session->has_session()) { |
| 4694 |
$session->set_customer_session_cookie(true); |
| 4695 |
} |
| 4696 |
$session->set('__vipps_buy_product', json_encode($_REQUEST)); |
| 4697 |
|
| 4698 |
// Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to |
| 4699 |
// add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12 |
| 4700 |
$url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url()); |
| 4701 |
|
| 4702 |
$result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url); |
| 4703 |
wp_send_json($result); |
| 4704 |
exit(); |
| 4705 |
} |
| 4706 |
|
| 4707 |
public function ajax_do_express_checkout () { |
| 4708 |
check_ajax_referer('do_express','sec'); |
| 4709 |
Vipps::nocache(); |
| 4710 |
static::set_locale_if_in_header(); |
| 4711 |
$gw = $this->gateway(); |
| 4712 |
|
| 4713 |
if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) { |
| 4714 |
$result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false); |
| 4715 |
wp_send_json($result); |
| 4716 |
exit(); |
| 4717 |
} |
| 4718 |
|
| 4719 |
|
| 4720 |
|
| 4721 |
|
| 4722 |
// Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices. |
| 4723 |
$toolate = false; |
| 4724 |
$msg = ""; |
| 4725 |
$valid = WC()->cart->check_cart_item_validity(); |
| 4726 |
if ( is_wp_error( $valid) ) { |
| 4727 |
$toolate = true; |
| 4728 |
$msg = "<br>" . $valid->get_error_message(); |
| 4729 |
} |
| 4730 |
$stock = WC()->cart->check_cart_item_stock(); |
| 4731 |
if ( is_wp_error( $stock) ) { |
| 4732 |
$toolate = true; |
| 4733 |
$msg = "<br>" . $stock->get_error_message(); |
| 4734 |
} |
| 4735 |
|
| 4736 |
if ($toolate) { |
| 4737 |
$result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false); |
| 4738 |
wp_send_json($result); |
| 4739 |
exit(); |
| 4740 |
} |
| 4741 |
|
| 4742 |
try { |
| 4743 |
$orderid = $gw->create_partial_order(); |
| 4744 |
do_action('woo_vipps_ajax_do_express_checkout', $orderid); |
| 4745 |
} catch (Exception $e) { |
| 4746 |
$this->log($e->getMessage(),'error'); |
| 4747 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false); |
| 4748 |
wp_send_json($result); |
| 4749 |
exit(); |
| 4750 |
} |
| 4751 |
if (!$orderid) { |
| 4752 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false); |
| 4753 |
wp_send_json($result); |
| 4754 |
exit(); |
| 4755 |
} |
| 4756 |
|
| 4757 |
try { |
| 4758 |
$this->maybe_add_static_shipping($gw,$orderid); |
| 4759 |
} catch (Exception $e) { |
| 4760 |
$this->log(__("Error calculating static shipping", 'woo-vipps'), 'error'); |
| 4761 |
$this->log($e->getMessage(),'error'); |
| 4762 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false); |
| 4763 |
wp_send_json($result); |
| 4764 |
exit(); |
| 4765 |
} |
| 4766 |
|
| 4767 |
$ok = $gw->process_payment($orderid); |
| 4768 |
if ($ok && $ok['result'] == 'success') { |
| 4769 |
$result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']); |
| 4770 |
wp_send_json($result); |
| 4771 |
exit(); |
| 4772 |
} |
| 4773 |
$result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>''); |
| 4774 |
wp_send_json($result); |
| 4775 |
exit(); |
| 4776 |
} |
| 4777 |
|
| 4778 |
// Same as ajax_do_express_checkout, but for a single product/variation. Duplicate code because we want to manipulate the cart differently here. IOK 2018-09-25 |
| 4779 |
public function ajax_do_single_product_express_checkout() { |
| 4780 |
check_ajax_referer('do_express','sec'); |
| 4781 |
Vipps::nocache(); |
| 4782 |
static::set_locale_if_in_header(); |
| 4783 |
require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php"); |
| 4784 |
$gw = $this->gateway(); |
| 4785 |
|
| 4786 |
if (!$gw->express_checkout_available()) { |
| 4787 |
$result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false); |
| 4788 |
wp_send_json($result); |
| 4789 |
exit(); |
| 4790 |
} |
| 4791 |
|
| 4792 |
|
| 4793 |
// Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but |
| 4794 |
// we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02 |
| 4795 |
$varid = intval(@$_POST['variation_id']); |
| 4796 |
$prodid = intval(@$_POST['product_id']); |
| 4797 |
$sku = sanitize_text_field(@$_POST['sku']); |
| 4798 |
$quant = intval(@$_POST['quantity']); |
| 4799 |
|
| 4800 |
// Get any attributes posted for variable products (where one of the dimensions is "any" for instance) |
| 4801 |
$variations = array(); |
| 4802 |
foreach ($_POST as $key => $value ) { |
| 4803 |
if ( 'attribute_' !== substr( $key, 0, 10 ) ) { |
| 4804 |
continue; |
| 4805 |
} |
| 4806 |
$variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value ); |
| 4807 |
} |
| 4808 |
|
| 4809 |
$product = null; |
| 4810 |
$variant = null; |
| 4811 |
$parent = null; |
| 4812 |
$parentid = null; |
| 4813 |
$quantity = 1; |
| 4814 |
if ($quant && $quant>1) $quantity=$quant; |
| 4815 |
|
| 4816 |
// Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02 |
| 4817 |
// Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24 |
| 4818 |
// This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report |
| 4819 |
try { |
| 4820 |
if ($varid) { |
| 4821 |
$product = wc_get_product($varid); |
| 4822 |
} elseif ($prodid) { |
| 4823 |
$product = wc_get_product($prodid); |
| 4824 |
} elseif ($sku) { |
| 4825 |
$skuid = wc_get_product_id_by_sku($sku); |
| 4826 |
$product = wc_get_product($skuid); |
| 4827 |
} |
| 4828 |
} catch (Exception $e) { |
| 4829 |
$result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false); |
| 4830 |
wp_send_json($result); |
| 4831 |
exit(); |
| 4832 |
} |
| 4833 |
|
| 4834 |
|
| 4835 |
if (!$product) { |
| 4836 |
$result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false); |
| 4837 |
wp_send_json($result); |
| 4838 |
exit(); |
| 4839 |
} |
| 4840 |
|
| 4841 |
$parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid. |
| 4842 |
$parent = $parentid ? wc_get_product($parentid) : null; |
| 4843 |
|
| 4844 |
// This can't really happen, but if it did.. |
| 4845 |
if ($prodid && $parentid && ($prodid != $parentid)) { |
| 4846 |
$result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false); |
| 4847 |
wp_send_json($result); |
| 4848 |
exit(); |
| 4849 |
} |
| 4850 |
if (!$gw->product_supports_express_checkout($product)) { |
| 4851 |
$result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false); |
| 4852 |
wp_send_json($result); |
| 4853 |
exit(); |
| 4854 |
} |
| 4855 |
|
| 4856 |
// Somebody addded the wrong SKU |
| 4857 |
if ($product->get_type() == 'variable'){ |
| 4858 |
$result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false); |
| 4859 |
wp_send_json($result); |
| 4860 |
exit(); |
| 4861 |
} |
| 4862 |
// Final check of availability |
| 4863 |
if (!$product->is_purchasable() || !$product->is_in_stock()) { |
| 4864 |
$result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false); |
| 4865 |
wp_send_json($result); |
| 4866 |
exit(); |
| 4867 |
} |
| 4868 |
|
| 4869 |
// Now it should be safe to continue to the checkout process. IOK 2018-10-02 |
| 4870 |
|
| 4871 |
// Create a new temporary cart for this order. We need to get (and save) the real session cart, |
| 4872 |
// because some plugins actually override this. |
| 4873 |
$current_cart = clone WC()->cart; |
| 4874 |
WC()->cart->empty_cart(); |
| 4875 |
|
| 4876 |
if ($parent && $parent->get_type() == 'variable') { |
| 4877 |
WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations); |
| 4878 |
} else { |
| 4879 |
WC()->cart->add_to_cart($product->get_id(),$quantity); |
| 4880 |
} |
| 4881 |
|
| 4882 |
try { |
| 4883 |
$orderid = $gw->create_partial_order(); |
| 4884 |
do_action('woo_vipps_ajax_do_express_checkout', $orderid); |
| 4885 |
} catch (Exception $e) { |
| 4886 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false); |
| 4887 |
wp_send_json($result); |
| 4888 |
exit(); |
| 4889 |
} |
| 4890 |
|
| 4891 |
if (!$orderid) { |
| 4892 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false); |
| 4893 |
wp_send_json($result); |
| 4894 |
exit(); |
| 4895 |
} |
| 4896 |
|
| 4897 |
try { |
| 4898 |
$this->maybe_add_static_shipping($gw,$orderid); |
| 4899 |
} catch (Exception $e) { |
| 4900 |
$this->log(__("Error calculating static shipping", 'woo-vipps'), 'error'); |
| 4901 |
$this->log($e->getMessage(),'error'); |
| 4902 |
$result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false); |
| 4903 |
wp_send_json($result); |
| 4904 |
exit(); |
| 4905 |
} |
| 4906 |
|
| 4907 |
|
| 4908 |
// Single product purchase, so save any contents of the real cart |
| 4909 |
$order = wc_get_order($orderid); |
| 4910 |
$order->update_meta_data('_vipps_single_product_express',true); |
| 4911 |
$order->save(); |
| 4912 |
$this->save_cart($order,$current_cart); |
| 4913 |
|
| 4914 |
$ok = $gw->process_payment($orderid); |
| 4915 |
if ($ok && $ok['result'] == 'success') { |
| 4916 |
$result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']); |
| 4917 |
wp_send_json($result); |
| 4918 |
exit(); |
| 4919 |
} |
| 4920 |
$result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>''); |
| 4921 |
wp_send_json($result); |
| 4922 |
exit(); |
| 4923 |
} |
| 4924 |
|
| 4925 |
// This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19 |
| 4926 |
// Made visible for consistency with add_static_shipping. IOK 2021-10-22 |
| 4927 |
public function maybe_add_static_shipping($gw, $orderid, $ischeckout=false) { |
| 4928 |
$key = $ischeckout ? 'enablestaticshipping_checkout' : 'enablestaticshipping'; |
| 4929 |
$ok = $gw->get_option($key) == 'yes'; |
| 4930 |
$ok = apply_filters('woo_vipps_enable_static_shipping', $ok, $orderid); |
| 4931 |
if ($ok) { |
| 4932 |
return $this->add_static_shipping($gw, $orderid, $ischeckout); |
| 4933 |
} |
| 4934 |
} |
| 4935 |
|
| 4936 |
// And this function adds static shipping no matter what. It may need to be used in plugins, hence visible. IOK 2021-10-22 |
| 4937 |
public function add_static_shipping ($gw, $orderid, $ischeckout=false) { |
| 4938 |
$order = wc_get_order($orderid); |
| 4939 |
$prefix = $gw->get_orderprefix(); |
| 4940 |
$vippsorderid = apply_filters('woo_vipps_orderid', $prefix.$orderid, $prefix, $order); |
| 4941 |
$addressinfo = $this->get_static_shipping_address_data(); |
| 4942 |
|
| 4943 |
// Both Checkout and new Express Checkout supports LocalPickup, so add it (it is normally only present for Gutenberg checkout) |
| 4944 |
// Add special shipping methods (LocalPickup etc); |
| 4945 |
$this->load_extra_shipping_methods($order, $addressinfo, $ischeckout); |
| 4946 |
|
| 4947 |
$options = $this->vipps_shipping_details_callback_handler($order, $addressinfo,$vippsorderid, $ischeckout); |
| 4948 |
|
| 4949 |
if ($options) { |
| 4950 |
$order->update_meta_data('_vipps_static_shipping', $options); |
| 4951 |
$order->save(); |
| 4952 |
} |
| 4953 |
} |
| 4954 |
|
| 4955 |
// Support local pickup. This is normally only registered when the Gutenberg Checkout block is either on the |
| 4956 |
// 'checkout-page' or in some template; but that's not nececssarily the case if Vipps MobilePay checkout is active. |
| 4957 |
// Supported also in express checkout. 2026-02-25 |
| 4958 |
// We'll add this if admin has stored *any* pickup locations at any point. IOK 2026-02-25 |
| 4959 |
// Afterwards, we need to post-process this, because *each* location gets a different rate. See the VippsCheckout class. |
| 4960 |
function maybe_load_pickup_locations () { |
| 4961 |
$locations = get_option('pickup_location_pickup_locations', array()); |
| 4962 |
if (!empty($locations) && class_exists('Automattic\WooCommerce\Blocks\Shipping\PickupLocation')) { |
| 4963 |
$ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() ); |
| 4964 |
} |
| 4965 |
} |
| 4966 |
|
| 4967 |
// Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08 |
| 4968 |
// Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback. |
| 4969 |
public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) { |
| 4970 |
// If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06 |
| 4971 |
add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) { |
| 4972 |
// Previously we loaded PickupLocations here; we now do that if any are defined at all. The old custom filter still runs though, |
| 4973 |
// and last. IOK 2026-02-25 |
| 4974 |
do_action('woo_vipps_express_load_shipping_methods', $order, $addressdata); |
| 4975 |
}, 99); |
| 4976 |
} |
| 4977 |
|
| 4978 |
|
| 4979 |
// Check the status of the order if it is a part of our session, and return a result to the handler function IOK 2018-05-04 |
| 4980 |
public function ajax_check_order_status () { |
| 4981 |
check_ajax_referer('vippsstatus','sec'); |
| 4982 |
static::set_locale_if_in_header(); |
| 4983 |
Vipps::nocache(); |
| 4984 |
|
| 4985 |
$orderid= wc_get_order_id_by_order_key(sanitize_text_field(@$_POST['key'])); |
| 4986 |
$transaction = sanitize_text_field(@$_POST['transaction']); |
| 4987 |
|
| 4988 |
$sessionorders= WC()->session->get('_vipps_session_orders'); |
| 4989 |
if (!isset($sessionorders[$orderid])) { |
| 4990 |
wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps'))); |
| 4991 |
} |
| 4992 |
|
| 4993 |
$order = wc_get_order($orderid); |
| 4994 |
if (!$order) { |
| 4995 |
wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps'))); |
| 4996 |
} |
| 4997 |
$order_status = $this->check_order_status($order); |
| 4998 |
// No callback has occured yet. If this has been going on for a while, check directly with Vipps |
| 4999 |
if ($order_status == 'pending') { |
| 5000 |
wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps'))); |
| 5001 |
return false; |
| 5002 |
} |
| 5003 |
if ($order_status == 'cancelled' || $order_status == 'failed') { |
| 5004 |
$this->maybe_restore_cart($orderid,'failed'); |
| 5005 |
wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps'), 'order_status' => $order_status)); |
| 5006 |
return false; |
| 5007 |
} |
| 5008 |
|
| 5009 |
// Order status isn't pending anymore, but there can be custom statuses, so check the payment status instead. |
| 5010 |
$order = wc_get_order($orderid); // Reload |
| 5011 |
$gw = $this->gateway(); |
| 5012 |
$payment = $gw->check_payment_status($order); |
| 5013 |
if ($payment == 'initiated') { |
| 5014 |
wp_send_json(array('status'=>'waiting', 'msg'=>__('Waiting on order', 'woo-vipps'))); |
| 5015 |
return false; |
| 5016 |
} |
| 5017 |
|
| 5018 |
|
| 5019 |
if ($payment == 'authorized') { |
| 5020 |
// IOK Previously handled in the thankyou hook 2023-07-17 |
| 5021 |
$this->woocommerce_before_thankyou($order->get_id()); |
| 5022 |
wp_send_json(array('status'=>'ok', 'msg'=>__('Payment authorized', 'woo-vipps'))); |
| 5023 |
return false; |
| 5024 |
} |
| 5025 |
if ($payment == 'complete') { |
| 5026 |
// IOK Previously handled in the thankyou hook 2023-07-17 |
| 5027 |
$this->woocommerce_before_thankyou($order->get_id()); |
| 5028 |
wp_send_json(array('status'=>'ok', 'msg'=>__('Payment captured', 'woo-vipps'))); |
| 5029 |
return false; |
| 5030 |
} |
| 5031 |
if ($payment == 'cancelled') { |
| 5032 |
$this->maybe_restore_cart($orderid,'failed'); |
| 5033 |
wp_send_json(array('status'=>'failed', 'msg'=>__('Order failed', 'woo-vipps'))); |
| 5034 |
return false; |
| 5035 |
} |
| 5036 |
wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment)); |
| 5037 |
return false; |
| 5038 |
} |
| 5039 |
|
| 5040 |
// The various return URLs for special pages of the Vipps stuff. Previously used a fake page and had to check permalink_structure. LP 2026-08-26 |
| 5041 |
private function make_special_page_url($action) { |
| 5042 |
return add_query_arg('action', $action, $this->get_special_page_url()); |
| 5043 |
} |
| 5044 |
|
| 5045 |
public function payment_return_url() { |
| 5046 |
return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment')); |
| 5047 |
} |
| 5048 |
public function express_checkout_url() { |
| 5049 |
return $this->make_special_page_url('do_express_checkout'); |
| 5050 |
} |
| 5051 |
public function buy_product_url() { |
| 5052 |
return $this->make_special_page_url('buy_product'); |
| 5053 |
} |
| 5054 |
|
| 5055 |
public static function is_special_page() { |
| 5056 |
$id = static::get_special_page_id(); |
| 5057 |
return $id && is_page($id); |
| 5058 |
} |
| 5059 |
|
| 5060 |
public static function get_special_page_id() { |
| 5061 |
$id = wc_get_page_id('vipps_special_page'); // -1 if not found |
| 5062 |
return $id > 0 ? $id : null; |
| 5063 |
} |
| 5064 |
|
| 5065 |
public static function get_special_page_url() { |
| 5066 |
return get_permalink(static::get_special_page_id()); |
| 5067 |
} |
| 5068 |
|
| 5069 |
// Just create a spinner and a overlay. |
| 5070 |
public function spinner () { |
| 5071 |
$flavour = sanitize_title($this->get_payment_method_name()); |
| 5072 |
ob_start(); |
| 5073 |
?> |
| 5074 |
<div class="vippsoverlay"> |
| 5075 |
<div id="floatingCirclesG" class="vippsspinner <?php echo esc_attr($flavour); ?>"> |
| 5076 |
<div class="f_circleG" id="frotateG_01"></div> |
| 5077 |
<div class="f_circleG" id="frotateG_02"></div> |
| 5078 |
<div class="f_circleG" id="frotateG_03"></div> |
| 5079 |
<div class="f_circleG" id="frotateG_04"></div> |
| 5080 |
<div class="f_circleG" id="frotateG_05"></div> |
| 5081 |
<div class="f_circleG" id="frotateG_06"></div> |
| 5082 |
<div class="f_circleG" id="frotateG_07"></div> |
| 5083 |
<div class="f_circleG" id="frotateG_08"></div> |
| 5084 |
</div> |
| 5085 |
</div> |
| 5086 |
<?php |
| 5087 |
return apply_filters('woo_vipps_spinner', ob_get_clean()); |
| 5088 |
} |
| 5089 |
|
| 5090 |
|
| 5091 |
// DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26 |
| 5092 |
// NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30 |
| 5093 |
public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') { |
| 5094 |
return $this->get_html_button_for_context($context); |
| 5095 |
} |
| 5096 |
|
| 5097 |
// DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26 |
| 5098 |
// Get payment logo based on payment method, then language NT 2023-11-30 |
| 5099 |
// and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15 |
| 5100 |
// NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30 |
| 5101 |
public function get_payment_logo($context = 'global') { |
| 5102 |
return $this->get_express_logo(null, null, null, $context); |
| 5103 |
} |
| 5104 |
|
| 5105 |
// Get express banner logo based on payment method. LP 2025-09-03 |
| 5106 |
private function get_express_banner_logo() { |
| 5107 |
$payment_method = $this->get_payment_method_name(); |
| 5108 |
|
| 5109 |
if($payment_method === "Vipps"){ |
| 5110 |
return plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__); |
| 5111 |
} else if($payment_method === "MobilePay"){ |
| 5112 |
return plugins_url('img/mobilepay-white.svg',__FILE__); |
| 5113 |
} |
| 5114 |
return null; |
| 5115 |
} |
| 5116 |
|
| 5117 |
// Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27 |
| 5118 |
// $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will |
| 5119 |
// be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26 |
| 5120 |
public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) { |
| 5121 |
$disabled = $disabled ? 'disabled' : ''; |
| 5122 |
$data = array(); |
| 5123 |
|
| 5124 |
// Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23 |
| 5125 |
if ($product_id && !$variation_id) { |
| 5126 |
$product = wc_get_product($product_id); |
| 5127 |
if ($product && is_a($product, 'WC_Product_Variation')) { |
| 5128 |
$variation_id = $product_id; |
| 5129 |
$product_id = $product->get_parent_id(); |
| 5130 |
} |
| 5131 |
} |
| 5132 |
|
| 5133 |
if ($sku) $data['product_sku'] = $sku; |
| 5134 |
if ($product_id) $data['product_id'] = $product_id; |
| 5135 |
if ($variation_id) $data['variation_id'] = $variation_id; |
| 5136 |
|
| 5137 |
$buttoncode = "<a href='javascript:void(0)' $disabled "; |
| 5138 |
foreach($data as $key=>$value) { |
| 5139 |
$value = esc_attr($value); |
| 5140 |
$buttoncode .= " data-$key='$value' "; |
| 5141 |
} |
| 5142 |
|
| 5143 |
$payment_method = $this->get_payment_method_name(); |
| 5144 |
$title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method); |
| 5145 |
|
| 5146 |
if (is_array($button_args_override) && $button_args_override) { |
| 5147 |
$button_args = $button_args_override; |
| 5148 |
} else { |
| 5149 |
$button_args = $this->get_html_button_attrs_for_context($context); |
| 5150 |
} |
| 5151 |
$short = ($button_args['compact'] ?? 'false') === 'true'; |
| 5152 |
$button = $this->get_html_button($button_args); |
| 5153 |
|
| 5154 |
# Extra classes, if passed IOK 2019-02-26 |
| 5155 |
if (is_array($classes)) { |
| 5156 |
$classes = join(" ", $classes); |
| 5157 |
} |
| 5158 |
if ($classes) $classes = " $classes"; |
| 5159 |
if ($short) $classes = "short $classes"; |
| 5160 |
|
| 5161 |
$buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>"; |
| 5162 |
|
| 5163 |
|
| 5164 |
|
| 5165 |
return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled); |
| 5166 |
} |
| 5167 |
|
| 5168 |
// Display a 'buy now with express checkout' button on the product page IOK 2018-09-27 |
| 5169 |
public function single_product_buy_now_button () { |
| 5170 |
$gw = $this->gateway(); |
| 5171 |
$how = $gw->get_option('singleproductexpress'); |
| 5172 |
if ($how == 'none') return; |
| 5173 |
if (!$gw->express_checkout_available()) return; |
| 5174 |
|
| 5175 |
global $product; |
| 5176 |
$prodid = $product->get_id(); |
| 5177 |
if (!$gw->product_supports_express_checkout($product)) return; |
| 5178 |
|
| 5179 |
// Vipps does not support 0,- products, so we need to check. |
| 5180 |
// get_price() should normally return the lowest price for variable products, but that can fail, |
| 5181 |
// so we dispatch on the type and use the *minimum* price instead, requiring that to be nonzero. IOK 2022-06-08 |
| 5182 |
$showit = true; |
| 5183 |
if (is_a($product, 'WC_Product_Variable')) { |
| 5184 |
$minprice = $product->get_variation_price('min', 0); |
| 5185 |
if ($minprice > 0) $showit = true; |
| 5186 |
} else { |
| 5187 |
if ($product->get_price() <= 0) $showit = false; |
| 5188 |
} |
| 5189 |
|
| 5190 |
if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false; |
| 5191 |
$showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product); |
| 5192 |
if (!$showit) return; |
| 5193 |
|
| 5194 |
$classes = array(); |
| 5195 |
$disabled=""; |
| 5196 |
if ($product->is_type('variable')) { |
| 5197 |
$disabled="disabled"; |
| 5198 |
$classes[] = 'variable-product'; |
| 5199 |
} |
| 5200 |
|
| 5201 |
# If true, add a class that signals that the button should be added in 'compat mode', which is compatible with |
| 5202 |
# more plugins because it does not handle tha product add itself. IOK 2019-02-26 |
| 5203 |
$compat = ($gw->get_option('singleproductbuynowcompatmode') == 'yes'); |
| 5204 |
$compat = apply_filters('woo_vipps_single_product_compat_mode', $compat, $product); |
| 5205 |
|
| 5206 |
if ($compat) $classes[] ='compat-mode'; |
| 5207 |
$classes = apply_filters('woo_vipps_single_product_buy_now_classes', $classes, $product); |
| 5208 |
|
| 5209 |
$button = $this->get_buy_now_button(false,false,false, ($product->is_type('variable') ? 'disabled' : false), $classes, 'product'); |
| 5210 |
$code = "<div class='vipps_buy_now_wrapper noloop'>$button</div>"; |
| 5211 |
echo $code; |
| 5212 |
} |
| 5213 |
|
| 5214 |
|
| 5215 |
// True for products that are purchasable using Vipps Express Checkout |
| 5216 |
public function loop_single_product_is_express_checkout_purchasable($product) { |
| 5217 |
if (!$product) return false; |
| 5218 |
if (!$product->is_purchasable() || !$product->is_in_stock() || !$product->supports( 'ajax_add_to_cart' )) return false; |
| 5219 |
$gw = $this->gateway(); |
| 5220 |
|
| 5221 |
if (!$gw->express_checkout_available()) return false; |
| 5222 |
if (!$gw->product_supports_express_checkout($product)) return false; |
| 5223 |
if ($gw->get_option('singleproductexpressarchives') != 'yes') return false; |
| 5224 |
|
| 5225 |
$how = $gw->get_option('singleproductexpress'); |
| 5226 |
if ($how == 'none') return false; |
| 5227 |
$prodid = $product->get_id(); |
| 5228 |
|
| 5229 |
$showit = true; |
| 5230 |
if ($product->get_price() <= 0) $showit = false; |
| 5231 |
if ( $how=='some' && 'yes' != get_post_meta($prodid, '_vipps_buy_now_button', true)) $showit = false; |
| 5232 |
$showit = apply_filters('woo_vipps_show_single_product_buy_now', $showit, $product); |
| 5233 |
$showit = apply_filters('woo_vipps_show_single_product_buy_now_in_loop', $showit, $product); |
| 5234 |
return $showit; |
| 5235 |
} |
| 5236 |
|
| 5237 |
// Print a "buy now with vipps" for products in the loop, like on a category page |
| 5238 |
public function loop_single_product_buy_now_button() { |
| 5239 |
global $product; |
| 5240 |
|
| 5241 |
if (!$this->loop_single_product_is_express_checkout_purchasable($product)) return; |
| 5242 |
|
| 5243 |
$sku = $product->get_sku(); |
| 5244 |
$button = $this->get_buy_now_button($product->get_id(),false,$sku, false, '', 'catalog'); |
| 5245 |
echo "<div class='vipps_buy_now_wrapper loop'>$button</div>"; |
| 5246 |
} |
| 5247 |
|
| 5248 |
|
| 5249 |
|
| 5250 |
// Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist |
| 5251 |
// IOK 2026-04-30 remove this when checkout is end-of-life'd |
| 5252 |
// We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18 |
| 5253 |
public function woocommerce_create_pages ($data) { |
| 5254 |
// Vipps Checkout page |
| 5255 |
$vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false); |
| 5256 |
if ($vipps_checkout_activated) { |
| 5257 |
$data['vipps_checkout'] = array( |
| 5258 |
'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ), |
| 5259 |
'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ), |
| 5260 |
'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->', |
| 5261 |
); |
| 5262 |
} |
| 5263 |
|
| 5264 |
// Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18 |
| 5265 |
$data['vipps_special_page'] = [ |
| 5266 |
'name' => 'vipps-payment', // slug |
| 5267 |
/* translators: company name */ |
| 5268 |
'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27 |
| 5269 |
'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->', |
| 5270 |
]; |
| 5271 |
return $data; |
| 5272 |
} |
| 5273 |
|
| 5274 |
// Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01 |
| 5275 |
// If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02 |
| 5276 |
public function maybe_create_vipps_pages () { |
| 5277 |
$make_pages = false; |
| 5278 |
|
| 5279 |
// Vipps Checkout page. LP 2026-08-18 |
| 5280 |
$checkoutid = wc_get_page_id('vipps_checkout'); |
| 5281 |
if (!$checkoutid || ! get_post_status($checkoutid)) { |
| 5282 |
delete_option('woocommerce_vipps_checkout_page_id'); |
| 5283 |
$make_pages = true; |
| 5284 |
} |
| 5285 |
|
| 5286 |
// vipps special page, previously a fake page. LP 2026-08-18 |
| 5287 |
$builtin_special_page_id = static::get_special_page_id(); |
| 5288 |
if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) { |
| 5289 |
delete_option('woocommerce_vipps_special_page_page_id'); |
| 5290 |
$make_pages = true; |
| 5291 |
} |
| 5292 |
|
| 5293 |
if ($make_pages) { |
| 5294 |
WC_Install::create_pages(); |
| 5295 |
} |
| 5296 |
} |
| 5297 |
|
| 5298 |
public function vipps_special_page_shortcode($atts, $content) { |
| 5299 |
// No point in expanding this unless we are actually doing the special actions. LP 2026-08-25 |
| 5300 |
if (is_admin()) return; |
| 5301 |
if (wp_doing_ajax()) return; |
| 5302 |
if (defined('REST_REQUEST') && REST_REQUEST) return; |
| 5303 |
|
| 5304 |
$action = $_GET['action'] ?? ''; |
| 5305 |
switch ($action) { |
| 5306 |
case 'wait_for_payment': |
| 5307 |
$html = $this->vipps_wait_for_payment(); |
| 5308 |
break; |
| 5309 |
case 'do_express_checkout': |
| 5310 |
$html = $this->vipps_express_checkout(); |
| 5311 |
break; |
| 5312 |
case 'buy_product': |
| 5313 |
$html = $this->vipps_buy_product(); |
| 5314 |
break; |
| 5315 |
default: |
| 5316 |
$html = ''; |
| 5317 |
} |
| 5318 |
|
| 5319 |
// Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11 |
| 5320 |
return $html; |
| 5321 |
} |
| 5322 |
|
| 5323 |
|
| 5324 |
// This URL will when accessed add a product to the cart and go directly to the express checkout page. |
| 5325 |
// The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for |
| 5326 |
// the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24 |
| 5327 |
public function vipps_buy_product() { |
| 5328 |
add_filter('body_class', function ($classes) { |
| 5329 |
$classes[] = 'vipps-express-checkout'; |
| 5330 |
$classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24 |
| 5331 |
return apply_filters('woo_vipps_express_checkout_body_class', $classes); |
| 5332 |
}); |
| 5333 |
|
| 5334 |
do_action('woo_vipps_express_checkout_page'); |
| 5335 |
|
| 5336 |
$session = WC()->session; |
| 5337 |
$posted = $session->get('__vipps_buy_product'); |
| 5338 |
$session->set('__vipps_buy_product', false); // Reloads won't work but that's ok. |
| 5339 |
|
| 5340 |
|
| 5341 |
if (!$posted) { |
| 5342 |
// Find product/variation using an external shareable link |
| 5343 |
if (array_key_exists('pr',$_REQUEST)) { |
| 5344 |
global $wpdb; |
| 5345 |
$externalkey = sanitize_text_field($_REQUEST['pr']); |
| 5346 |
$search = '_vipps_shareable_link_'.esc_sql($externalkey); |
| 5347 |
$existing = $wpdb->get_row("SELECT post_id from {$wpdb->prefix}postmeta where meta_key='$search' limit 1",'ARRAY_A'); |
| 5348 |
if (!empty($existing)) { |
| 5349 |
$posted = get_post_meta($existing['post_id'], $search, true); |
| 5350 |
} |
| 5351 |
} |
| 5352 |
} |
| 5353 |
|
| 5354 |
$productinfo = false; |
| 5355 |
if (is_array($posted)) { |
| 5356 |
$productinfo = $posted; |
| 5357 |
} else { |
| 5358 |
$productinfo = $posted ? @json_decode($posted,true) : false; |
| 5359 |
} |
| 5360 |
|
| 5361 |
if (!$productinfo) { |
| 5362 |
$title = __("Product is no longer available",'woo-vipps'); |
| 5363 |
$content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps'); |
| 5364 |
return $this->special_page_html($title,$content); |
| 5365 |
} |
| 5366 |
|
| 5367 |
// Pass the productinfo to the express checkout form |
| 5368 |
$args = array(); |
| 5369 |
$args['quantity'] = 1; |
| 5370 |
if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']); |
| 5371 |
if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']); |
| 5372 |
if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']); |
| 5373 |
if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']); |
| 5374 |
|
| 5375 |
// For variable products where some of the attributes are "any", we need to add these as well. This is from woos form-handler for these. |
| 5376 |
foreach ($productinfo as $key => $value) { |
| 5377 |
if ( 'attribute_' !== substr( $key, 0, 10 ) ) { |
| 5378 |
continue; |
| 5379 |
} |
| 5380 |
$args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value)); |
| 5381 |
} |
| 5382 |
|
| 5383 |
return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args); |
| 5384 |
} |
| 5385 |
|
| 5386 |
// This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts. |
| 5387 |
public function vipps_express_checkout() { |
| 5388 |
// We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached. |
| 5389 |
// IOK 2018-05-28 |
| 5390 |
$ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express'); |
| 5391 |
|
| 5392 |
|
| 5393 |
$backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']); |
| 5394 |
if (!$backurl) $backurl = home_url(); |
| 5395 |
|
| 5396 |
if (!$ok) { |
| 5397 |
wc_add_notice(__('Link expired, please try again', 'woo-vipps')); |
| 5398 |
wp_redirect($backurl); |
| 5399 |
exit(); |
| 5400 |
} |
| 5401 |
|
| 5402 |
if ( WC()->cart->get_cart_contents_count() == 0 ) { |
| 5403 |
wc_add_notice(__('Your shopping cart is empty','woo-vipps'),'error'); |
| 5404 |
wp_redirect($backurl); |
| 5405 |
exit(); |
| 5406 |
} |
| 5407 |
|
| 5408 |
add_filter('body_class', function ($classes) { |
| 5409 |
$classes[] = 'vipps-express-checkout'; |
| 5410 |
$classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24 |
| 5411 |
return apply_filters('woo_vipps_express_checkout_body_class', $classes); |
| 5412 |
}); |
| 5413 |
|
| 5414 |
do_action('woo_vipps_express_checkout_page'); |
| 5415 |
|
| 5416 |
return $this->express_checkout_page_html(true, 'do_express_checkout'); |
| 5417 |
} |
| 5418 |
|
| 5419 |
// This method tries to ensure that a customer does not 'lose' the return page and |
| 5420 |
// starts ordering the same products twice. IOK 2020-01-22 |
| 5421 |
protected function validate_express_checkout_orderspec ($orderspec) { |
| 5422 |
if (empty($orderspec)) return true; // It's not a duplicate, it's nothing. |
| 5423 |
|
| 5424 |
// First build for the current order an array of hash-tables keyed by prodid, varid and quantity. |
| 5425 |
$orderset = array(); |
| 5426 |
foreach($orderspec as $entry) $orderset[] = join(':', $entry); |
| 5427 |
|
| 5428 |
// Then get open orders |
| 5429 |
$sessionorders = array(); |
| 5430 |
$sessionorderdata = WC()->session->get('_vipps_session_orders'); |
| 5431 |
if ($sessionorderdata) { |
| 5432 |
foreach(array_keys($sessionorderdata) as $oid) { |
| 5433 |
$orderobject = wc_get_order($oid); |
| 5434 |
// Check to see that this hasn't been deleted yet IOK 2020-01-07 |
| 5435 |
if ($orderobject instanceof WC_Order) { |
| 5436 |
$sessionorders[] = $orderobject; |
| 5437 |
} |
| 5438 |
} |
| 5439 |
} |
| 5440 |
// Nothing more to do here |
| 5441 |
if (empty($sessionorders)) return true; |
| 5442 |
|
| 5443 |
// And create a similar hash table for each of the open orders |
| 5444 |
$openorderdata = array(); |
| 5445 |
foreach ($sessionorders as $open_order) { |
| 5446 |
$status = $open_order->get_status(); |
| 5447 |
if ($status == 'cancelled' || $status == 'pending') continue; |
| 5448 |
$when = strtotime($open_order->get_date_modified()); |
| 5449 |
$cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60)); |
| 5450 |
if (time() > $cutoff) { |
| 5451 |
continue; |
| 5452 |
} |
| 5453 |
$orderdata = array(); |
| 5454 |
foreach($open_order->get_items() as $item) { |
| 5455 |
$productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity(); |
| 5456 |
$orderdata[] = $productspec; |
| 5457 |
} |
| 5458 |
$openorderdata[]=$orderdata; |
| 5459 |
} |
| 5460 |
|
| 5461 |
// Now: For each entry in the orderhash, check if there is an order that has a) all of them and b) not any more of them. |
| 5462 |
foreach($openorderdata as $prevorder) { |
| 5463 |
$a = array_diff($prevorder, $orderset); |
| 5464 |
$b = array_diff($orderset, $prevorder); |
| 5465 |
if (empty($a) && empty($b)) { |
| 5466 |
$this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info'); |
| 5467 |
return false; |
| 5468 |
} |
| 5469 |
} |
| 5470 |
// Else, order is good. |
| 5471 |
return true; |
| 5472 |
} |
| 5473 |
|
| 5474 |
// Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value. |
| 5475 |
// Return value is like in a cart. |
| 5476 |
// Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22 |
| 5477 |
protected function get_orderspec_from_arguments ($productinfo) { |
| 5478 |
if (!$productinfo) return array(); |
| 5479 |
$variantid = 0; |
| 5480 |
$productid = 0; |
| 5481 |
$quantity = intval(@$productinfo['quantity']); |
| 5482 |
if (!$quantity) $quantity = 1; |
| 5483 |
if (isset($productinfo['sku']) && $productinfo['sku']) { |
| 5484 |
$sku = $productinfo['sku']; |
| 5485 |
$skuid = wc_get_product_id_by_sku($sku); |
| 5486 |
$product = wc_get_product($skuid); |
| 5487 |
$parentid = $product ? $product->get_parent_id() : null; |
| 5488 |
if ($product) { |
| 5489 |
if ($parentid) { |
| 5490 |
$variantid = $skuid; $productid = $parentid; |
| 5491 |
} else { |
| 5492 |
$productid = $skuid; |
| 5493 |
} |
| 5494 |
} |
| 5495 |
} else if (isset($productinfo['product_id']) && $productinfo['product_id']) { |
| 5496 |
$productid = intval($productinfo['product_id']); |
| 5497 |
$variantid = intval(@$productinfo['variation_id']); |
| 5498 |
} |
| 5499 |
if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity)); |
| 5500 |
return array(); |
| 5501 |
} |
| 5502 |
// If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24 |
| 5503 |
protected function get_orderspec_from_cart () { |
| 5504 |
$cartitems = WC()->cart->get_cart(); |
| 5505 |
$orderspec = array(); |
| 5506 |
foreach($cartitems as $item => $values) { |
| 5507 |
$orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']); |
| 5508 |
} |
| 5509 |
return $orderspec; |
| 5510 |
} |
| 5511 |
|
| 5512 |
// Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28 |
| 5513 |
// Returns the html. LP 2026-08-27 |
| 5514 |
protected function express_checkout_page_html($execute,$action,$productinfo=null) { |
| 5515 |
$gw = $this->gateway(); |
| 5516 |
|
| 5517 |
$expressCheckoutMessages = array(); |
| 5518 |
$expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ); |
| 5519 |
$expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()); |
| 5520 |
$expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name()); |
| 5521 |
|
| 5522 |
wp_register_script('vipps-express-checkout',plugins_url('js/express-checkout.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/express-checkout.js"), 'true'); |
| 5523 |
wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages); |
| 5524 |
wp_enqueue_script('vipps-express-checkout'); |
| 5525 |
// If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button |
| 5526 |
// to actually perform the express checkout. |
| 5527 |
$buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button()); |
| 5528 |
|
| 5529 |
|
| 5530 |
|
| 5531 |
$orderspec = $this->get_orderspec_from_arguments($productinfo); |
| 5532 |
if (empty($orderspec)) { |
| 5533 |
$orderspec = $this->get_orderspec_from_cart(); |
| 5534 |
} |
| 5535 |
$orderisOK = $this->validate_express_checkout_orderspec($orderspec); |
| 5536 |
$orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec); |
| 5537 |
|
| 5538 |
$askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true; |
| 5539 |
$askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes'); |
| 5540 |
$askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms); |
| 5541 |
|
| 5542 |
$askForConfirmationHTML = ''; |
| 5543 |
if (!$orderisOK) { |
| 5544 |
$header = __("Are you sure?",'woo-vipps'); |
| 5545 |
$body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps'); |
| 5546 |
$askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>"); |
| 5547 |
} |
| 5548 |
// Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20 |
| 5549 |
$execute = $execute && $orderisOK && !$askForTerms; |
| 5550 |
$execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo); |
| 5551 |
|
| 5552 |
$content = $this->spinner(); |
| 5553 |
|
| 5554 |
// We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24 |
| 5555 |
// The form data below is sent on order creation; the sec is also used to poll session status |
| 5556 |
$classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout"); |
| 5557 |
$content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>"; |
| 5558 |
$content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>"; |
| 5559 |
if ($this->gateway()->get_option('vippsorderattribution') == 'yes') { |
| 5560 |
// This is for the new order attribution feature of woo. IOK 2024-01-09 |
| 5561 |
$content .= '<input type="hidden" id="vippsorderattribution" value="1" />'; |
| 5562 |
ob_start(); |
| 5563 |
do_action( 'woocommerce_after_order_notes'); |
| 5564 |
$content .= ob_get_clean(); |
| 5565 |
} |
| 5566 |
$content .= wp_nonce_field('do_express','sec',1,false); |
| 5567 |
|
| 5568 |
$termsHTML = ''; |
| 5569 |
if ($askForTerms) { |
| 5570 |
// Include shop terms |
| 5571 |
ob_start(); |
| 5572 |
wc_get_template('checkout/terms.php'); |
| 5573 |
$termsHTML = ob_get_clean(); |
| 5574 |
$termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML); |
| 5575 |
} |
| 5576 |
$termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML); |
| 5577 |
|
| 5578 |
if ($productinfo) { |
| 5579 |
foreach($productinfo as $key=>$value) { |
| 5580 |
$k = esc_attr($key); |
| 5581 |
$v = esc_attr($value); |
| 5582 |
$content .= "<input type='hidden' name='$k' value='$v' />"; |
| 5583 |
} |
| 5584 |
} |
| 5585 |
ob_start(); |
| 5586 |
$content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo); |
| 5587 |
$content .= ob_get_clean(); |
| 5588 |
$content .= "</form>"; |
| 5589 |
|
| 5590 |
$extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML); |
| 5591 |
$pressTheButtonHTML = ""; |
| 5592 |
if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) { |
| 5593 |
$pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>"; |
| 5594 |
} |
| 5595 |
|
| 5596 |
if ($execute) { |
| 5597 |
$content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>"; |
| 5598 |
$content .= "<div id='vipps-status-message'></div>"; |
| 5599 |
return $this->special_page_html('', $content); |
| 5600 |
} else { |
| 5601 |
$content .= $askForConfirmationHTML; |
| 5602 |
$content .= $extraHTML; |
| 5603 |
$content .= $termsHTML; |
| 5604 |
$content .= apply_filters('woo_vipps_express_checkout_validation_elements', ''); |
| 5605 |
$title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name()); |
| 5606 |
$content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>"; |
| 5607 |
$content .= "<div id='vipps-status-message'></div>"; |
| 5608 |
return $this->special_page_html('', $content); |
| 5609 |
} |
| 5610 |
} |
| 5611 |
|
| 5612 |
|
| 5613 |
|
| 5614 |
public function vipps_wait_for_payment() { |
| 5615 |
$orderid = WC()->session->get('_vipps_pending_order'); |
| 5616 |
|
| 5617 |
$order = null; |
| 5618 |
$gw = $this->gateway(); |
| 5619 |
|
| 5620 |
// Failsafe for when the session disappears IOK 2018-11-19 |
| 5621 |
$no_session = $orderid ? false : true; |
| 5622 |
$limited_session = sanitize_text_field(@$_GET['ls']); |
| 5623 |
|
| 5624 |
// Now we *should* have a session at this point, but the session may have been deleted, |
| 5625 |
// or the session may be in another browser, because we get here by the Vipps app opening the app. |
| 5626 |
// If so, we will read the order id from the GET arguments and check if the auth token is correct, |
| 5627 |
// simulating the session with that. |
| 5628 |
// IOK 2019-11-19, changed to using GET 2023-01-23 |
| 5629 |
if ($no_session && $limited_session) { |
| 5630 |
$orderid = intval(@$_GET['id']); |
| 5631 |
} |
| 5632 |
if ($orderid) { |
| 5633 |
clean_post_cache($orderid); |
| 5634 |
$order = wc_get_order($orderid); |
| 5635 |
} |
| 5636 |
|
| 5637 |
// if we came here with no session, check to see if we are allowed to do stuff with the order. |
| 5638 |
if ($order && $no_session) { |
| 5639 |
if (!$order->get_meta('_vipps_limited_session') || (!wp_check_password($limited_session, $order->get_meta('_vipps_limited_session')))) { |
| 5640 |
$this->log("Wrong order session id on Vipps payment return url", 'error'); |
| 5641 |
$order = null; $orderid=0; |
| 5642 |
} else { |
| 5643 |
$session = WC()->session; |
| 5644 |
if (!$session->has_session()) { |
| 5645 |
$session->set_customer_session_cookie(true); |
| 5646 |
} |
| 5647 |
$session->set('_vipps_pending_order', $orderid); |
| 5648 |
} |
| 5649 |
} |
| 5650 |
|
| 5651 |
do_action('woo_vipps_wait_for_payment_page',$order); |
| 5652 |
|
| 5653 |
$deleted_order=0; |
| 5654 |
if ($orderid && !$order) { |
| 5655 |
// If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled. |
| 5656 |
// Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26 |
| 5657 |
$this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug'); |
| 5658 |
$deleted_order=1; |
| 5659 |
} |
| 5660 |
|
| 5661 |
if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps')); |
| 5662 |
|
| 5663 |
// If we are done, we are done, so go directly to the end. IOK 2018-05-16 |
| 5664 |
$status = $deleted_order ? 'cancelled' : $order->get_status(); |
| 5665 |
|
| 5666 |
// This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04 |
| 5667 |
$do_poll = true; |
| 5668 |
|
| 5669 |
// Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16 |
| 5670 |
if ($do_poll && $status == 'pending') { |
| 5671 |
// Just in case the callback hasn't come yet, do a quick check of the order status at Vipps. |
| 5672 |
$newstatus = $gw->callback_check_order_status($order); |
| 5673 |
if ($status != $newstatus) { |
| 5674 |
$status = $newstatus; |
| 5675 |
clean_post_cache($orderid); |
| 5676 |
$order = wc_get_order($orderid); // Reload order object |
| 5677 |
} |
| 5678 |
} else { |
| 5679 |
// No need to do anyting here. IOK 2020-01-26 |
| 5680 |
} |
| 5681 |
|
| 5682 |
$payment = 'notchecked'; |
| 5683 |
if ($do_poll) { |
| 5684 |
$payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order); |
| 5685 |
} |
| 5686 |
|
| 5687 |
// All these payment statuses are successes so go to the thankyou page. |
| 5688 |
if ($payment == 'authorized' || $payment == 'complete') { |
| 5689 |
// IOK 2023-07-17 this used to be called in the woocommerce_thankyou hook, now we do it here instead, since |
| 5690 |
// we may need to be logged in to be able to get to that hook. |
| 5691 |
$this->woocommerce_before_thankyou($order->get_id()); |
| 5692 |
wp_redirect($gw->get_return_url($order)); |
| 5693 |
exit(); |
| 5694 |
} |
| 5695 |
|
| 5696 |
// We are done, but in failure. Don't poll. |
| 5697 |
$content = ""; |
| 5698 |
$failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid); |
| 5699 |
|
| 5700 |
// Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status. |
| 5701 |
// For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17 |
| 5702 |
if ('failed' == $status) { |
| 5703 |
$failure_redirect = $failure_redirect ?: $gw->get_return_url($order); |
| 5704 |
wp_redirect($failure_redirect); |
| 5705 |
exit(); |
| 5706 |
} |
| 5707 |
if ($status == 'cancelled' || $payment == 'cancelled') { |
| 5708 |
$this->maybe_restore_cart($orderid,'failed'); |
| 5709 |
if ($failure_redirect){ |
| 5710 |
wp_redirect($failure_redirect); |
| 5711 |
exit(); |
| 5712 |
} |
| 5713 |
$content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>'; |
| 5714 |
$content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>'; |
| 5715 |
$content .= "</div>"; |
| 5716 |
return $this->special_page_html('', $content); |
| 5717 |
} |
| 5718 |
|
| 5719 |
// Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27 |
| 5720 |
|
| 5721 |
// Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16 |
| 5722 |
wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true'); |
| 5723 |
|
| 5724 |
$signal = $this->callbackSignal($order); |
| 5725 |
$content = ""; |
| 5726 |
$content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name()); |
| 5727 |
|
| 5728 |
if ($signal && !is_file($signal)) $signal = ''; |
| 5729 |
$signalurl = $this->callbackSignalURL($signal); |
| 5730 |
|
| 5731 |
$content .= "</p></div>"; |
| 5732 |
|
| 5733 |
// We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24 |
| 5734 |
$classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout"); |
| 5735 |
$content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>"; |
| 5736 |
$content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>"; |
| 5737 |
$content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>"; |
| 5738 |
$content .= "<input type='hidden' name='action' value='check_order_status'>"; |
| 5739 |
$content .= wp_nonce_field('vippsstatus','sec',1,false); |
| 5740 |
$content .= "</form>"; |
| 5741 |
|
| 5742 |
|
| 5743 |
$content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>'; |
| 5744 |
$content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>"; |
| 5745 |
$content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>'; |
| 5746 |
$content .= "</div>"; |
| 5747 |
|
| 5748 |
$content .= "<div id=success style='display:none'><p>". __('Order confirmed', 'woo-vipps') . '</p>'; |
| 5749 |
$content .= "<p><a class='btn button' id='continueToThankYou' href='" . $gw->get_return_url($order) . "'>".__('Continue','woo-vipps') ."</a></p>"; |
| 5750 |
$content .= '</div>'; |
| 5751 |
|
| 5752 |
$content .= "<div id=failure style='display:none'><p>". __('Order cancelled', 'woo-vipps') . '</p>'; |
| 5753 |
$content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>'; |
| 5754 |
$content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>"; |
| 5755 |
$content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>"; |
| 5756 |
$content .= "</div>"; |
| 5757 |
|
| 5758 |
return $this->special_page_html('', $content); |
| 5759 |
} |
| 5760 |
|
| 5761 |
// Returns formatted html for the vipps special page. LP 2026-08-27 |
| 5762 |
public function special_page_html($header, $content) { |
| 5763 |
$header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : ''; |
| 5764 |
$html = <<<EOF |
| 5765 |
$header_html |
| 5766 |
<div class="vipps-special-page-content">$content</div> |
| 5767 |
EOF; |
| 5768 |
return apply_filters('woo_vipps_special_page_html', $html, $header, $content); |
| 5769 |
} |
| 5770 |
|
| 5771 |
|
| 5772 |
// Support the interactivity API with data about our cart IOK 2026-02-23 |
| 5773 |
public function woo_vipps_store_api_cart_data() { |
| 5774 |
// Reverting the condition with the directive data-wp-bind--hidden does not work, so we need the flipped bool here (hide instead of show). LP 2026-02-10 |
| 5775 |
|
| 5776 |
$checkout_page = $this->gateway()->vipps_checkout_available(); |
| 5777 |
$standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id')); |
| 5778 |
$checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout; |
| 5779 |
|
| 5780 |
$cart_data = array( |
| 5781 |
'cart_hide_express' => !$this->gateway()->show_express_checkout(), |
| 5782 |
'cart_supports_checkout' => (bool) $checkout_page, |
| 5783 |
'checkout_url' => $checkout_url, |
| 5784 |
); |
| 5785 |
|
| 5786 |
return $cart_data; |
| 5787 |
} |
| 5788 |
|
| 5789 |
public function woo_vipps_store_api_cart_schema() { |
| 5790 |
return array( |
| 5791 |
'cart_hide_express' => array( |
| 5792 |
'description' => sprintf(__( 'Whether to hide the %1$s Express Checkout in the cart', 'woo-vipps' ), $this->get_payment_method_name()), |
| 5793 |
'type' => array( 'boolean', 'null' ), |
| 5794 |
'readonly' => true, |
| 5795 |
), |
| 5796 |
'cart_supports_checkout' => array( |
| 5797 |
'description' => sprintf(__( 'True if %1$s is active and the cart supports it', 'woo-vipps' ), $this->CheckoutName()), |
| 5798 |
'type' => array( 'boolean', 'null' ), |
| 5799 |
'readonly' => true, |
| 5800 |
), |
| 5801 |
'checkout_url' => array( |
| 5802 |
'description' => sprintf(__( 'Current checkout url based on cart state', 'woo-vipps' ), $this->get_payment_method_name()), |
| 5803 |
'type' => array( 'string', 'null' ), |
| 5804 |
'readonly' => true, |
| 5805 |
), |
| 5806 |
); |
| 5807 |
} |
| 5808 |
|
| 5809 |
// Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26 |
| 5810 |
// new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15 |
| 5811 |
private function init_button_options() { |
| 5812 |
/* New structure as of now |
| 5813 |
* [ |
| 5814 |
* 'version' => x.x, |
| 5815 |
* 'express' => [ |
| 5816 |
* 'version' => x.x, used to migrate from previous iterations |
| 5817 |
* 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override |
| 5818 |
* 'global' => ['compact' => ..., 'verb' => ..., ...], |
| 5819 |
* 'cart' => [...], |
| 5820 |
* 'product' => [...], |
| 5821 |
* 'checkout' => [...], |
| 5822 |
* ... |
| 5823 |
* ], |
| 5824 |
* 'product_configs' => [ overrides for specific products |
| 5825 |
* 1532 => ['compact' => ..., 'verb' => ..., ...], |
| 5826 |
* ... |
| 5827 |
* ], |
| 5828 |
* ], |
| 5829 |
* ] |
| 5830 |
*/ |
| 5831 |
$options = get_option('vipps_button_options2'); |
| 5832 |
if (!empty($options)) return; |
| 5833 |
|
| 5834 |
$old_options = get_option('vipps_button_options'); |
| 5835 |
$default_config = $this->get_html_button_default_attrs(); |
| 5836 |
unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01 |
| 5837 |
$default_compact = array_replace($default_config, ['compact' => 'true']); |
| 5838 |
|
| 5839 |
$default_options = [ |
| 5840 |
'version' => $this->button_options_version, |
| 5841 |
'express' => [ |
| 5842 |
'version' => $this->button_options_express_version, |
| 5843 |
'configs' => [ |
| 5844 |
'global' => $default_config, |
| 5845 |
// Need compact version by default for below pages. LP 2026-07-07 |
| 5846 |
'catalog' => $default_compact, |
| 5847 |
'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07 |
| 5848 |
], |
| 5849 |
'product_configs' => [], |
| 5850 |
], |
| 5851 |
]; |
| 5852 |
|
| 5853 |
$new_options = $default_options; |
| 5854 |
|
| 5855 |
//Actually, we have some options from the old structure IOK 2026-07-15 |
| 5856 |
if (!empty($old_options)) { |
| 5857 |
$new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? ''); |
| 5858 |
unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01 |
| 5859 |
} |
| 5860 |
|
| 5861 |
// Migrate context/page mini override to new context config. LP 2026-06-26 |
| 5862 |
if (is_array($old_options['express']['force-mini'] ?? null)) { |
| 5863 |
foreach($old_options['express']['force-mini'] as $context => $use_mini) { |
| 5864 |
if ("yes" === $use_mini) { |
| 5865 |
$config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? ''); |
| 5866 |
unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01 |
| 5867 |
$config['compact'] = 'true'; |
| 5868 |
$new_options['express']['configs'][$context] = $config; |
| 5869 |
} |
| 5870 |
} |
| 5871 |
} |
| 5872 |
|
| 5873 |
if ($this->get_payment_method_name() !== 'MobilePay') { |
| 5874 |
// Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01 |
| 5875 |
foreach(($new_options['express']['configs'] ?? []) as $context => $config) { |
| 5876 |
if ('fi' === ($config['language'] ?? '')) { |
| 5877 |
$config['language'] = 'store'; |
| 5878 |
$new_options['express']['configs'][$context] = $config; |
| 5879 |
} |
| 5880 |
} |
| 5881 |
} |
| 5882 |
|
| 5883 |
/* translators: placeholders are arrays */ |
| 5884 |
$this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug'); |
| 5885 |
|
| 5886 |
|
| 5887 |
|
| 5888 |
update_option('vipps_button_options2', $new_options); |
| 5889 |
} |
| 5890 |
|
| 5891 |
// Old variant string => new config array. LP 2026-06-26 |
| 5892 |
public function migrate_button_variant_to_config($variant_slug) { |
| 5893 |
if (!is_string($variant_slug)) return []; |
| 5894 |
$config = $this->get_html_button_default_attrs(); |
| 5895 |
$config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false'; |
| 5896 |
$config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false'; |
| 5897 |
if (str_contains($variant_slug, 'buy-now')) { |
| 5898 |
$config['verb'] = 'buy'; |
| 5899 |
} else if (str_contains($variant_slug, 'express')) { |
| 5900 |
$config['verb'] = 'express'; |
| 5901 |
} |
| 5902 |
return $config; |
| 5903 |
} |
| 5904 |
|
| 5905 |
// Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01 |
| 5906 |
public function get_express_logo_variants() { |
| 5907 |
return [ |
| 5908 |
'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'), |
| 5909 |
'buy-now-pill' => __('Buy now pill', 'woo-vipps'), |
| 5910 |
'express-rectangular' => __('Express rectangular', 'woo-vipps'), |
| 5911 |
'express-pill' => __('Express pill', 'woo-vipps'), |
| 5912 |
'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'), |
| 5913 |
'express-pill-mini' => __('Express pill mini', 'woo-vipps'), |
| 5914 |
]; |
| 5915 |
} |
| 5916 |
|
| 5917 |
|
| 5918 |
// Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18 |
| 5919 |
public static function order_is_vipps_retryable($order_id) { |
| 5920 |
$order = wc_get_order($order_id); |
| 5921 |
if (!$order) return false; |
| 5922 |
$api = $order->get_meta('_vipps_api'); |
| 5923 |
$nonexpress_epayment = 'epayment' === $api && !$order->get_meta('_vipps_express_checkout'); |
| 5924 |
$shipping_set = $order->get_meta('_vipps_shipping_set'); |
| 5925 |
|
| 5926 |
// Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18 |
| 5927 |
return $nonexpress_epayment || $shipping_set; |
| 5928 |
} |
| 5929 |
|
| 5930 |
/** Returns the plugin's rest api namespace including the version. |
| 5931 |
* Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */ |
| 5932 |
public static function get_rest_namespace($version = 'latest') { |
| 5933 |
$version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version; |
| 5934 |
return self::REST_NAMESPACE_BASE . "/$version"; |
| 5935 |
} |
| 5936 |
|
| 5937 |
/** Returns the plugin's rest api url. |
| 5938 |
* $version accepts 'latest', but you probably don't want to do that. |
| 5939 |
* Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */ |
| 5940 |
public static function get_rest_url($version, $route) { |
| 5941 |
return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest'); |
| 5942 |
} |
| 5943 |
} |
| 5944 |
|