From 28ddf490676cbea24eaee755a6004007cb5ec5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 15:33:26 -0300 Subject: [PATCH 1/8] Fallback minimum amount helper to Stripe provided amounts --- includes/class-wc-payments-utils.php | 62 ++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 58b3ce95451..40c10d1f022 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -702,6 +702,62 @@ public static function get_filtered_error_status_code( Exception $e ): int { return $status_code ?? 400; } + /** + * Retrieves Stripe minimum order value authorized per currency. + * The values are based on Stripe's recommendations. + * See https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts. + * + * @param string $currency The currency. + * + * @return int The minimum amount. + */ + public static function get_stripe_minimum_amount( $currency ) { + switch ( $currency ) { + case 'AED': + case 'MYR': + case 'PLN': + case 'RON': + $minimum_amount = 200; + break; + case 'BGN': + $minimum_amount = 100; + break; + case 'CZK': + $minimum_amount = 1500; + break; + case 'DKK': + $minimum_amount = 250; + break; + case 'GBP': + $minimum_amount = 30; + break; + case 'HKD': + $minimum_amount = 400; + break; + case 'HUF': + $minimum_amount = 17500; + break; + case 'JPY': + $minimum_amount = 5000; + break; + case 'MXN': + case 'THB': + $minimum_amount = 1000; + break; + case 'NOK': + case 'SEK': + $minimum_amount = 300; + break; + default: + $minimum_amount = 50; + break; + } + + self::cache_minimum_amount( $currency, $minimum_amount ); + + return $minimum_amount; + } + /** * Saves the minimum amount required for transactions in a given currency. * @@ -713,15 +769,15 @@ public static function cache_minimum_amount( $currency, $amount ) { } /** - * Checks if there is a minimum amount required for transactions in a given currency. + * Retrieves the minimum amount required for transactions in a given currency. * * @param string $currency The currency to check for. * - * @return int|null Either the minimum amount, or `null` if not available. + * @return int The minimum amount. */ public static function get_cached_minimum_amount( $currency ) { $cached = get_transient( 'wcpay_minimum_amount_' . strtolower( $currency ) ); - return (int) $cached ? (int) $cached : null; + return (int) $cached ? (int) $cached : self::get_stripe_minimum_amount( $currency ); } /** From 2b585e27082efb72551f06981f31e8b18920c70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 15:34:10 -0300 Subject: [PATCH 2/8] Pass minimum order amount value to front-end --- ...ments-payment-method-messaging-element.php | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/includes/class-wc-payments-payment-method-messaging-element.php b/includes/class-wc-payments-payment-method-messaging-element.php index d27409d1be0..51850b13150 100644 --- a/includes/class-wc-payments-payment-method-messaging-element.php +++ b/includes/class-wc-payments-payment-method-messaging-element.php @@ -98,19 +98,20 @@ public function init() { 'WCPAY_PRODUCT_DETAILS', 'wcpayStripeSiteMessaging', [ - 'productId' => 'base_product', - 'productVariations' => $product_variations, - 'country' => empty( $billing_country ) ? $store_country : $billing_country, - 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), - 'accountId' => $this->account->get_stripe_account_id(), - 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), - 'paymentMethods' => array_values( $bnpl_payment_methods ), - 'currencyCode' => $currency_code, - 'isCart' => is_cart(), - 'isCartBlock' => $is_cart_block, - 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), - 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), - 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'productId' => 'base_product', + 'productVariations' => $product_variations, + 'country' => empty( $billing_country ) ? $store_country : $billing_country, + 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), + 'accountId' => $this->account->get_stripe_account_id(), + 'publishableKey' => $this->account->get_publishable_key( WC_Payments::mode()->is_test() ), + 'paymentMethods' => array_values( $bnpl_payment_methods ), + 'currencyCode' => $currency_code, + 'isCart' => is_cart(), + 'isCartBlock' => $is_cart_block, + 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), + 'minimumOrderAmount' => WC_Payments_Utils::get_cached_minimum_amount( $currency_code ), + 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), + 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), ] ); From af41a738ed6ca366218e5fb2e08d81229ff2356a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 15:34:38 -0300 Subject: [PATCH 3/8] Make sure skeleton is not rendered if it won't load anything --- client/product-details/bnpl-site-messaging/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/product-details/bnpl-site-messaging/index.js b/client/product-details/bnpl-site-messaging/index.js index 91be16744ab..f90168d1dbc 100644 --- a/client/product-details/bnpl-site-messaging/index.js +++ b/client/product-details/bnpl-site-messaging/index.js @@ -53,10 +53,12 @@ export const initializeBnplSiteMessaging = async () => { isCart, isCartBlock, cartTotal, + minimumOrderAmount, } = window.wcpayStripeSiteMessaging; let amount; let elementLocation = 'bnplProductPage'; + const minOrderAmount = parseInt( minimumOrderAmount, 10 ) || 0; if ( isCart || isCartBlock ) { amount = parseInt( cartTotal, 10 ) || 0; @@ -149,7 +151,7 @@ export const initializeBnplSiteMessaging = async () => { ); let paymentMessageLoading; - if ( ! isCart ) { + if ( ! isCart && amount > minOrderAmount ) { paymentMessageLoading = document.createElement( 'div' ); paymentMessageLoading.classList.add( 'pmme-loading' ); paymentMessageContainer.prepend( paymentMessageLoading ); @@ -208,7 +210,7 @@ export const initializeBnplSiteMessaging = async () => { pmme.style.setProperty( '--wc-bnpl-margin-bottom', '-4px' ); }, 2000 ); } else { - paymentMessageLoading.remove(); + paymentMessageLoading?.remove(); } } ); } From 9f8b0b017982905e015dd383209c431b510d6bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 16:06:17 -0300 Subject: [PATCH 4/8] Update comment --- .../bnpl-site-messaging/index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/product-details/bnpl-site-messaging/index.js b/client/product-details/bnpl-site-messaging/index.js index f90168d1dbc..11283742a3e 100644 --- a/client/product-details/bnpl-site-messaging/index.js +++ b/client/product-details/bnpl-site-messaging/index.js @@ -8,14 +8,6 @@ import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles'; import { getUPEConfig } from 'wcpay/utils/checkout'; import apiRequest from 'wcpay/checkout/utils/request'; -/** - * Initializes the appearance of the payment element by retrieving the UPE configuration - * from the API and saving the appearance if it doesn't exist. If the appearance already exists, - * it is simply returned. - * - * @param {Object} api The API object used to save the UPE configuration. - * @return {Promise<Object>} The appearance object for the UPE. - */ const elementsLocations = { bnplProductPage: { configKey: 'upeBnplProductPageAppearance', @@ -27,6 +19,16 @@ const elementsLocations = { }, }; +/** + * Initializes the appearance of the payment element by retrieving the UPE configuration + * from the API and saving the appearance if it doesn't exist. If the appearance already exists, + * it is simply returned. + * + * @param {Object} api The API object used to save the UPE configuration. + * @param {string} location The location of the UPE. + * + * @return {Promise<Object>} The appearance object for the UPE. + */ async function initializeAppearance( api, location ) { const { configKey, appearanceKey } = elementsLocations[ location ]; From 67df5f469a4224f504fc4261d61a0014abadf249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 16:06:26 -0300 Subject: [PATCH 5/8] Add changelog entry --- changelog/fix-9244-pmme-skeleton | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/fix-9244-pmme-skeleton diff --git a/changelog/fix-9244-pmme-skeleton b/changelog/fix-9244-pmme-skeleton new file mode 100644 index 00000000000..ba34b475bff --- /dev/null +++ b/changelog/fix-9244-pmme-skeleton @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent preload of BNPL messaging if minimum order amount isn't hit. From f97098babf29a0d9643a1216de81b6920379997a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 19:50:10 -0300 Subject: [PATCH 6/8] Make sure the new logic in the cached amount impact order processing --- ...payments-payment-method-messaging-element.php | 2 +- includes/class-wc-payments-utils.php | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/includes/class-wc-payments-payment-method-messaging-element.php b/includes/class-wc-payments-payment-method-messaging-element.php index 51850b13150..6270b99e912 100644 --- a/includes/class-wc-payments-payment-method-messaging-element.php +++ b/includes/class-wc-payments-payment-method-messaging-element.php @@ -109,7 +109,7 @@ public function init() { 'isCart' => is_cart(), 'isCartBlock' => $is_cart_block, 'cartTotal' => WC_Payments_Utils::prepare_amount( $cart_total, $currency_code ), - 'minimumOrderAmount' => WC_Payments_Utils::get_cached_minimum_amount( $currency_code ), + 'minimumOrderAmount' => WC_Payments_Utils::get_cached_minimum_amount( $currency_code, true ), 'nonce' => wp_create_nonce( 'wcpay-get-cart-total' ), 'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ), ] diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index 40c10d1f022..32157c55719 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -769,15 +769,23 @@ public static function cache_minimum_amount( $currency, $amount ) { } /** - * Retrieves the minimum amount required for transactions in a given currency. + * Checks if there is a minimum amount required for transactions in a given currency. * * @param string $currency The currency to check for. + * @param bool $fallback_to_local_list Whether to fallback to the local Stripe list if the cached value is not available. * - * @return int The minimum amount. + * @return int|null Either the minimum amount, or `null` if not available. */ - public static function get_cached_minimum_amount( $currency ) { + public static function get_cached_minimum_amount( $currency, $fallback_to_local_list = false ) { $cached = get_transient( 'wcpay_minimum_amount_' . strtolower( $currency ) ); - return (int) $cached ? (int) $cached : self::get_stripe_minimum_amount( $currency ); + + if ( (int) $cached ) { + return (int) $cached; + } elseif ( $fallback_to_local_list ) { + return self::get_stripe_minimum_amount( $currency ); + } + + return null; } /** From ff16c74fb452a7787c61b0a0e1ef63d5d8279e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Wed, 28 Aug 2024 20:03:25 -0300 Subject: [PATCH 7/8] Add a PHP test --- tests/unit/test-class-wc-payments-utils.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test-class-wc-payments-utils.php b/tests/unit/test-class-wc-payments-utils.php index 08a9420d674..a7aba06f9ab 100644 --- a/tests/unit/test-class-wc-payments-utils.php +++ b/tests/unit/test-class-wc-payments-utils.php @@ -931,6 +931,12 @@ public function test_get_cached_minimum_amount_returns_null_without_cache() { $this->assertNull( $result ); } + public function test_get_cached_minimum_amount_returns_amount_fallbacking_from_stripe_list() { + delete_transient( 'wcpay_minimum_amount_usd' ); + $result = WC_Payments_Utils::get_cached_minimum_amount( 'usd', true ); + $this->assertSame( 50, $result ); + } + public function test_get_last_refund_from_order_id_returns_correct_refund() { $order = WC_Helper_Order::create_order(); $refund_1 = wc_create_refund( [ 'order_id' => $order->get_id() ] ); From db768369585974dec6006558e15505c6304323a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <cesar.costa@automattic.com> Date: Fri, 30 Aug 2024 15:33:56 -0300 Subject: [PATCH 8/8] Hide entire payment message container if amount is not the minimum required --- client/product-details/bnpl-site-messaging/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/product-details/bnpl-site-messaging/index.js b/client/product-details/bnpl-site-messaging/index.js index 11283742a3e..36cb845fb71 100644 --- a/client/product-details/bnpl-site-messaging/index.js +++ b/client/product-details/bnpl-site-messaging/index.js @@ -61,12 +61,19 @@ export const initializeBnplSiteMessaging = async () => { let amount; let elementLocation = 'bnplProductPage'; const minOrderAmount = parseInt( minimumOrderAmount, 10 ) || 0; + const paymentMessageContainer = document.getElementById( + 'payment-method-message' + ); if ( isCart || isCartBlock ) { amount = parseInt( cartTotal, 10 ) || 0; elementLocation = 'bnplClassicCart'; } else { amount = parseInt( productVariations.base_product.amount, 10 ) || 0; + + if ( amount < minOrderAmount ) { + paymentMessageContainer.style.setProperty( 'display', 'none' ); + } } let paymentMessageElement; @@ -144,16 +151,13 @@ export const initializeBnplSiteMessaging = async () => { } // Set the `--wc-bnpl-margin-bottom` CSS variable to the computed bottom margin of the price element. - const paymentMessageContainer = document.getElementById( - 'payment-method-message' - ); paymentMessageContainer.style.setProperty( '--wc-bnpl-margin-bottom', bottomMargin ); let paymentMessageLoading; - if ( ! isCart && amount > minOrderAmount ) { + if ( ! isCart ) { paymentMessageLoading = document.createElement( 'div' ); paymentMessageLoading.classList.add( 'pmme-loading' ); paymentMessageContainer.prepend( paymentMessageLoading );