Files
shuffle_and_skirmish_website/wp-content/plugins/woocommerce-payments/includes/multi-currency/MultiCurrency.php
2025-11-24 21:33:55 +00:00

1738 lines
57 KiB
PHP

<?php
/**
* Class MultiCurrency
*
* @package WooCommerce\Payments\MultiCurrency
*/
namespace WCPay\MultiCurrency;
use WCPay\MultiCurrency\Exceptions\InvalidCurrencyException;
use WCPay\MultiCurrency\Exceptions\InvalidCurrencyRateException;
use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface;
use WCPay\MultiCurrency\Interfaces\MultiCurrencyApiClientInterface;
use WCPay\MultiCurrency\Interfaces\MultiCurrencyCacheInterface;
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface;
use WCPay\MultiCurrency\Logger;
use WCPay\MultiCurrency\Notes\NoteMultiCurrencyAvailable;
use WCPay\MultiCurrency\Utils;
use WC_Payments_Features;
defined( 'ABSPATH' ) || exit;
/**
* Class that controls Multi-Currency functionality.
*/
class MultiCurrency {
const CURRENCY_SESSION_KEY = 'wcpay_currency';
const CURRENCY_META_KEY = 'wcpay_currency';
const FILTER_PREFIX = 'wcpay_multi_currency_';
const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_stored_customer_currencies';
/**
* The plugin's ID.
*
* @var string
*/
public $id = 'wcpay_multi_currency';
/**
* Static flag to show if the currencies initialization has been completed
*
* @var bool
*/
protected static $is_initialized = false;
/**
* Compatibility instance.
*
* @var Compatibility
*/
protected $compatibility;
/**
* Geolocation instance.
*
* @var Geolocation
*/
protected $geolocation;
/**
* The Currency Switcher Widget instance.
*
* @var null|CurrencySwitcherWidget
*/
protected $currency_switcher_widget;
/**
* Gutenberg Block implementation of the Currency Switcher Widget instance.
*
* @var CurrencySwitcherBlock
*/
protected $currency_switcher_block;
/**
* Utils instance.
*
* @var Utils
*/
protected $utils;
/**
* FrontendPrices instance.
*
* @var FrontendPrices
*/
protected $frontend_prices;
/**
* FrontendCurrencies instance.
*
* @var FrontendCurrencies
*/
protected $frontend_currencies;
/**
* StorefrontIntegration instance.
*
* @var StorefrontIntegration
*/
protected $storefront_integration;
/**
* The available currencies.
*
* @var Currency[]|null
*/
protected $available_currencies;
/**
* The default currency.
*
* @var Currency|null
*/
protected $default_currency;
/**
* The enabled currencies.
*
* @var Currency[]|null
*/
protected $enabled_currencies;
/**
* Instance of MultiCurrencySettingsInterface.
*
* @var MultiCurrencySettingsInterface
*/
private $settings_service;
/**
* Client for making requests to the API
*
* @var MultiCurrencyApiClientInterface
*/
private $payments_api_client;
/**
* Instance of MultiCurrencyAccountInterface.
*
* @var MultiCurrencyAccountInterface
*/
private $payments_account;
/**
* Instance of MultiCurrencyLocalizationInterface.
*
* @var MultiCurrencyLocalizationInterface
*/
private $localization_service;
/**
* Instance of MultiCurrencyCacheInterface.
*
* @var MultiCurrencyCacheInterface
*/
private $cache;
/**
* Tracking instance.
*
* @var Tracking
*/
protected $tracking;
/**
* Simulation variables array.
*
* @var array
*/
protected $simulation_params = [];
/**
* Class constructor.
*
* @param MultiCurrencySettingsInterface $settings_service Settings service.
* @param MultiCurrencyApiClientInterface $payments_api_client Payments API client.
* @param MultiCurrencyAccountInterface $payments_account Payments Account instance.
* @param MultiCurrencyLocalizationInterface $localization_service Localization Service instance.
* @param MultiCurrencyCacheInterface $cache Cache instance.
* @param Utils|null $utils Optional Utils instance.
*/
public function __construct( MultiCurrencySettingsInterface $settings_service, MultiCurrencyApiClientInterface $payments_api_client, MultiCurrencyAccountInterface $payments_account, MultiCurrencyLocalizationInterface $localization_service, MultiCurrencyCacheInterface $cache, ?Utils $utils = null ) {
$this->settings_service = $settings_service;
$this->payments_api_client = $payments_api_client;
$this->payments_account = $payments_account;
$this->localization_service = $localization_service;
$this->cache = $cache;
// If a Utils instance is not passed as argument, initialize it. This allows to mock it in tests.
$this->utils = $utils ?? new Utils();
$this->geolocation = new Geolocation( $this->localization_service );
$this->compatibility = new Compatibility( $this, $this->utils );
$this->currency_switcher_block = new CurrencySwitcherBlock( $this, $this->compatibility );
}
/**
* Backwards compatibility for the old `instance()` static method.
*
* We need to use this as some plugins still call `MultiCurrency::instance()` directly.
*
* @return null|MultiCurrency - Main instance.
*/
public static function instance() {
if ( function_exists( 'WC_Payments_Multi_Currency' ) ) {
return WC_Payments_Multi_Currency();
}
}
/**
* Initializes this class' WP hooks.
*
* @return void
*/
public function init_hooks() {
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
add_filter( 'woocommerce_get_settings_pages', [ $this, 'init_settings_pages' ] );
// Enqueue the scripts after the main WC_Payments_Admin does.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ], 20 );
}
add_action( 'init', [ $this, 'init' ] );
add_action( 'rest_api_init', [ $this, 'init_rest_api' ] );
add_action( 'widgets_init', [ $this, 'init_widgets' ] );
$is_frontend_request = ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request();
if ( $is_frontend_request || Utils::is_store_api_request() ) {
// Make sure that this runs after the main init function.
add_action( 'init', [ $this, 'update_selected_currency_by_url' ], 11 );
add_action( 'init', [ $this, 'update_selected_currency_by_geolocation' ], 12 );
add_action( 'init', [ $this, 'possible_simulation_activation' ], 13 );
add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] );
}
if ( ! Utils::is_store_batch_request() && ! Utils::is_store_api_request() && WC()->is_rest_api_request() ) {
if ( isset( $_GET['currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$get_currency_from_query_param = function () {
$currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
return strtoupper( $currency );
};
add_filter( self::FILTER_PREFIX . 'override_selected_currency', $get_currency_from_query_param );
} else {
// If the request is a REST API request, ensure we default to the store currency and leave price as-is.
add_filter( self::FILTER_PREFIX . 'should_return_store_currency', '__return_true' );
add_filter( self::FILTER_PREFIX . 'should_convert_product_price', '__return_false' );
$get_default_currency_code = function () {
return $this->get_default_currency()->get_code();
};
add_filter( self::FILTER_PREFIX . 'override_selected_currency', $get_default_currency_code );
}
}
add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_props_to_wcpay_js_config' ] );
$this->currency_switcher_block->init_hooks();
}
/**
* Called after the WooCommerce session has been initialized. Initialises the available currencies,
* default currency and enabled currencies for the Multi-Currency plugin.
*
* @return void
*/
public function init() {
$store_currency_updated = $this->check_store_currency_for_change();
$this->initialize_available_currencies();
$this->set_default_currency();
$this->initialize_enabled_currencies();
// If the store currency has been updated, we need to update the notice that will display any manual currencies.
if ( $store_currency_updated ) {
$this->update_manual_rate_currencies_notice_option();
}
$admin_notices = new AdminNotices();
$user_settings = new UserSettings( $this );
new Analytics( $this, $this->settings_service );
$this->frontend_prices = new FrontendPrices( $this, $this->compatibility );
$this->frontend_currencies = new FrontendCurrencies( $this, $this->localization_service, $this->utils, $this->compatibility );
$this->tracking = new Tracking( $this );
// Init all the hooks.
$admin_notices->init_hooks();
$user_settings->init_hooks();
$this->frontend_prices->init_hooks();
$this->frontend_currencies->init_hooks();
$this->tracking->init_hooks();
add_action( 'woocommerce_order_refunded', [ $this, 'add_order_meta_on_refund' ], 50, 2 );
// Check to make sure there are enabled currencies, then for Storefront being active, and then load the integration.
$theme = wp_get_theme();
if ( 'storefront' === $theme->get_stylesheet() || 'storefront' === $theme->get_template() ) {
$this->storefront_integration = new StorefrontIntegration( $this );
}
if ( is_admin() ) {
add_action( 'admin_init', [ $this, 'add_woo_admin_notes' ] );
}
// Update the customer currencies option after an order status change.
add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_update_customer_currencies_option' ] );
static::$is_initialized = true;
}
/**
* Initialize the REST API controller.
*
* @return void
*/
public function init_rest_api() {
// Ensures we are not initializing our REST during `rest_preload_api_request`.
// When constructors signature changes, in manual update scenarios we were run into fatals.
// Those fatals are not critical, but it causes hickups in release process as catches unnecessary attention.
if ( function_exists( 'get_current_screen' ) && get_current_screen() ) {
return;
}
$api_controller = new RestController( $this );
$api_controller->register_routes();
}
/**
* Initialize the legacy widgets.
*
* @return void
*/
public function init_widgets() {
// Register the legacy widget.
$this->currency_switcher_widget = new CurrencySwitcherWidget( $this, $this->compatibility );
register_widget( $this->currency_switcher_widget );
}
/**
* Initialize the Settings Pages.
*
* @param array $settings_pages The settings pages.
*
* @return array The new settings pages.
*/
public function init_settings_pages( $settings_pages ): array {
// We don't need to check if the payment provider is connected for the
// Settings page generation on the incoming CLI and async job calls.
if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) {
return $settings_pages;
}
// Due to autoloader limitations, we shouldn't initiate MCCY settings if the plugin was just upgraded:
// https://github.com/Automattic/woocommerce-payments/issues/9676.
if ( did_action( 'upgrader_process_complete' ) ) {
return $settings_pages;
}
if ( $this->payments_account->is_provider_connected() ) {
$settings = new Settings( $this );
$settings->init_hooks();
$settings_pages[] = $settings;
} else {
$settings_onboard_cta = new SettingsOnboardCta( $this, $this->payments_account );
$settings_onboard_cta->init_hooks();
$settings_pages[] = $settings_onboard_cta;
}
return $settings_pages;
}
/**
* Load the admin assets.
*
* @return void
*/
public function enqueue_admin_scripts() {
global $current_tab;
// Enqueue the settings JS and CSS only on the WCPay multi-currency settings page.
if ( 'wcpay_multi_currency' !== $current_tab ) {
return;
}
$this->register_admin_scripts();
wp_enqueue_script( 'WCPAY_MULTI_CURRENCY_SETTINGS' );
wp_enqueue_style( 'WCPAY_MULTI_CURRENCY_SETTINGS' );
}
/**
* Add multi-currency specific props to the WCPay JS config.
*
* @param array $config The JS config that will be loaded on the frontend.
*
* @return array The updated JS config.
*/
public function add_props_to_wcpay_js_config( $config ) {
$config['isMultiCurrencyEnabled'] = true;
return $config;
}
/**
* Gets and caches the data for the currency rates from the server.
* Will be returned as an array with two keys:
* - 'currencies' (the currencies)
* - 'updated' (when this data was fetched from the API).
*
* @return ?array
*/
public function get_cached_currencies() {
// If connection to server cannot be established, payment provider is not connected, or the account is rejected,
// return any data we have cached (expired or not) or null.
if ( ! $this->payments_api_client->is_server_connected() || ! $this->payments_account->is_provider_connected() || $this->payments_account->is_account_rejected() ) {
$cached_data = $this->cache->get( MultiCurrencyCacheInterface::CURRENCIES_KEY, true );
return $cached_data ?? null;
}
return $this->cache->get_or_add(
MultiCurrencyCacheInterface::CURRENCIES_KEY,
function () {
try {
$currency_data = $this->payments_api_client->get_currency_rates( strtolower( get_woocommerce_currency() ) );
return [
'currencies' => $currency_data,
'updated' => time(),
];
} catch ( \Exception $e ) {
return null;
}
},
function ( $data ) {
return is_array( $data ) && isset( $data['currencies'], $data['updated'] );
}
);
}
/**
* Returns the Compatibility instance.
*
* @return Compatibility
*/
public function get_compatibility() {
return $this->compatibility;
}
/**
* Returns the Currency Switcher Widget instance.
*
* @return CurrencySwitcherWidget|null
*/
public function get_currency_switcher_widget() {
return $this->currency_switcher_widget;
}
/**
* Returns the FrontendPrices instance.
*
* @return FrontendPrices
*/
public function get_frontend_prices(): FrontendPrices {
return $this->frontend_prices;
}
/**
* Returns the FrontendCurrencies instance.
*
* @return FrontendCurrencies
*/
public function get_frontend_currencies(): FrontendCurrencies {
return $this->frontend_currencies;
}
/**
* Returns the StorefrontIntegration instance.
*
* @return StorefrontIntegration|null
*/
public function get_storefront_integration() {
return $this->storefront_integration;
}
/**
* Generates the switcher widget markup.
*
* @param array $instance The widget's instance settings.
* @param array $args The widget's arguments.
*
* @return string The widget markup.
*/
public function get_switcher_widget_markup( array $instance = [], array $args = [] ): string {
/**
* The spl_object_hash function is used here due to we register the widget with an instance of the widget and
* not the class name of the widget. WordPress core takes the instance and passes it through spl_object_hash
* to get a hash and adds that as the widget's name in the $wp_widget_factory->widgets[] array. In order to
* call the_widget, you need to have the name of the widget, so we get the instance and hash to use.
*/
ob_start();
$currency_switcher_widget = $this->get_currency_switcher_widget();
if ( ! is_object( $currency_switcher_widget ) ) {
Logger::notice(
sprintf(
'Invalid widget markup. Widget instance must be type object, %s given.',
gettype( $currency_switcher_widget )
)
);
return ob_get_clean();
}
the_widget(
spl_object_hash( $currency_switcher_widget ),
apply_filters( self::FILTER_PREFIX . 'theme_widget_instance', $instance ),
apply_filters( self::FILTER_PREFIX . 'theme_widget_args', $args )
);
return ob_get_clean();
}
/**
* Returns the store's current available, enabled, and default currencies.
*
* @return array
*/
public function get_store_currencies(): array {
return [
'available' => $this->get_available_currencies(),
'enabled' => $this->get_enabled_currencies(),
'default' => $this->get_default_currency(),
];
}
/**
* Gets the currency settings for a single currency.
*
* @param string $currency_code The currency code to get settings for.
*
* @return array The currency's settings.
*
* @throws InvalidCurrencyException
*/
public function get_single_currency_settings( string $currency_code ): array {
// Confirm the currency code is valid before trying to get the settings.
if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) {
$this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code );
}
$currency_code = strtolower( $currency_code );
return [
'exchange_rate_type' => get_option( 'wcpay_multi_currency_exchange_rate_' . $currency_code, 'automatic' ),
'manual_rate' => get_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, null ),
'price_rounding' => get_option( 'wcpay_multi_currency_price_rounding_' . $currency_code, null ),
'price_charm' => get_option( 'wcpay_multi_currency_price_charm_' . $currency_code, null ),
];
}
/**
* Updates the currency settings for a single currency.
*
* @param string $currency_code The single currency code to be updated.
* @param string $exchange_rate_type The exchange rate type setting.
* @param float $price_rounding The price rounding setting.
* @param float $price_charm The price charm setting.
* @param ?float $manual_rate The manual rate setting, or null.
*
* @return void
*
* @throws InvalidCurrencyException
* @throws InvalidCurrencyRateException
*/
public function update_single_currency_settings( string $currency_code, string $exchange_rate_type, float $price_rounding, float $price_charm, $manual_rate = null ) {
// Confirm the currency code is valid before trying to update the settings.
if ( ! array_key_exists( strtoupper( $currency_code ), $this->get_available_currencies() ) ) {
$this->log_and_throw_invalid_currency_exception( __FUNCTION__, $currency_code );
}
$currency_code = strtolower( $currency_code );
if ( 'manual' === $exchange_rate_type && ! is_null( $manual_rate ) ) {
if ( ! is_numeric( $manual_rate ) || 0 >= $manual_rate ) {
$message = 'Invalid manual currency rate passed to update_single_currency_settings: ' . $manual_rate;
Logger::error( $message );
throw new InvalidCurrencyRateException( esc_html( $message ), 500 );
}
update_option( 'wcpay_multi_currency_manual_rate_' . $currency_code, $manual_rate );
}
update_option( 'wcpay_multi_currency_price_rounding_' . $currency_code, $price_rounding );
update_option( 'wcpay_multi_currency_price_charm_' . $currency_code, $price_charm );
if ( in_array( $exchange_rate_type, [ 'automatic', 'manual' ], true ) ) {
update_option( 'wcpay_multi_currency_exchange_rate_' . $currency_code, esc_attr( $exchange_rate_type ) );
}
}
/**
* Updates the customer currencies option.
*
* @param int $order_id The order ID.
*
* @return void
*/
public function maybe_update_customer_currencies_option( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$currency = strtoupper( $order->get_currency() );
$currencies = self::get_all_customer_currencies();
// Skip if the currency is already in the list.
if ( in_array( $currency, $currencies, true ) ) {
return;
}
$currencies[] = $currency;
update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies );
}
/**
* Gets the currencies available. Initializes it if needed.
*
* @return Currency[] Array of Currency objects.
*/
public function get_available_currencies(): array {
if ( null === $this->available_currencies ) {
$this->init();
}
return $this->available_currencies ?? [];
}
/**
* Gets the store base currency. Initializes it if needed.
*
* @return Currency The store base currency.
*/
public function get_default_currency(): Currency {
if ( null === $this->default_currency ) {
$this->init();
}
return $this->default_currency ?? new Currency( $this->localization_service, get_woocommerce_currency() );
}
/**
* Gets the currently enabled currencies. Initializes it if needed.
*
* @return Currency[] Array of Currency objects.
*/
public function get_enabled_currencies(): array {
if ( null === $this->enabled_currencies ) {
$this->init();
}
return $this->enabled_currencies ?? [];
}
/**
* Sets the enabled currencies for the store.
*
* @param string[] $currencies Array of currency codes to be enabled.
*
* @return void
*
* @throws InvalidCurrencyException
*/
public function set_enabled_currencies( $currencies = [] ) {
// If curriencies is not an array, or if there are no currencies, just exit.
if ( ! is_array( $currencies ) || 0 === count( $currencies ) ) {
return;
}
// Confirm the currencies submitted are available/valid currencies.
$invalid_currencies = array_diff( $currencies, array_keys( $this->get_available_currencies() ) );
if ( 0 < count( $invalid_currencies ) ) {
$this->log_and_throw_invalid_currency_exception( __FUNCTION__, implode( ', ', $invalid_currencies ) );
}
// Get the currencies that were removed before they are updated.
$removed_currencies = array_diff( array_keys( $this->get_enabled_currencies() ), $currencies );
// Update the enabled currencies and reinitialize.
update_option( $this->id . '_enabled_currencies', $currencies );
$this->initialize_enabled_currencies();
Logger::debug(
'Enabled currencies updated: '
. var_export( $currencies, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
);
// Now remove the removed currencies settings.
if ( 0 < count( $removed_currencies ) ) {
$this->remove_currencies_settings( $removed_currencies );
}
}
/**
* Gets the user selected currency, or `$default_currency` if is not set.
*
* @return Currency
*/
public function get_selected_currency(): Currency {
$multi_currency_code = $this->compatibility->override_selected_currency();
$currency_code = $multi_currency_code ? $multi_currency_code : $this->get_stored_currency_code();
return $this->get_enabled_currencies()[ $currency_code ] ?? $this->get_default_currency();
}
/**
* Update the selected currency from a currency code.
*
* @param string $currency_code Three letter currency code.
* @param bool $persist_change Set true to store the change in the session cookie if it doesn't exist yet.
*
* @return void
*/
public function update_selected_currency( string $currency_code, bool $persist_change = true ) {
$code = strtoupper( $currency_code );
$user_id = get_current_user_id();
$currency = $this->get_enabled_currencies()[ $code ] ?? null;
if ( null === $currency ) {
return;
}
// We discard the cache for the front-end.
$this->frontend_currencies->selected_currency_changed();
// initializing the session (useful for Store API),
// so that the selected currency (set as query string parameter) can be correctly set.
if ( ! isset( WC()->session ) ) {
WC()->initialize_session();
}
if ( $this->get_stored_currency_code() !== $code && $persist_change ) {
$this->frontend_currencies->clear_url_price_params();
}
if ( 0 === $user_id && WC()->session ) {
WC()->session->set( self::CURRENCY_SESSION_KEY, $currency->get_code() );
// Set the session cookie if is not yet to persist the selected currency.
if ( ! WC()->session->has_session() && ! headers_sent() && $persist_change ) {
$this->utils->set_customer_session_cookie( true );
}
} elseif ( $user_id ) {
update_user_meta( $user_id, self::CURRENCY_META_KEY, $currency->get_code() );
}
// Recalculate cart when currency changes.
if ( did_action( 'wp_loaded' ) ) {
$this->recalculate_cart();
} else {
add_action( 'wp_loaded', [ $this, 'recalculate_cart' ] );
}
}
/**
* Update the selected currency from url param `currency`.
*
* @return void
*/
public function update_selected_currency_by_url() {
if ( ! isset( $_GET['currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
$this->update_selected_currency( sanitize_text_field( wp_unslash( $_GET['currency'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
/**
* Update the selected currency from the user's geolocation country.
*
* @return void
*/
public function update_selected_currency_by_geolocation() {
// We only want to automatically set the currency if the option is enabled and it shouldn't be disabled for any reason.
if ( ! $this->is_using_auto_currency_switching() || $this->compatibility->should_disable_currency_switching() ) {
return;
}
// Display notice, prevent duplicates in simulation.
if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) {
add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] );
}
// Update currency only if it's already not set.
if ( $this->get_stored_currency_code() ) {
return;
}
$currency = $this->geolocation->get_currency_by_customer_location();
if ( empty( $this->get_enabled_currencies()[ $currency ] ) ) {
return;
}
$this->update_selected_currency( $currency, false );
}
/**
* Gets the configured value for apply charm pricing only to products.
*
* @return mixed The configured value.
*/
public function get_apply_charm_only_to_products() {
return apply_filters( self::FILTER_PREFIX . 'apply_charm_only_to_products', true );
}
/**
* Gets the converted price using the current currency with the rounding and charm pricing settings.
*
* @param mixed $price The price to be converted.
* @param string $type The type of price being converted. One of 'product', 'shipping', 'tax', 'coupon', or 'exchange_rate'.
*
* @return float The converted price.
*/
public function get_price( $price, string $type ): float {
$supported_types = [ 'product', 'shipping', 'tax', 'coupon', 'exchange_rate' ];
$currency = $this->get_selected_currency();
if ( ! in_array( $type, $supported_types, true ) || $currency->get_is_default() ) {
return (float) $price;
}
$converted_price = ( (float) $price ) * $currency->get_rate();
if ( 'tax' === $type || 'coupon' === $type || 'exchange_rate' === $type ) {
// We must make sure the price is rounded properly before returning it, otherwise we
// may end up with inconsistent prices in the cart.
$num_decimals = absint(
$this->localization_service->get_currency_format(
$currency->get_code()
)['num_decimals']
);
return round( $converted_price, $num_decimals );
}
$charm_compatible_types = [ 'product', 'shipping' ];
$apply_charm_pricing = $this->get_apply_charm_only_to_products()
? 'product' === $type
: in_array( $type, $charm_compatible_types, true );
return $this->get_adjusted_price( $converted_price, $apply_charm_pricing, $currency );
}
/**
* Gets a raw converted amount based on the amount and currency codes passed.
* This is a helper method for external conversions, if needed.
*
* @param float $amount The amount to be converted.
* @param string $to_currency The 3 letter currency code to convert the amount to.
* @param string $from_currency The 3 letter currency code to convert the amount from.
*
* @return float The converted amount.
*
* @throws InvalidCurrencyException
* @throws InvalidCurrencyRateException
*/
public function get_raw_conversion( float $amount, string $to_currency, string $from_currency = '' ): float {
$enabled_currencies = $this->get_enabled_currencies();
// If the from_currency is not set, use the store currency.
if ( '' === $from_currency ) {
$from_currency = $this->get_default_currency()->get_code();
}
// We throw an exception if either of the currencies are not enabled.
$to_currency = strtoupper( $to_currency );
$from_currency = strtoupper( $from_currency );
foreach ( [ $to_currency, $from_currency ] as $code ) {
if ( ! isset( $enabled_currencies[ $code ] ) ) {
$this->log_and_throw_invalid_currency_exception( __FUNCTION__, $code );
}
}
// Get the rates.
$to_currency_rate = $enabled_currencies[ $to_currency ]->get_rate();
$from_currency_rate = $enabled_currencies[ $from_currency ]->get_rate();
// Throw an exception in case from_currency_rate is less than or equal to 0.
if ( 0 >= $from_currency_rate ) {
$message = 'Invalid rate for from_currency in get_raw_conversion: ' . $from_currency_rate;
Logger::error( $message );
throw new InvalidCurrencyRateException( esc_html( $message ), 500 );
}
$amount = $amount * ( $to_currency_rate / $from_currency_rate );
return (float) $amount;
}
/**
* Recalculates WooCommerce cart totals.
*
* @return void
*/
public function recalculate_cart() {
if ( WC()->cart ) {
WC()->cart->calculate_totals();
}
}
/**
* When an order is refunded, a new psuedo order is created to represent the refund.
* We want to check if the original order was a multi-currency order, and if so, copy the meta data
* to the new order.
*
* @param int $order_id The order ID.
* @param int $refund_id The refund order ID.
*/
public function add_order_meta_on_refund( $order_id, $refund_id ) {
$default_currency = $this->get_default_currency();
$order = wc_get_order( $order_id );
$refund = wc_get_order( $refund_id );
// Do not add exchange rate if order was made in the store's default currency.
if ( ! $order || ! $refund || $default_currency->get_code() === $order->get_currency() ) {
return;
}
$order_exchange_rate = $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true );
$stripe_exchange_rate = $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true );
$order_default_currency = $order->get_meta( '_wcpay_multi_currency_order_default_currency', true );
$refund->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $order_exchange_rate );
$refund->update_meta_data( '_wcpay_multi_currency_order_default_currency', $order_default_currency );
if ( $stripe_exchange_rate ) {
$refund->update_meta_data( '_wcpay_multi_currency_stripe_exchange_rate', $stripe_exchange_rate );
}
$refund->save_meta_data();
}
/**
* Displays a notice on the frontend informing the customer of the
* automatic currency switch.
*/
public function display_geolocation_currency_update_notice() {
$current_currency = $this->get_selected_currency();
$store_currency = get_option( 'woocommerce_currency' );
$country = $this->geolocation->get_country_by_customer_location();
$geolocated_currency = $this->geolocation->get_currency_by_customer_location();
$currencies = get_woocommerce_currencies();
// Don't run next checks if simulation is enabled.
if ( ! $this->is_simulation_enabled() ) {
// Do not display notice if using the store's default currency.
if ( $store_currency === $current_currency->get_code() ) {
return;
}
// Do not display notice for other currencies than geolocated.
if ( $current_currency->get_code() !== $geolocated_currency ) {
return;
}
}
$message = sprintf(
/* translators: %1 User's country, %2 Selected currency name, %3 Default store currency name, %4 Link to switch currency */
__( 'We noticed you\'re visiting from %1$s. We\'ve updated our prices to %2$s for your shopping convenience. <a href="%4$s">Use %3$s instead.</a>', 'woocommerce-payments' ),
apply_filters( self::FILTER_PREFIX . 'override_notice_country', WC()->countries->countries[ $country ] ),
apply_filters( self::FILTER_PREFIX . 'override_notice_currency_name', $current_currency->get_name() ),
esc_html( $currencies[ $store_currency ] ),
esc_url( '?currency=' . $store_currency )
);
$notice_id = md5( $message );
echo '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id . 2 ) . '" style="display:none;">';
// No need to escape here as the contents of $message is already escaped.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $message;
echo ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce-payments' ) . '</a></p>';
}
/**
* Sets a new customer's currency meta to what's in their session.
* This is needed for when a new user/customer is created during the checkout process.
*
* @param int $customer_id The user/customer id.
*
* @return void
*/
public function set_new_customer_currency_meta( $customer_id ) {
$code = 0 !== $customer_id && WC()->session ? WC()->session->get( self::CURRENCY_SESSION_KEY ) : false;
if ( $code ) {
update_user_meta( $customer_id, self::CURRENCY_META_KEY, $code );
}
}
/**
* Adds Multi-Currency notes to the WC-Admin inbox.
*
* @return void
*/
public function add_woo_admin_notes() {
// Do not try to add notes on ajax requests to improve their performance.
if ( wp_doing_ajax() ) {
return;
}
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
NoteMultiCurrencyAvailable::set_account( $this->payments_account );
NoteMultiCurrencyAvailable::possibly_add_note();
}
}
/**
* Removes Multi-Currency notes from the WC-Admin inbox.
*
* @return void
*/
public static function remove_woo_admin_notes() {
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) {
NoteMultiCurrencyAvailable::possibly_delete_note();
}
}
/**
* Checks if the merchant has enabled automatic currency switching and geolocation.
*
* @return bool
*/
public function is_using_auto_currency_switching(): bool {
return 'yes' === get_option( $this->id . '_enable_auto_currency', 'no' );
}
/**
* Checks if the merchant has enabled the currency switcher widget.
*
* @return bool
*/
public function is_using_storefront_switcher(): bool {
return 'yes' === get_option( $this->id . '_enable_storefront_switcher', 'no' );
}
/**
* Gets the store settings.
*
* @return array The store settings.
*/
public function get_settings() {
return [
$this->id . '_enable_auto_currency' => $this->is_using_auto_currency_switching(),
$this->id . '_enable_storefront_switcher' => $this->is_using_storefront_switcher(),
'site_theme' => wp_get_theme()->get( 'Name' ),
'date_format' => esc_attr( get_option( 'date_format', 'F j, Y' ) ),
'time_format' => esc_attr( get_option( 'time_format', 'g:i a' ) ),
'store_url' => esc_attr( get_page_uri( wc_get_page_id( 'shop' ) ) ),
];
}
/**
* Updates the store settings
*
* @param array $params Update requested values.
*
* @return void
*/
public function update_settings( $params ) {
$updateable_options = [
'wcpay_multi_currency_enable_auto_currency',
'wcpay_multi_currency_enable_storefront_switcher',
];
foreach ( $updateable_options as $key ) {
if ( isset( $params[ $key ] ) ) {
update_option( $key, sanitize_text_field( $params[ $key ] ) );
}
}
}
/**
* Load script with all required dependencies.
*
* @param string $handler Script handler.
* @param string $script Script name relative to the plugin root.
* @param array $additional_dependencies Additional dependencies.
*
* @return void
*/
public function register_script_with_dependencies( string $handler, string $script, array $additional_dependencies = [] ) {
$script_file = $script . '.js';
$script_src_url = plugins_url( $script_file, $this->settings_service->get_plugin_file_path() );
$script_asset_path = plugin_dir_path( $this->settings_service->get_plugin_file_path() ) . $script . '.asset.php';
$script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; // nosemgrep: audit.php.lang.security.file.inclusion-arg -- server generated path is used.
$all_dependencies = array_merge( $script_asset['dependencies'], $additional_dependencies );
wp_register_script(
$handler,
$script_src_url,
$all_dependencies,
$this->get_file_version( $script_file ),
true
);
}
/**
* Get the file modified time as a cache buster if we're in dev mode.
*
* @param string $file Local path to the file.
*
* @return string
*/
public function get_file_version( $file ) {
$plugin_path = plugin_dir_path( $this->settings_service->get_plugin_file_path() );
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $plugin_path . $file ) ) {
return (string) filemtime( $plugin_path . trim( $file, '/' ) );
}
return $this->settings_service->get_plugin_version();
}
/**
* Validates the given currency code.
*
* @param string $currency_code The currency code to check validity.
*
* @return string|false Returns back the currency code in uppercase letters if it's valid, or `false` if not.
*/
public function validate_currency_code( $currency_code ) {
return array_key_exists( strtoupper( $currency_code ), $this->available_currencies )
? strtoupper( $currency_code )
: false;
}
/**
* Get simulation params from querystring and activate when needed
*
* @return void
*/
public function possible_simulation_activation() {
// This is required in the MC onboarding simulation iframe.
$this->simulation_params = $this->get_multi_currency_onboarding_simulation_variables();
if ( ! $this->is_simulation_enabled() ) {
return;
}
// Modify the page links to deliver required params in the simulation.
$this->add_simulation_params_to_preview_urls();
$this->simulate_client_currency();
}
/**
* Returns whether the simulation querystring param is set and active
*
* @return bool Whether the simulation is enabled or not
*/
public function is_simulation_enabled() {
return 0 < count( $this->simulation_params );
}
/**
* Gets the Multi-Currency onboarding preview overrides from the querystring.
*
* @return array Override variables
*/
public function get_multi_currency_onboarding_simulation_variables() {
$parameters = $_GET; // phpcs:ignore WordPress.Security.NonceVerification
// Check if we are in a preview session, don't interfere with the main session.
if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) {
// Check if the page referer has the variables.
$server = $_SERVER; // phpcs:ignore WordPress.Security.NonceVerification
// Check if we are coming from a simulation session (if we don't have the necessary query strings).
if ( isset( $server['HTTP_REFERER'] ) && 0 < strpos( $server['HTTP_REFERER'], 'is_mc_onboarding_simulation' ) ) {
wp_parse_str( wp_parse_url( $server['HTTP_REFERER'], PHP_URL_QUERY ), $parameters );
if ( ! isset( $parameters['is_mc_onboarding_simulation'] ) || ! (bool) $parameters['is_mc_onboarding_simulation'] ) {
return [];
}
} else {
return [];
}
}
// Define variables which can be overridden inside the preview session, with their sanitization methods.
$possible_variables = [
'enable_storefront_switcher' => 'wp_validate_boolean',
'enable_auto_currency' => 'wp_validate_boolean',
];
// Define the defaults if the parameter is missing in the request.
$defaults = [
'enable_storefront_switcher' => false,
'enable_auto_currency' => false,
];
// Prepare the params array.
$values = [];
// Walk through the querystring parameter possibilities, and prepare the params.
foreach ( $possible_variables as $possible_variable => $sanitization_callback ) {
// phpcs:disable WordPress.Security.NonceVerification
if ( isset( $parameters[ $possible_variable ] ) ) {
$values[ $possible_variable ] = $sanitization_callback( $parameters[ $possible_variable ] );
} else {
// Append the default, the param is missing in the querystring.
$values [ $possible_variable ] = $defaults[ $possible_variable ];
}
}
return $values;
}
/**
* Checks if the currently displayed page is the WooCommerce Payments
* settings page for the Multi-Currency settings.
*
* @return bool
*/
public function is_multi_currency_settings_page(): bool {
global $current_screen, $current_tab;
return (
is_admin()
&& $current_tab && $current_screen
&& 'wcpay_multi_currency' === $current_tab
&& 'woocommerce_page_wc-settings' === $current_screen->base
);
}
/**
* Get all the currencies that have been used in the store.
*
* @return array
*/
public function get_all_customer_currencies(): array {
global $wpdb;
$currencies = get_option( self::CUSTOMER_CURRENCIES_KEY );
if ( self::is_customer_currencies_data_valid( $currencies ) ) {
return array_map( 'strtoupper', $currencies );
}
$currencies = $this->get_available_currencies();
$query_union = [];
if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) &&
\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
foreach ( $currencies as $currency ) {
$query_union[] = $wpdb->prepare(
"SELECT %s AS currency_code, EXISTS(SELECT currency FROM {$wpdb->prefix}wc_orders WHERE currency=%s LIMIT 1) AS exists_in_orders",
$currency->code,
$currency->code
);
}
} else {
foreach ( $currencies as $currency ) {
$query_union[] = $wpdb->prepare(
"SELECT %s AS currency_code, EXISTS(SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key=%s AND meta_value=%s LIMIT 1) AS exists_in_orders",
$currency->code,
'_order_currency',
$currency->code
);
}
}
$sub_query = implode( ' UNION ALL ', $query_union );
$query = "SELECT currency_code FROM ( $sub_query ) as subquery WHERE subquery.exists_in_orders=1 ORDER BY currency_code ASC";
$currencies = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( self::is_customer_currencies_data_valid( $currencies ) ) {
update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies );
return array_map( 'strtoupper', $currencies );
}
return [];
}
/**
* Checks if there are additional currencies enabled beyond the store's default one.
*
* @return bool
*/
public function has_additional_currencies_enabled(): bool {
$enabled_currencies = $this->get_enabled_currencies();
return count( $enabled_currencies ) > 1;
}
/**
* Returns if the currency initializations are completed.
*
* @return bool If the initializations have been completed.
*/
public function is_initialized(): bool {
return static::$is_initialized;
}
/**
* Adjusts the given amount for the currently selected currency.
*
* Applies charm pricing if specified, and adjusts the amount according to
* the selected currency's conversion rate.
*
* @param float $amount The original amount to adjust.
* @param bool $apply_charm_pricing Optional. Whether to apply charm pricing to the adjusted amount. Default true.
* @return float The amount adjusted for the selected currency.
*/
public function adjust_amount_for_selected_currency( $amount, $apply_charm_pricing = true ) {
return $this->get_adjusted_price( $amount, $apply_charm_pricing, $this->get_selected_currency() );
}
/**
* Returns the amount with the backend format.
*
* @param float $amount The amount to format.
* @param array $args The arguments to pass to wc_price.
*
* @return string The formatted amount.
*/
public function get_backend_formatted_wc_price( float $amount, array $args = [] ): string {
return wc_price( $amount, $args );
}
/**
* Gets the price after adjusting it with the rounding and charm settings.
*
* @param float $price The price to be adjusted.
* @param bool $apply_charm_pricing Whether charm pricing should be applied.
* @param Currency $currency The currency to be used when adjusting.
*
* @return float The adjusted price.
*/
protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float {
$rounding = (float) $currency->get_rounding();
// If rounding is configured to be `0.00` we still need to round to the nearest lowest
// currency denomination.
// Otherwise we ceil the price to the configured rounding option.
// NOTE: We don't round if currency rounding is > 0.00 because in those cases we want to
// ceil the amount. For example: if $price = 1.251 and currency rounding = 0.25 we
// want that amount ceiled to 1.50. If we round( 1.251 ) to 1.25 before ceiling the
// price to the nearest 0.25 amount the final amount will be 1.25, which is incorrect.
if ( 0.00 === $rounding ) {
$num_decimals = absint(
$this->localization_service->get_currency_format(
$currency->get_code()
)['num_decimals']
);
$price = round( $price, $num_decimals );
} else {
$price = $this->ceil_price( $price, $rounding );
}
if ( $apply_charm_pricing ) {
$price += (float) $currency->get_charm();
}
// Do not return negative prices (possible because of $currency->get_charm()).
return max( 0, $price );
}
/**
* Ceils the price to the next number based on the rounding value.
*
* @param float $price The price to be ceiled.
* @param float $rounding The rounding option.
*
* @return float The ceiled price.
*/
protected function ceil_price( float $price, float $rounding ): float {
if ( 0.00 === $rounding ) {
return $price;
}
return ceil( $price / $rounding ) * $rounding;
}
/**
* Sets up the available currencies, which are alphabetical by name.
*
* @return void
*/
private function initialize_available_currencies() {
// Add default store currency with a rate of 1.0.
$woocommerce_currency = get_woocommerce_currency();
$this->available_currencies[ $woocommerce_currency ] = new Currency( $this->localization_service, $woocommerce_currency, 1.0 );
$available_currencies = [];
$currencies = $this->get_account_available_currencies();
if ( ! empty( $currencies ) ) {
$cache_data = $this->get_cached_currencies();
foreach ( $currencies as $currency_code ) {
$currency_rate = $cache_data['currencies'][ $currency_code ] ?? 1.0;
$update_time = $cache_data['updated'] ?? null;
$new_currency = new Currency( $this->localization_service, $currency_code, $currency_rate, $update_time );
// Add this to our list of available currencies.
$available_currencies[ $new_currency->get_name() ] = $new_currency;
}
}
ksort( $available_currencies );
foreach ( $available_currencies as $currency ) {
$this->available_currencies[ $currency->get_code() ] = $currency;
}
}
/**
* Sets up the enabled currencies.
*
* @return void
*/
private function initialize_enabled_currencies() {
$available_currencies = $this->get_available_currencies();
$enabled_currency_codes = get_option( $this->id . '_enabled_currencies', [] );
$enabled_currency_codes = is_array( $enabled_currency_codes ) ? $enabled_currency_codes : [];
$default_code = $this->get_default_currency()->get_code();
$default = [];
$enabled_currency_codes[] = $default_code;
// This allows to keep the alphabetical sorting by name.
$enabled_currencies = array_filter(
$available_currencies,
function ( $currency ) use ( $enabled_currency_codes ) {
return in_array( $currency->get_code(), $enabled_currency_codes, true );
}
);
$this->enabled_currencies = [];
foreach ( $enabled_currencies as $enabled_currency ) {
// Get the charm and rounding for each enabled currency and add the currencies to the object property.
$currency = clone $enabled_currency;
$charm = get_option( $this->id . '_price_charm_' . $currency->get_id(), 0.00 );
$rounding = get_option( $this->id . '_price_rounding_' . $currency->get_id(), $currency->get_is_zero_decimal() ? '100' : '1.00' );
$currency->set_charm( $charm );
$currency->set_rounding( $rounding );
// If the currency is set to be manual, set the rate to the stored manual rate.
$type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), 'automatic' );
if ( 'manual' === $type ) {
$manual_rate = get_option( $this->id . '_manual_rate_' . $currency->get_id(), $currency->get_rate() );
$currency->set_rate( $manual_rate );
}
$this->enabled_currencies[ $currency->get_code() ] = $currency;
}
// Set default currency to the top of the list.
$default[ $default_code ] = $this->enabled_currencies[ $default_code ];
unset( $this->enabled_currencies[ $default_code ] );
$this->enabled_currencies = array_merge( $default, $this->enabled_currencies );
}
/**
* Sets the default currency.
*
* @return void
*/
private function set_default_currency() {
$available_currencies = $this->get_available_currencies();
$this->default_currency = $available_currencies[ get_woocommerce_currency() ] ?? null;
}
/**
* Returns the currency code stored for the user or in the session.
*
* @return string|null Currency code.
*/
private function get_stored_currency_code() {
$user_id = get_current_user_id();
if ( $user_id ) {
return get_user_meta( $user_id, self::CURRENCY_META_KEY, true );
}
WC()->initialize_session();
$currency_code = WC()->session->get( self::CURRENCY_SESSION_KEY );
return is_string( $currency_code ) ? $currency_code : null;
}
/**
* Checks to see if the store currency has changed. If it has, this will
* also update the option containing the store currency.
*
* @return bool
*/
private function check_store_currency_for_change(): bool {
$last_known_currency = get_option( $this->id . '_store_currency', false );
$woocommerce_currency = get_woocommerce_currency();
// If the last known currency was not set, update the option to set it and return false.
if ( ! $last_known_currency ) {
update_option( $this->id . '_store_currency', $woocommerce_currency );
return false;
}
if ( $last_known_currency !== $woocommerce_currency ) {
update_option( $this->id . '_store_currency', $woocommerce_currency );
return true;
}
return false;
}
/**
* Called when the store currency has changed. Puts any manual rate currencies into an option for a notice to display.
*
* @return void
*/
private function update_manual_rate_currencies_notice_option() {
$enabled_currencies = $this->get_enabled_currencies();
$manual_currencies = [];
// Check enabled currencies for manual rates.
foreach ( $enabled_currencies as $currency ) {
$rate_type = get_option( $this->id . '_exchange_rate_' . $currency->get_id(), false );
if ( 'manual' === $rate_type ) {
$manual_currencies[] = $currency->get_name();
}
}
if ( 0 < count( $manual_currencies ) ) {
update_option( $this->id . '_show_store_currency_changed_notice', $manual_currencies );
}
}
/**
* Accepts an array of currencies that should have their settings removed.
*
* @param array $currencies Array of Currency objects or 3 letter currency codes.
*
* @return void
*/
private function remove_currencies_settings( array $currencies ) {
foreach ( $currencies as $currency ) {
$this->remove_currency_settings( $currency );
}
}
/**
* Will remove a currency's settings if it is not enabled.
*
* @param mixed $currency Currency object or 3 letter currency code.
*
* @return void
*/
private function remove_currency_settings( $currency ) {
$code = is_a( $currency, Currency::class ) ? $currency->get_code() : strtoupper( $currency );
// Bail if the currency code passed is not 3 characters, or if the currency is presently enabled.
if ( 3 !== strlen( $code ) || isset( $this->get_enabled_currencies()[ $code ] ) ) {
return;
}
$settings = [
'price_charm',
'price_rounding',
'manual_rate',
'exchange_rate',
];
// Go through each setting and remove them.
foreach ( $settings as $setting ) {
delete_option( $this->id . '_' . $setting . '_' . strtolower( $code ) );
}
}
/**
* Returns the currencies enabled for the payment provider account that are
* also available in WC.
*
* Can be filtered with the 'wcpay_multi_currency_available_currencies' hook.
*
* @return array Array with the available currencies' codes.
*/
private function get_account_available_currencies(): array {
// If the payment provider is not connected, return an empty array. This prevents using MC without being connected to the payment provider.
if ( ! $this->payments_account->is_provider_connected() ) {
return [];
}
$wc_currencies = array_keys( get_woocommerce_currencies() );
$account_currencies = $wc_currencies;
$account = $this->payments_account->get_cached_account_data();
$supported_currencies = $this->payments_account->get_account_customer_supported_currencies();
if ( $account && ! empty( $supported_currencies ) ) {
$account_currencies = array_map( 'strtoupper', $supported_currencies );
}
/**
* Filter the available currencies for WooCommerce Multi-Currency.
*
* This filter can be used to modify the currencies available for WC Pay
* Multi-Currency. Currencies have to be added in uppercase and should
* also be available in `get_woocommerce_currencies` for them to work.
*
* @since 2.8.0
*
* @param array $available_currencies Current available currencies. Calculated based on
* WC Pay's account currencies and WC currencies.
*/
return apply_filters( self::FILTER_PREFIX . 'available_currencies', array_intersect( $account_currencies, $wc_currencies ) );
}
/**
* Register the CSS and JS admin scripts.
*
* @return void
*/
private function register_admin_scripts() {
$this->register_script_with_dependencies( 'WCPAY_MULTI_CURRENCY_SETTINGS', 'dist/multi-currency', [ 'WCPAY_ADMIN_SETTINGS', 'wp-components' ] );
wp_register_style(
'WCPAY_MULTI_CURRENCY_SETTINGS',
plugins_url( 'dist/multi-currency.css', $this->settings_service->get_plugin_file_path() ),
[ 'wc-components', 'WCPAY_ADMIN_SETTINGS' ],
$this->get_file_version( 'dist/multi-currency.css' ),
'all'
);
}
/**
* Enables simulation of client browser currency.
*
* @return void
*/
private function simulate_client_currency() {
if ( ! $this->simulation_params['enable_auto_currency'] ) {
return;
}
$countries = $this->payments_account->get_supported_countries();
$predefined_simulation_currencies = [
'USD' => $countries['US'],
'GBP' => $countries['GB'],
];
$simulation_currency = 'USD' === get_option( 'woocommerce_currency', 'USD' ) ? 'GBP' : 'USD';
$simulation_currency_name = $this->available_currencies[ $simulation_currency ]->get_name();
$simulation_country = $predefined_simulation_currencies[ $simulation_currency ];
// Simulate client currency from geolocation.
add_filter(
'wcpay_multi_currency_override_notice_currency_name',
function ( $selected_currency_name ) use ( $simulation_currency_name ) {
return $simulation_currency_name;
}
);
// Simulate client country from geolocation.
add_filter(
'wcpay_multi_currency_override_notice_country',
function ( $selected_country ) use ( $simulation_country ) {
return $simulation_country;
}
);
// Always display the notice on simulation screen, prevent duplicate hooks.
if ( ! has_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] ) ) {
add_action( 'wp_footer', [ $this, 'display_geolocation_currency_update_notice' ] );
}
// Skip recalculating the cart to prevent infinite loop in simulation.
remove_action( 'wp_loaded', [ $this, 'recalculate_cart' ] );
}
/**
* Adds the required querystring parameters to all urls in preview pages.
*
* @return void
*/
private function add_simulation_params_to_preview_urls() {
$params = $this->simulation_params;
add_filter(
'wp_footer',
function () use ( $params ) {
?>
<script type="text/javascript" id="wcpay_multi_currency-simulation-script">
// Add simulation overrides to all links.
document.querySelectorAll('a').forEach((link) => {
const parsedURL = new URL(link.href);
if (
false === parsedURL.searchParams.has( 'is_mc_onboarding_simulation' )
) {
parsedURL.searchParams.set('is_mc_onboarding_simulation', true);
parsedURL.searchParams.set('enable_auto_currency', <?php echo esc_attr( $params['enable_auto_currency'] ? 'true' : 'false' ); ?>);
parsedURL.searchParams.set('enable_storefront_switcher', <?php echo esc_attr( $params['enable_storefront_switcher'] ? 'true' : 'false' ); ?>);
link.href = parsedURL.toString();
}
});
// Unhide the store notice in simulation mode.
document.addEventListener('DOMContentLoaded', () => {
const noticeElement = document.querySelector('.woocommerce-store-notice.demo_store')
if( noticeElement ) {
const noticeId = noticeElement.getAttribute('data-notice-id');
cookieStore.delete( 'store_notice' + noticeId );
}
});
</script>
<?php
}
);
}
/**
* Logs a message and throws InvalidCurrencyException.
*
* @param string $method The method that's actually throwing the exception.
* @param string $currency_code The currency code that was invalid.
* @param int $code The exception code.
*
* @throws InvalidCurrencyException
*/
private function log_and_throw_invalid_currency_exception( $method, $currency_code, $code = 500 ) {
$message = 'Invalid currency passed to ' . $method . ': ' . $currency_code;
Logger::error( $message );
throw new InvalidCurrencyException( esc_html( $message ), esc_html( $code ) );
}
/**
* Checks if the customer currencies data is valid.
*
* @param mixed $currencies The currencies to check.
*
* @return bool
*/
private function is_customer_currencies_data_valid( $currencies ) {
return ! empty( $currencies ) && is_array( $currencies );
}
}