diff --git a/multi-currency/src/Compatibility.php b/multi-currency/src/Compatibility.php index 1a4d23af909..87e10cbde78 100644 --- a/multi-currency/src/Compatibility.php +++ b/multi-currency/src/Compatibility.php @@ -39,7 +39,7 @@ class Compatibility extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { add_action( 'init', [ $this, 'init_compatibility_classes' ], 11 ); if ( defined( 'DOING_CRON' ) ) { diff --git a/multi-currency/src/Compatibility/BaseCompatibility.php b/multi-currency/src/Compatibility/BaseCompatibility.php index a98073fd113..3e7d1a67a20 100644 --- a/multi-currency/src/Compatibility/BaseCompatibility.php +++ b/multi-currency/src/Compatibility/BaseCompatibility.php @@ -46,5 +46,5 @@ public function __construct( MultiCurrency $multi_currency, Utils $utils ) { * * @return void */ - abstract protected function init(); + abstract public function init(); } diff --git a/multi-currency/src/Compatibility/WooCommerceBookings.php b/multi-currency/src/Compatibility/WooCommerceBookings.php index 5a99534e5d6..756e4eef355 100644 --- a/multi-currency/src/Compatibility/WooCommerceBookings.php +++ b/multi-currency/src/Compatibility/WooCommerceBookings.php @@ -39,7 +39,7 @@ public function __construct( MultiCurrency $multi_currency, Utils $utils, Fronte * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if Bookings is active. if ( class_exists( 'WC_Bookings' ) ) { if ( ! is_admin() || wp_doing_ajax() ) { diff --git a/multi-currency/src/Compatibility/WooCommerceDeposits.php b/multi-currency/src/Compatibility/WooCommerceDeposits.php index f92819785c5..e2ffa89d441 100644 --- a/multi-currency/src/Compatibility/WooCommerceDeposits.php +++ b/multi-currency/src/Compatibility/WooCommerceDeposits.php @@ -20,7 +20,7 @@ class WooCommerceDeposits extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { if ( class_exists( 'WC_Deposits' ) ) { /* * Multi-currency support was added to WooCommerce Deposits in version 2.0.1. diff --git a/multi-currency/src/Compatibility/WooCommerceFedEx.php b/multi-currency/src/Compatibility/WooCommerceFedEx.php index 738e738150f..8a38d058e40 100644 --- a/multi-currency/src/Compatibility/WooCommerceFedEx.php +++ b/multi-currency/src/Compatibility/WooCommerceFedEx.php @@ -20,7 +20,7 @@ class WooCommerceFedEx extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if FedEx is active. if ( class_exists( 'WC_Shipping_Fedex_Init' ) ) { add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] ); diff --git a/multi-currency/src/Compatibility/WooCommerceNameYourPrice.php b/multi-currency/src/Compatibility/WooCommerceNameYourPrice.php index 155f99e1a4d..fad352e1d1f 100644 --- a/multi-currency/src/Compatibility/WooCommerceNameYourPrice.php +++ b/multi-currency/src/Compatibility/WooCommerceNameYourPrice.php @@ -21,7 +21,7 @@ class WooCommerceNameYourPrice extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if Name Your Price is active. if ( class_exists( 'WC_Name_Your_Price' ) ) { // Convert meta prices. diff --git a/multi-currency/src/Compatibility/WooCommercePointsAndRewards.php b/multi-currency/src/Compatibility/WooCommercePointsAndRewards.php index 9d78886eb41..38819d15322 100644 --- a/multi-currency/src/Compatibility/WooCommercePointsAndRewards.php +++ b/multi-currency/src/Compatibility/WooCommercePointsAndRewards.php @@ -33,7 +33,7 @@ class WooCommercePointsAndRewards extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed filters if Points & Rewards is active and it's not an admin request. if ( is_admin() || ! class_exists( 'WC_Points_Rewards' ) ) { return; diff --git a/multi-currency/src/Compatibility/WooCommercePreOrders.php b/multi-currency/src/Compatibility/WooCommercePreOrders.php index 3c3fe9d5efc..b16dd91b646 100644 --- a/multi-currency/src/Compatibility/WooCommercePreOrders.php +++ b/multi-currency/src/Compatibility/WooCommercePreOrders.php @@ -20,7 +20,7 @@ class WooCommercePreOrders extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if Pre-Orders is active. if ( class_exists( 'WC_Pre_Orders' ) ) { add_filter( 'wc_pre_orders_fee', [ $this, 'wc_pre_orders_fee' ] ); diff --git a/multi-currency/src/Compatibility/WooCommerceProductAddOns.php b/multi-currency/src/Compatibility/WooCommerceProductAddOns.php index 7d245cd4e4f..add583059f8 100644 --- a/multi-currency/src/Compatibility/WooCommerceProductAddOns.php +++ b/multi-currency/src/Compatibility/WooCommerceProductAddOns.php @@ -23,7 +23,7 @@ class WooCommerceProductAddOns extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if Product Add Ons is active. if ( class_exists( 'WC_Product_Addons' ) ) { if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) { diff --git a/multi-currency/src/Compatibility/WooCommerceSubscriptions.php b/multi-currency/src/Compatibility/WooCommerceSubscriptions.php index eb11007fb91..6701f6739dd 100644 --- a/multi-currency/src/Compatibility/WooCommerceSubscriptions.php +++ b/multi-currency/src/Compatibility/WooCommerceSubscriptions.php @@ -59,7 +59,7 @@ class WooCommerceSubscriptions extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if WC Subscriptions or WCPay Subscriptions are active. if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Payments_Subscriptions' ) ) { if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) { diff --git a/multi-currency/src/Compatibility/WooCommerceUPS.php b/multi-currency/src/Compatibility/WooCommerceUPS.php index 6a53f47bff3..427aa060d52 100644 --- a/multi-currency/src/Compatibility/WooCommerceUPS.php +++ b/multi-currency/src/Compatibility/WooCommerceUPS.php @@ -20,7 +20,7 @@ class WooCommerceUPS extends BaseCompatibility { * * @return void */ - protected function init() { + public function init() { // Add needed actions and filters if UPS is active. if ( class_exists( 'WC_Shipping_UPS_Init' ) ) { add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] ); diff --git a/multi-currency/src/Logger.php b/multi-currency/src/Logger.php index 769d57c4844..d9f6865c5b7 100644 --- a/multi-currency/src/Logger.php +++ b/multi-currency/src/Logger.php @@ -12,6 +12,11 @@ */ class Logger { + /** + * Log source identifier. + */ + const LOG_FILE = 'woopayments-multi-currency'; + /** * The WooCommerce logger instance. * @@ -19,11 +24,6 @@ class Logger { */ private $logger; - /** - * Log source identifier. - */ - const LOG_FILE = 'woopayments-multi-currency'; - /** * Log a debug message. * diff --git a/multi-currency/src/MultiCurrency.php b/multi-currency/src/MultiCurrency.php index 009af60e653..4caab08a113 100644 --- a/multi-currency/src/MultiCurrency.php +++ b/multi-currency/src/MultiCurrency.php @@ -601,94 +601,6 @@ public function maybe_update_customer_currencies_option( $order_id ) { update_option( self::CUSTOMER_CURRENCIES_KEY, $currencies ); } - /** - * 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(); - $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; - } - /** * Gets the currencies available. Initializes it if needed. * @@ -1080,355 +992,588 @@ public static function remove_woo_admin_notes() { } /** - * 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. + * Checks if the merchant has enabled automatic currency switching and geolocation. * - * @return float The adjusted price. + * @return bool */ - protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float { - $price = $this->ceil_price( $price, (float) $currency->get_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 ); + public function is_using_auto_currency_switching(): bool { + return 'yes' === get_option( $this->id . '_enable_auto_currency', 'no' ); } /** - * 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. + * Checks if the merchant has enabled the currency switcher widget. * - * @return float The ceiled price. + * @return bool */ - protected function ceil_price( float $price, float $rounding ): float { - if ( 0.00 === $rounding ) { - return $price; - } - return ceil( $price / $rounding ) * $rounding; + public function is_using_storefront_switcher(): bool { + return 'yes' === get_option( $this->id . '_enable_storefront_switcher', 'no' ); } /** - * Returns the currency code stored for the user or in the session. + * Gets the store settings. * - * @return string|null Currency code. + * @return array The store settings. */ - 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; + 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' ) ) ), + ]; } /** - * Checks to see if the store currency has changed. If it has, this will - * also update the option containing the store currency. + * Updates the store settings * - * @return bool + * @param array $params Update requested values. + * + * @return void */ - 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; - } + public function update_settings( $params ) { + $updateable_options = [ + 'wcpay_multi_currency_enable_auto_currency', + 'wcpay_multi_currency_enable_storefront_switcher', + ]; - if ( $last_known_currency !== $woocommerce_currency ) { - update_option( $this->id . '_store_currency', $woocommerce_currency ); - return true; + foreach ( $updateable_options as $key ) { + if ( isset( $params[ $key ] ) ) { + update_option( $key, sanitize_text_field( $params[ $key ] ) ); + } } - - return false; } /** - * Called when the store currency has changed. Puts any manual rate currencies into an option for a notice to display. + * Apply client order currency format and reduces the rounding precision to 2. * - * @return void + * @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(); + public function set_client_format_and_rounding_precision() { + $screen = get_current_screen(); + if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) : + $order = wc_get_order(); + if ( ! $order ) { + return; } - } + $currency = $order->get_currency(); + $currency_format_num_decimals = $this->backend_currencies->get_price_decimals( $currency ); + $currency_format_decimal_sep = $this->backend_currencies->get_price_decimal_separator( $currency ); + $currency_format_thousand_sep = $this->backend_currencies->get_price_thousand_separator( $currency ); + $currency_format = str_replace( [ '%1$s', '%2$s', ' ' ], [ '%s', '%v', ' ' ], $this->backend_currencies->get_woocommerce_price_format( $currency ) ); - if ( 0 < count( $manual_currencies ) ) { - update_option( $this->id . '_show_store_currency_changed_notice', $manual_currencies ); - } + $rounding_precision = wc_get_price_decimals() ?? wc_get_rounding_precision(); + ?> + + gateway_context['plugin_file_path'] ); + $script_asset_path = plugin_dir_path( $this->gateway_context['plugin_file_path'] ) . $script . '.asset.php'; + $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; + $all_dependencies = array_merge( $script_asset['dependencies'], $additional_dependencies ); - foreach ( $currencies as $currency ) { - $this->remove_currency_settings( $currency ); - } + wp_register_script( + $handler, + $script_src_url, + $all_dependencies, + $this->get_file_version( $script_file ), + true + ); } /** - * Will remove a currency's settings if it is not enabled. + * Get the file modified time as a cache buster if we're in dev mode. * - * @param mixed $currency Currency object or 3 letter currency code. + * @param string $file Local path to the file. * - * @return void + * @return string */ - private function remove_currency_settings( $currency ) { - $code = is_a( $currency, Currency::class ) ? $currency->get_code() : strtoupper( $currency ); + public function get_file_version( $file ) { + $plugin_path = plugin_dir_path( $this->gateway_context['plugin_file_path'] ); - // 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; + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $plugin_path . $file ) ) { + return (string) filemtime( $plugin_path . trim( $file, '/' ) ); } - $settings = [ - 'price_charm', - 'price_rounding', - 'manual_rate', - 'exchange_rate', + return $this->gateway_context['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', ]; - // Go through each setting and remove them. - foreach ( $settings as $setting ) { - delete_option( $this->id . '_' . $setting . '_' . strtolower( $code ) ); + // 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; } /** - * Returns the currencies enabled for the payment provider account that are - * also available in WC. + * Checks if the currently displayed page is the WooCommerce Payments + * settings page for the Multi-Currency settings. * - * Can be filtered with the 'wcpay_multi_currency_available_currencies' hook. + * @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 Array with the available currencies' codes. + * @return array */ - 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 []; + 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 ); } - $wc_currencies = array_keys( get_woocommerce_currencies() ); - $account_currencies = $wc_currencies; + $currencies = $this->get_available_currencies(); + $query_union = []; - $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 ); + 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 + ); + } } - /** - * 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 ) ); + $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 the merchant has enabled automatic currency switching and geolocation. + * Checks if there are additional currencies enabled beyond the store's default one. * * @return bool */ - public function is_using_auto_currency_switching(): bool { - return 'yes' === get_option( $this->id . '_enable_auto_currency', 'no' ); + public function has_additional_currencies_enabled(): bool { + $enabled_currencies = $this->get_enabled_currencies(); + return count( $enabled_currencies ) > 1; } /** - * Checks if the merchant has enabled the currency switcher widget. + * Returns if the currency initializations are completed. * - * @return bool + * @return bool If the initializations have been completed. */ - public function is_using_storefront_switcher(): bool { - return 'yes' === get_option( $this->id . '_enable_storefront_switcher', 'no' ); + public function is_initialized(): bool { + return static::$is_initialized; } /** - * Gets the store settings. + * Gets the price after adjusting it with the rounding and charm settings. * - * @return array The store 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. */ - 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' ) ) ), - ]; + protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float { + $price = $this->ceil_price( $price, (float) $currency->get_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 ); } /** - * Updates the store settings + * Ceils the price to the next number based on the rounding value. * - * @param array $params Update requested values. + * @param float $price The price to be ceiled. + * @param float $rounding The rounding option. * - * @return void + * @return float The ceiled price. */ - public function update_settings( $params ) { - $updateable_options = [ - 'wcpay_multi_currency_enable_auto_currency', - 'wcpay_multi_currency_enable_storefront_switcher', - ]; + protected function ceil_price( float $price, float $rounding ): float { + if ( 0.00 === $rounding ) { + return $price; + } + return ceil( $price / $rounding ) * $rounding; + } - foreach ( $updateable_options as $key ) { - if ( isset( $params[ $key ] ) ) { - update_option( $key, sanitize_text_field( $params[ $key ] ) ); - } + /** + * 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(); + $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; } } /** - * Apply client order currency format and reduces the rounding precision to 2. + * Sets up the enabled currencies. * - * @return void + * @return void */ - public function set_client_format_and_rounding_precision() { - $screen = get_current_screen(); - if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) : - $order = wc_get_order(); - if ( ! $order ) { - return; + 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 ); } - $currency = $order->get_currency(); - $currency_format_num_decimals = $this->backend_currencies->get_price_decimals( $currency ); - $currency_format_decimal_sep = $this->backend_currencies->get_price_decimal_separator( $currency ); - $currency_format_thousand_sep = $this->backend_currencies->get_price_thousand_separator( $currency ); - $currency_format = str_replace( [ '%1$s', '%2$s', ' ' ], [ '%s', '%v', ' ' ], $this->backend_currencies->get_woocommerce_price_format( $currency ) ); + ); - $rounding_precision = wc_get_price_decimals() ?? wc_get_rounding_precision(); - ?> - - 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 ); } /** - * Register the CSS and JS admin scripts. + * Sets the default currency. * * @return void */ - private function register_admin_scripts() { - $this->register_script_with_dependencies( 'WCPAY_MULTI_CURRENCY_SETTINGS', 'dist/multi-currency', [ 'WCPAY_ADMIN_SETTINGS' ] ); + private function set_default_currency() { + $available_currencies = $this->get_available_currencies(); + $this->default_currency = $available_currencies[ get_woocommerce_currency() ] ?? null; + } - wp_register_style( - 'WCPAY_MULTI_CURRENCY_SETTINGS', - plugins_url( 'dist/multi-currency.css', $this->gateway_context['plugin_file_path'] ), - [ 'wc-components', 'WCPAY_ADMIN_SETTINGS' ], - $this->get_file_version( 'dist/multi-currency.css' ), - 'all' - ); + /** + * 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; } /** - * Load script with all required dependencies. + * 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 string $handler Script handler. - * @param string $script Script name relative to the plugin root. - * @param array $additional_dependencies Additional dependencies. + * @param array $currencies Array of Currency objects or 3 letter currency codes. * * @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->gateway_context['plugin_file_path'] ); - $script_asset_path = plugin_dir_path( $this->gateway_context['plugin_file_path'] ) . $script . '.asset.php'; - $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; - $all_dependencies = array_merge( $script_asset['dependencies'], $additional_dependencies ); + private function remove_currencies_settings( array $currencies ) { - wp_register_script( - $handler, - $script_src_url, - $all_dependencies, - $this->get_file_version( $script_file ), - true - ); + foreach ( $currencies as $currency ) { + $this->remove_currency_settings( $currency ); + } } /** - * Get the file modified time as a cache buster if we're in dev mode. + * Will remove a currency's settings if it is not enabled. * - * @param string $file Local path to the file. + * @param mixed $currency Currency object or 3 letter currency code. * - * @return string + * @return void */ - public function get_file_version( $file ) { - $plugin_path = plugin_dir_path( $this->gateway_context['plugin_file_path'] ); + private function remove_currency_settings( $currency ) { + $code = is_a( $currency, Currency::class ) ? $currency->get_code() : strtoupper( $currency ); - if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $plugin_path . $file ) ) { - return (string) filemtime( $plugin_path . trim( $file, '/' ) ); + // 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; } - return $this->gateway_context['plugin_version']; + $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 ) ); + } } /** - * Validates the given currency code. + * Returns the currencies enabled for the payment provider account that are + * also available in WC. * - * @param string $currency_code The currency code to check validity. + * Can be filtered with the 'wcpay_multi_currency_available_currencies' hook. * - * @return string|false Returns back the currency code in uppercase letters if it's valid, or `false` if not. + * @return array Array with the available currencies' codes. */ - public function validate_currency_code( $currency_code ) { - return array_key_exists( strtoupper( $currency_code ), $this->available_currencies ) - ? strtoupper( $currency_code ) - : false; + 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 ) ); } /** - * Get simulation params from querystring and activate when needed + * Register the CSS and JS admin scripts. * - * @return void + * @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(); + private function register_admin_scripts() { + $this->register_script_with_dependencies( 'WCPAY_MULTI_CURRENCY_SETTINGS', 'dist/multi-currency', [ 'WCPAY_ADMIN_SETTINGS' ] ); + + wp_register_style( + 'WCPAY_MULTI_CURRENCY_SETTINGS', + plugins_url( 'dist/multi-currency.css', $this->gateway_context['plugin_file_path'] ), + [ 'wc-components', 'WCPAY_ADMIN_SETTINGS' ], + $this->get_file_version( 'dist/multi-currency.css' ), + 'all' + ); } /** @@ -1477,67 +1622,6 @@ function ( $selected_country ) use ( $simulation_country ) { remove_action( 'wp_loaded', [ $this, 'recalculate_cart' ] ); } - /** - * 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; - } - /** * Adds the required querystring parameters to all urls in preview pages. * @@ -1577,90 +1661,6 @@ function () use ( $params ) { ); } - /** - * 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; - } - /** * Logs a message and throws InvalidCurrencyException. *