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 );