From c2565ec5569b11d334bacea3cd4c5add20428701 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 24 Jul 2025 11:21:52 +0200 Subject: [PATCH 01/13] Show payment methods without currency support at the end of the payment list --- .../payment-methods-list.js | 63 +++++++++++++++++-- client/utils/use-payment-method-currencies.js | 10 ++- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index e1ad8b6e6f..30a90efbb9 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -1,12 +1,13 @@ /* global wc_stripe_settings_params */ import { sprintf } from '@wordpress/i18n'; -import React from 'react'; +import React, { useContext, useMemo } from 'react'; import styled from '@emotion/styled'; import classnames from 'classnames'; import { Icon as IconComponent, dragHandle } from '@wordpress/icons'; import { Reorder } from 'framer-motion'; import interpolateComponents from 'interpolate-components'; import PaymentMethodsMap from '../../payment-methods-map'; +import UpeToggleContext from '../upe-toggle/context'; import PaymentMethodDescription from './payment-method-description'; import PaymentMethod from './payment-method'; import { @@ -16,6 +17,7 @@ import { } from 'wcstripe/data'; import { useAccount } from 'wcstripe/data/account'; import PaymentMethodFeesPill from 'wcstripe/components/payment-method-fees-pill'; +import { getPaymentMethodCurrencies } from 'wcstripe/utils/use-payment-method-currencies'; import { PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_AFTERPAY_CLEARPAY, @@ -126,6 +128,55 @@ const StyledFees = styled( PaymentMethodFeesPill )` flex: 1 0 auto; `; +/** + * Hook to sort the payment methods based on whether the payment method is supported by the store currency. + * Unsupported payment methods are placed at the end of the list so irrelevant payment methods don't clutter the screen. + * + * @param {string[]} orderedPaymentMethodIds Ordered payment method IDs. + * @return {string[]} Sorted payment method IDs. + */ +const usePaymentMethodsSortedByStoreCurrencySupport = ( + orderedPaymentMethodIds +) => { + const { isUpeEnabled } = useContext( UpeToggleContext ); + + const storeCurrency = window?.wcSettings?.currency?.code; + + // In the logic below, note that getPaymentMethodCurrencies() can return [] + // when the payment method supports all currencies. + // Note that when we don't have a store currency, we put all methods in the supported list. + + const supportedPaymentMethodIds = useMemo( () => { + return orderedPaymentMethodIds.filter( ( paymentMethodId ) => { + const paymentMethodCurrencies = getPaymentMethodCurrencies( + paymentMethodId, + isUpeEnabled + ); + return ( + ! storeCurrency || + paymentMethodCurrencies.length === 0 || + paymentMethodCurrencies.includes( storeCurrency ) + ); + } ); + }, [ orderedPaymentMethodIds, storeCurrency, isUpeEnabled ] ); + + const unsupportedPaymentMethodIds = useMemo( () => { + return orderedPaymentMethodIds.filter( ( paymentMethodId ) => { + const paymentMethodCurrencies = getPaymentMethodCurrencies( + paymentMethodId, + isUpeEnabled + ); + return ( + storeCurrency && + paymentMethodCurrencies.length > 0 && + ! paymentMethodCurrencies.includes( storeCurrency ) + ); + } ); + }, [ orderedPaymentMethodIds, storeCurrency, isUpeEnabled ] ); + + return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ]; +}; + /** * Formats the payment method description with the account default currency. * @@ -190,13 +241,17 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => { setOrderedPaymentMethodIds( newOrderedPaymentMethodIds ); }; + const sortedPaymentMethodIds = usePaymentMethodsSortedByStoreCurrencySupport( + availablePaymentMethods + ); + return isChangingDisplayOrder ? ( - { availablePaymentMethods.map( ( method ) => { + { sortedPaymentMethodIds.map( ( method ) => { // Skip giropay as it was deprecated by Jun, 30th 2024. if ( method === PAYMENT_METHOD_GIROPAY ) { return null; @@ -258,7 +313,7 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => { ) : ( - { availablePaymentMethods.map( ( method ) => { + { sortedPaymentMethodIds.map( ( method ) => { // Skip giropay as it was deprecated by Jun, 30th 2024. if ( method === PAYMENT_METHOD_GIROPAY ) { return null; diff --git a/client/utils/use-payment-method-currencies.js b/client/utils/use-payment-method-currencies.js index afe7a29876..0f495a9301 100644 --- a/client/utils/use-payment-method-currencies.js +++ b/client/utils/use-payment-method-currencies.js @@ -230,9 +230,7 @@ const getAmazonPayCurrencies = () => { } }; -export const usePaymentMethodCurrencies = ( paymentMethodId ) => { - const { isUpeEnabled } = useContext( UpeToggleContext ); - +export const getPaymentMethodCurrencies = ( paymentMethodId, isUpeEnabled ) => { switch ( paymentMethodId ) { case PAYMENT_METHOD_ALIPAY: return getAliPayCurrencies( isUpeEnabled ); @@ -247,4 +245,10 @@ export const usePaymentMethodCurrencies = ( paymentMethodId ) => { } }; +export const usePaymentMethodCurrencies = ( paymentMethodId ) => { + const { isUpeEnabled } = useContext( UpeToggleContext ); + + return getPaymentMethodCurrencies( paymentMethodId, isUpeEnabled ); +}; + export default usePaymentMethodCurrencies; From dcaba9a3781635a5ac014926a8f58919418d42ac Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 24 Jul 2025 11:25:12 +0200 Subject: [PATCH 02/13] Add JSDoc --- client/utils/use-payment-method-currencies.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/utils/use-payment-method-currencies.js b/client/utils/use-payment-method-currencies.js index 0f495a9301..ec8871fa87 100644 --- a/client/utils/use-payment-method-currencies.js +++ b/client/utils/use-payment-method-currencies.js @@ -230,6 +230,14 @@ const getAmazonPayCurrencies = () => { } }; +/** + * Returns the currencies supported by a payment method. + * Note that [] is returned for payment methods that support all currencies. + * + * @param {string} paymentMethodId + * @param {boolean} isUpeEnabled + * @return {string[]} Array of currencies supported by that payment method. + */ export const getPaymentMethodCurrencies = ( paymentMethodId, isUpeEnabled ) => { switch ( paymentMethodId ) { case PAYMENT_METHOD_ALIPAY: @@ -245,6 +253,13 @@ export const getPaymentMethodCurrencies = ( paymentMethodId, isUpeEnabled ) => { } }; +/** + * Hook to return the currencies supported by a payment method. + * Note that [] is returned for payment methods that support all currencies. + * + * @param {string} paymentMethodId + * @return {string[]} Array of currencies supported by that payment method. + */ export const usePaymentMethodCurrencies = ( paymentMethodId ) => { const { isUpeEnabled } = useContext( UpeToggleContext ); From 7377fbf144848276d96275e5bd12bf4800be2f99 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 24 Jul 2025 12:07:25 +0200 Subject: [PATCH 03/13] Memoize and refactor sorting --- .../payment-methods-list.js | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index 30a90efbb9..a23d0bcaea 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -146,35 +146,34 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( // when the payment method supports all currencies. // Note that when we don't have a store currency, we put all methods in the supported list. - const supportedPaymentMethodIds = useMemo( () => { - return orderedPaymentMethodIds.filter( ( paymentMethodId ) => { + const sortedPaymentMethodIds = useMemo( () => { + if ( ! storeCurrency ) { + return orderedPaymentMethodIds; + } + + const supportedPaymentMethodIds = []; + const unsupportedPaymentMethodIds = []; + + orderedPaymentMethodIds.forEach( ( paymentMethodId ) => { const paymentMethodCurrencies = getPaymentMethodCurrencies( paymentMethodId, isUpeEnabled ); - return ( - ! storeCurrency || + + if ( paymentMethodCurrencies.length === 0 || paymentMethodCurrencies.includes( storeCurrency ) - ); + ) { + supportedPaymentMethodIds.push( paymentMethodId ); + } else { + unsupportedPaymentMethodIds.push( paymentMethodId ); + } } ); - }, [ orderedPaymentMethodIds, storeCurrency, isUpeEnabled ] ); - const unsupportedPaymentMethodIds = useMemo( () => { - return orderedPaymentMethodIds.filter( ( paymentMethodId ) => { - const paymentMethodCurrencies = getPaymentMethodCurrencies( - paymentMethodId, - isUpeEnabled - ); - return ( - storeCurrency && - paymentMethodCurrencies.length > 0 && - ! paymentMethodCurrencies.includes( storeCurrency ) - ); - } ); + return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ]; }, [ orderedPaymentMethodIds, storeCurrency, isUpeEnabled ] ); - return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ]; + return sortedPaymentMethodIds; }; /** From b3956c5d66cfc3e7e26ce8a4dc0bd276923ccbcb Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 24 Jul 2025 12:54:56 +0200 Subject: [PATCH 04/13] Fix JS unit tests --- .../general-settings-section.test.js | 85 ++++++++++++++++++- .../payment-methods-list.js | 2 +- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/client/settings/general-settings-section/__tests__/general-settings-section.test.js b/client/settings/general-settings-section/__tests__/general-settings-section.test.js index 2defb652e2..15c7190239 100644 --- a/client/settings/general-settings-section/__tests__/general-settings-section.test.js +++ b/client/settings/general-settings-section/__tests__/general-settings-section.test.js @@ -12,7 +12,10 @@ import { useGetOrderedPaymentMethodIds, useIsPMCEnabled, } from 'wcstripe/data'; -import { usePaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; +import { + usePaymentMethodCurrencies, + getPaymentMethodCurrencies, +} from 'utils/use-payment-method-currencies'; import { useAccount, useGetCapabilities } from 'wcstripe/data/account'; import { PAYMENT_METHOD_ALIPAY, @@ -36,6 +39,7 @@ jest.mock( 'wcstripe/data', () => ( { } ) ); jest.mock( 'utils/use-payment-method-currencies', () => ( { usePaymentMethodCurrencies: jest.fn().mockReturnValue( [] ), + getPaymentMethodCurrencies: jest.fn().mockReturnValue( [] ), } ) ); jest.mock( 'wcstripe/data/account', () => ( { useAccount: jest.fn(), @@ -515,7 +519,7 @@ describe( 'GeneralSettingsSection', () => { ).not.toBeInTheDocument(); } ); - it( 'should disable the payment method checkbox when currency is not supported', () => { + it( 'should disable the payment method checkbox and show the requires currency notice when currency is not supported', () => { useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, @@ -536,9 +540,11 @@ describe( 'GeneralSettingsSection', () => { name: /Credit card/, } ) ).toBeDisabled(); + + expect( screen.queryByText( 'Requires currency' ) ).toBeVisible(); } ); - it( 'should enable the payment method checkbox when currency is supported', () => { + it( 'should enable the payment method checkbox and not show the requires currency notice when currency is supported', () => { useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, @@ -559,5 +565,78 @@ describe( 'GeneralSettingsSection', () => { name: /Credit card/, } ) ).toBeEnabled(); + + expect( + screen.queryByText( 'Requires currency' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should show the payment method with supported currencies at the top of the list', () => { + useGetAvailablePaymentMethodIds.mockReturnValue( [ + PAYMENT_METHOD_CARD, + PAYMENT_METHOD_ALIPAY, + PAYMENT_METHOD_SEPA, + ] ); + useGetOrderedPaymentMethodIds.mockReturnValue( { + orderedPaymentMethodIds: [ + PAYMENT_METHOD_CARD, + PAYMENT_METHOD_ALIPAY, + PAYMENT_METHOD_SEPA, + ], + setOrderedPaymentMethodIds: jest.fn(), + saveOrderedPaymentMethodIds: jest.fn(), + } ); + + /*useEnabledPaymentMethodIds.mockReturnValue( [ + [ PAYMENT_METHOD_CARD ], + ] ); + */ + usePaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { + if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { + return [ 'USD' ]; + } + return [ 'EUR' ]; + } ); + window.wcSettings = { currency: { code: 'EUR' } }; + + getPaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { + if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { + return [ 'USD' ]; + } + return [ 'EUR' ]; + } ); + + render( + + + + ); + + const cardElement = screen.getByRole( 'checkbox', { + name: /Credit card/, + } ); + const alipayElement = screen.getByRole( 'checkbox', { + name: 'Alipay', + } ); + const sepaElement = screen.getByRole( 'checkbox', { + name: 'Direct debit payment', + } ); + + expect( cardElement ).toBeEnabled(); + expect( alipayElement ).not.toBeEnabled(); + expect( sepaElement ).toBeEnabled(); + + // Card should be first + expect( cardElement.compareDocumentPosition( alipayElement ) ).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ); + expect( cardElement.compareDocumentPosition( sepaElement ) ).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ); + + // SEPA should be before AliPay + expect( sepaElement.compareDocumentPosition( alipayElement ) ).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ); } ); } ); diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index a23d0bcaea..5c80bf1083 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -10,6 +10,7 @@ import PaymentMethodsMap from '../../payment-methods-map'; import UpeToggleContext from '../upe-toggle/context'; import PaymentMethodDescription from './payment-method-description'; import PaymentMethod from './payment-method'; +import { getPaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; import { useEnabledPaymentMethodIds, useGetOrderedPaymentMethodIds, @@ -17,7 +18,6 @@ import { } from 'wcstripe/data'; import { useAccount } from 'wcstripe/data/account'; import PaymentMethodFeesPill from 'wcstripe/components/payment-method-fees-pill'; -import { getPaymentMethodCurrencies } from 'wcstripe/utils/use-payment-method-currencies'; import { PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_AFTERPAY_CLEARPAY, From bf373e39ce27cdbda56d6fa26b27e0ced1ea9e4f Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 31 Jul 2025 10:22:45 +0200 Subject: [PATCH 05/13] Add changelog --- changelog.txt | 1 + readme.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 68d173cda1..4e432cb714 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ * Fix - Free trial subscription orders with payment methods that require redirection (eg: iDeal, Bancontact) * Tweak - Update checkout error message for invalid API key to be more generic and user-friendly * Tweak - Disable Amazon Pay in the merchant's Payment Method Configuration object if it is still behind a feature flag +* Update - Show all available payment methods before unavailable payment methods = 9.7.1 - 2025-07-28 = * Add - Add state mapping for Lithuania in express checkout diff --git a/readme.txt b/readme.txt index e15f823a41..0ec387476b 100644 --- a/readme.txt +++ b/readme.txt @@ -128,5 +128,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Fix - Free trial subscription orders with payment methods that require redirection (eg: iDeal, Bancontact) * Tweak - Update checkout error message for invalid API key to be more generic and user-friendly * Tweak - Disable Amazon Pay in the merchant's Payment Method Configuration object if it is still behind a feature flag +* Update - Show all available payment methods before unavailable payment methods [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). From f1b8d3881c6f0e08afe671e8fc1321e6e5cb0f70 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 31 Jul 2025 10:37:45 +0200 Subject: [PATCH 06/13] Use getSetting() instead of global reference; rename variable --- .../general-settings-section/payment-methods-list.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index 5c80bf1083..8c761461ec 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -1,4 +1,5 @@ /* global wc_stripe_settings_params */ +import { getSetting } from '@woocommerce/settings'; import { sprintf } from '@wordpress/i18n'; import React, { useContext, useMemo } from 'react'; import styled from '@emotion/styled'; @@ -140,14 +141,14 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( ) => { const { isUpeEnabled } = useContext( UpeToggleContext ); - const storeCurrency = window?.wcSettings?.currency?.code; + const storeCurrencyCode = getSetting( 'currency' )?.code; // In the logic below, note that getPaymentMethodCurrencies() can return [] // when the payment method supports all currencies. // Note that when we don't have a store currency, we put all methods in the supported list. const sortedPaymentMethodIds = useMemo( () => { - if ( ! storeCurrency ) { + if ( ! storeCurrencyCode ) { return orderedPaymentMethodIds; } @@ -162,7 +163,7 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( if ( paymentMethodCurrencies.length === 0 || - paymentMethodCurrencies.includes( storeCurrency ) + paymentMethodCurrencies.includes( storeCurrencyCode ) ) { supportedPaymentMethodIds.push( paymentMethodId ); } else { @@ -171,7 +172,7 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( } ); return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ]; - }, [ orderedPaymentMethodIds, storeCurrency, isUpeEnabled ] ); + }, [ orderedPaymentMethodIds, storeCurrencyCode, isUpeEnabled ] ); return sortedPaymentMethodIds; }; From c966d8831921bde23da945db93ad138ad7778f53 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 31 Jul 2025 11:11:52 +0200 Subject: [PATCH 07/13] Update unit tests to mock getSetting() and centralise currency mocking --- .../general-settings-section.test.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/client/settings/general-settings-section/__tests__/general-settings-section.test.js b/client/settings/general-settings-section/__tests__/general-settings-section.test.js index 15c7190239..1286636666 100644 --- a/client/settings/general-settings-section/__tests__/general-settings-section.test.js +++ b/client/settings/general-settings-section/__tests__/general-settings-section.test.js @@ -1,3 +1,4 @@ +import { getSetting } from '@woocommerce/settings'; import React from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -45,6 +46,9 @@ jest.mock( 'wcstripe/data/account', () => ( { useAccount: jest.fn(), useGetCapabilities: jest.fn(), } ) ); +jest.mock( '@woocommerce/settings', () => ( { + getSetting: jest.fn(), +} ) ); jest.mock( '@wordpress/data', () => ( { useDispatch: jest.fn().mockReturnValue( {} ), createReduxStore: jest.fn(), @@ -62,8 +66,18 @@ jest.mock( '../../loadable-settings-section', () => ( { children } ) => describe( 'GeneralSettingsSection', () => { const globalValues = global.wcSettings; + /** + * Helper to ensure that the wcSettings global and the getSetting() helper are in sync. + * + * @param {string} currencyCode Currency code to set. + */ + const mockCurrencyCode = ( currencyCode ) => { + global.wcSettings = { currency: { code: currencyCode } }; + getSetting.mockReturnValue( { code: currencyCode } ); + }; + beforeEach( () => { - global.wcSettings = { currency: { code: 'EUR' } }; + mockCurrencyCode( 'EUR' ); global.wc_stripe_settings_params = { are_apms_deprecated: false }; useGetCapabilities.mockReturnValue( { card_payments: 'active', @@ -338,7 +352,7 @@ describe( 'GeneralSettingsSection', () => { } ); it( 'does not display the payment method checkbox when currency is not supprted', () => { - global.wcSettings = { currency: { code: 'USD' } }; + mockCurrencyCode( 'USD' ); useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, @@ -528,7 +542,7 @@ describe( 'GeneralSettingsSection', () => { [ PAYMENT_METHOD_CARD ], ] ); usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] ); - window.wcSettings = { currency: { code: 'EUR' } }; + mockCurrencyCode( 'EUR' ); render( @@ -553,7 +567,7 @@ describe( 'GeneralSettingsSection', () => { [ PAYMENT_METHOD_CARD ], ] ); usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] ); - window.wcSettings = { currency: { code: 'USD' } }; + mockCurrencyCode( 'USD' ); render( @@ -587,17 +601,13 @@ describe( 'GeneralSettingsSection', () => { saveOrderedPaymentMethodIds: jest.fn(), } ); - /*useEnabledPaymentMethodIds.mockReturnValue( [ - [ PAYMENT_METHOD_CARD ], - ] ); - */ usePaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { return [ 'USD' ]; } return [ 'EUR' ]; } ); - window.wcSettings = { currency: { code: 'EUR' } }; + mockCurrencyCode( 'EUR' ); getPaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { From 9f3e05eb96b532ecb8046ab0dd2fddd260390696 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Thu, 31 Jul 2025 11:12:35 +0200 Subject: [PATCH 08/13] Return early when UPE is disabled; update notes --- .../general-settings-section/payment-methods-list.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index 8c761461ec..f96e5e5680 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -145,10 +145,9 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( // In the logic below, note that getPaymentMethodCurrencies() can return [] // when the payment method supports all currencies. - // Note that when we don't have a store currency, we put all methods in the supported list. - + // When we don't have a store currency or UPE is disabled, we put all methods in the supported list. const sortedPaymentMethodIds = useMemo( () => { - if ( ! storeCurrencyCode ) { + if ( ! storeCurrencyCode || ! isUpeEnabled ) { return orderedPaymentMethodIds; } From f3a981a58658f638c9fe02f879b818d283f16b39 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Fri, 15 Aug 2025 15:24:29 +0200 Subject: [PATCH 09/13] Refactor logic into central helper function --- .../index.js | 75 ++++++++--------- .../index.js | 82 +++++++++---------- .../payment-methods-list.js | 17 ++-- client/stripe-utils/constants.js | 5 ++ .../get-payment-method-unavailable-reason.js | 57 +++++++++++++ .../use-payment-method-unavailable-reason.js | 24 ++++++ 6 files changed, 168 insertions(+), 92 deletions(-) create mode 100644 client/utils/get-payment-method-unavailable-reason.js create mode 100644 client/utils/use-payment-method-unavailable-reason.js diff --git a/client/components/payment-method-missing-currency-pill/index.js b/client/components/payment-method-missing-currency-pill/index.js index 604f43afbb..2efdd864b8 100644 --- a/client/components/payment-method-missing-currency-pill/index.js +++ b/client/components/payment-method-missing-currency-pill/index.js @@ -5,7 +5,8 @@ import interpolateComponents from 'interpolate-components'; import { Icon, info } from '@wordpress/icons'; import Popover from 'wcstripe/components/popover'; import { usePaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; -import { PAYMENT_METHOD_CARD } from 'wcstripe/stripe-utils/constants'; +import usePaymentMethodUnavailableReason from 'utils/use-payment-method-unavailable-reason'; +import { PAYMENT_METHOD_UNAVAILABLE_REASONS } from 'wcstripe/stripe-utils/constants'; const StyledPill = styled.span` display: inline-flex; @@ -47,47 +48,47 @@ const IconComponent = ( { children, ...props } ) => ( const PaymentMethodMissingCurrencyPill = ( { id, label } ) => { const paymentMethodCurrencies = usePaymentMethodCurrencies( id ); - const storeCurrency = window?.wcSettings?.currency?.code; + const unavailableReason = usePaymentMethodUnavailableReason( id ); if ( - id !== PAYMENT_METHOD_CARD && - ! paymentMethodCurrencies.includes( storeCurrency ) + unavailableReason !== + PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY ) { - return ( - - { __( 'Requires currency', 'woocommerce-gateway-stripe' ) } - { - // Stop propagation is necessary so it doesn't trigger the tooltip click event. - ev.stopPropagation(); - } } - /> - ), - }, - } ) } - /> - - ); + return null; } - return null; + return ( + + { __( 'Requires currency', 'woocommerce-gateway-stripe' ) } + { + // Stop propagation is necessary so it doesn't trigger the tooltip click event. + ev.stopPropagation(); + } } + /> + ), + }, + } ) } + /> + + ); }; export default PaymentMethodMissingCurrencyPill; diff --git a/client/components/payment-method-unavailable-due-conflict-pill/index.js b/client/components/payment-method-unavailable-due-conflict-pill/index.js index 7610b54346..9304282ee4 100644 --- a/client/components/payment-method-unavailable-due-conflict-pill/index.js +++ b/client/components/payment-method-unavailable-due-conflict-pill/index.js @@ -1,14 +1,11 @@ -/* global wc_stripe_settings_params */ import { __, sprintf } from '@wordpress/i18n'; import React from 'react'; import styled from '@emotion/styled'; import interpolateComponents from 'interpolate-components'; import { Icon, info } from '@wordpress/icons'; +import usePaymentMethodUnavailableReason from 'utils/use-payment-method-unavailable-reason'; import Popover from 'wcstripe/components/popover'; -import { - PAYMENT_METHOD_AFFIRM, - PAYMENT_METHOD_KLARNA, -} from 'wcstripe/stripe-utils/constants'; +import { PAYMENT_METHOD_UNAVAILABLE_REASONS } from 'wcstripe/stripe-utils/constants'; const StyledPill = styled.span` display: inline-flex; @@ -49,48 +46,45 @@ const IconComponent = ( { children, ...props } ) => ( ); const PaymentMethodUnavailableDueConflictPill = ( { id, label } ) => { + const unavailableReason = usePaymentMethodUnavailableReason( id ); + if ( - ( id === PAYMENT_METHOD_AFFIRM && - // eslint-disable-next-line camelcase - wc_stripe_settings_params.has_affirm_gateway_plugin ) || - ( id === PAYMENT_METHOD_KLARNA && - // eslint-disable-next-line camelcase - wc_stripe_settings_params.has_klarna_gateway_plugin ) + unavailableReason !== + PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT ) { - return ( - - { __( 'Has plugin conflict', 'woocommerce-gateway-stripe' ) } - { - // Stop propagation is necessary so it doesn't trigger the tooltip click event. - ev.stopPropagation(); - } } - /> - ), - }, - } ) } - /> - - ); + return null; } - - return null; + return ( + + { __( 'Has plugin conflict', 'woocommerce-gateway-stripe' ) } + { + // Stop propagation is necessary so it doesn't trigger the tooltip click event. + ev.stopPropagation(); + } } + /> + ), + }, + } ) } + /> + + ); }; export default PaymentMethodUnavailableDueConflictPill; diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index f96e5e5680..74dd86ed46 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -11,7 +11,7 @@ import PaymentMethodsMap from '../../payment-methods-map'; import UpeToggleContext from '../upe-toggle/context'; import PaymentMethodDescription from './payment-method-description'; import PaymentMethod from './payment-method'; -import { getPaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; +import getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason'; import { useEnabledPaymentMethodIds, useGetOrderedPaymentMethodIds, @@ -143,8 +143,6 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( const storeCurrencyCode = getSetting( 'currency' )?.code; - // In the logic below, note that getPaymentMethodCurrencies() can return [] - // when the payment method supports all currencies. // When we don't have a store currency or UPE is disabled, we put all methods in the supported list. const sortedPaymentMethodIds = useMemo( () => { if ( ! storeCurrencyCode || ! isUpeEnabled ) { @@ -155,15 +153,12 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( const unsupportedPaymentMethodIds = []; orderedPaymentMethodIds.forEach( ( paymentMethodId ) => { - const paymentMethodCurrencies = getPaymentMethodCurrencies( + const unavailableReason = getPaymentMethodUnavailableReason( { paymentMethodId, - isUpeEnabled - ); - - if ( - paymentMethodCurrencies.length === 0 || - paymentMethodCurrencies.includes( storeCurrencyCode ) - ) { + isUpeEnabled, + storeCurrencyCode, + } ); + if ( unavailableReason === null ) { supportedPaymentMethodIds.push( paymentMethodId ); } else { unsupportedPaymentMethodIds.push( paymentMethodId ); diff --git a/client/stripe-utils/constants.js b/client/stripe-utils/constants.js index 796085f4b8..6eb590910f 100644 --- a/client/stripe-utils/constants.js +++ b/client/stripe-utils/constants.js @@ -158,3 +158,8 @@ export const BNPL_METHODS = [ PAYMENT_METHOD_AFTERPAY_CLEARPAY, PAYMENT_METHOD_KLARNA, ]; + +export const PAYMENT_METHOD_UNAVAILABLE_REASONS = { + UNSUPPORTED_CURRENCY: 'unsupported_currency', + OFFICIAL_PLUGIN_CONFLICT: 'official_plugin_conflict', +}; diff --git a/client/utils/get-payment-method-unavailable-reason.js b/client/utils/get-payment-method-unavailable-reason.js new file mode 100644 index 0000000000..48c417aa35 --- /dev/null +++ b/client/utils/get-payment-method-unavailable-reason.js @@ -0,0 +1,57 @@ +import { + PAYMENT_METHOD_AFFIRM, + PAYMENT_METHOD_KLARNA, + PAYMENT_METHOD_UNAVAILABLE_REASONS, +} from 'wcstripe/stripe-utils/constants'; +import { getPaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; + +/** + * Returns the reason why a payment method is unavailable, or null if it is available. + * Intentionally outside of a React hook to support looping over payment methods. + * + * @param {Object} context + * @param {string} context.paymentMethodId The payment method ID. + * @param {boolean} context.isUpeEnabled Whether UPE is enabled. If false, the payment method is available. + * @param {string|null} context.storeCurrencyCode The store currency code. If null, the payment method is available. + * @return {string|null} The reason why the payment method is unavailable, or null if it is available. See `PAYMENT_METHOD_UNAVAILABLE_REASONS` for possible values. + */ +const getPaymentMethodUnavailableReason = ( { + paymentMethodId, + isUpeEnabled = true, + storeCurrencyCode, +} ) => { + if ( ! storeCurrencyCode || ! isUpeEnabled ) { + return null; + } + + if ( + paymentMethodId === PAYMENT_METHOD_KLARNA && + window?.wc_stripe_settings_params?.has_klarna_gateway_plugin + ) { + return PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT; + } + + if ( + paymentMethodId === PAYMENT_METHOD_AFFIRM && + window?.wc_stripe_settings_params?.has_affirm_gateway_plugin + ) { + return PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT; + } + + const paymentMethodCurrencies = getPaymentMethodCurrencies( + paymentMethodId, + true + ); + + // Note that getPaymentMethodCurrencies() returns [] when the payment method supports all currencies. + if ( paymentMethodCurrencies.length === 0 ) { + return null; + } + if ( paymentMethodCurrencies.includes( storeCurrencyCode ) ) { + return null; + } + + return PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY; +}; + +export default getPaymentMethodUnavailableReason; diff --git a/client/utils/use-payment-method-unavailable-reason.js b/client/utils/use-payment-method-unavailable-reason.js new file mode 100644 index 0000000000..c68338d1b4 --- /dev/null +++ b/client/utils/use-payment-method-unavailable-reason.js @@ -0,0 +1,24 @@ +import { getSetting } from '@woocommerce/settings'; +import { useContext } from 'react'; +import UpeToggleContext from 'wcstripe/settings/upe-toggle/context'; +import getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason'; + +/** + * React hook to return the reason why a payment method is unavailable, or null if it is available. + * + * @param {string} paymentMethodId The payment method ID. + * @return {string|null} The reason why the payment method is unavailable, or null if it is available. See `PAYMENT_METHOD_UNAVAILABLE_REASONS` for possible values. + */ +const usePaymentMethodUnavailableReason = ( paymentMethodId ) => { + const { isUpeEnabled } = useContext( UpeToggleContext ); + + const storeCurrencyCode = getSetting( 'currency' )?.code; + + return getPaymentMethodUnavailableReason( { + paymentMethodId, + isUpeEnabled, + storeCurrencyCode, + } ); +}; + +export default usePaymentMethodUnavailableReason; From a2230e521d2f6160efd54ee6e0b9b369cb71119c Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Fri, 15 Aug 2025 16:32:54 +0200 Subject: [PATCH 10/13] Update unit tests --- .../__tests__/index.test.js | 23 +++--- .../general-settings-section.test.js | 70 ++++++++++--------- .../payment-method.js | 18 ++--- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/client/components/payment-method-missing-currency-pill/__tests__/index.test.js b/client/components/payment-method-missing-currency-pill/__tests__/index.test.js index 996f91afcf..fc837ed06e 100644 --- a/client/components/payment-method-missing-currency-pill/__tests__/index.test.js +++ b/client/components/payment-method-missing-currency-pill/__tests__/index.test.js @@ -2,6 +2,8 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import PaymentMethodMissingCurrencyPill from '..'; import { usePaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; +import usePaymentMethodUnavailableReason from 'utils/use-payment-method-unavailable-reason'; +import { PAYMENT_METHOD_UNAVAILABLE_REASONS } from 'wcstripe/stripe-utils/constants'; jest.mock( '../../../payment-methods-map', () => ( { card: { currencies: [] }, @@ -12,13 +14,19 @@ jest.mock( 'utils/use-payment-method-currencies', () => ( { usePaymentMethodCurrencies: jest.fn(), } ) ); +jest.mock( 'utils/use-payment-method-unavailable-reason' ); + describe( 'PaymentMethodMissingCurrencyPill', () => { beforeEach( () => { global.wcSettings = { currency: { code: 'USD' } }; usePaymentMethodCurrencies.mockReturnValue( [ 'EUR' ] ); } ); - it( 'should render the "Requires currency" text', () => { + it( 'should render the "Requires currency" text when currency is not supported', () => { + usePaymentMethodUnavailableReason.mockReturnValue( + PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY + ); + render( ); @@ -26,20 +34,13 @@ describe( 'PaymentMethodMissingCurrencyPill', () => { expect( screen.queryByText( 'Requires currency' ) ).toBeInTheDocument(); } ); - it( 'should not render when currency matches', () => { - global.wcSettings = { currency: { code: 'EUR' } }; + it( 'should not render when currency is supported', () => { + usePaymentMethodUnavailableReason.mockReturnValue( null ); + const { container } = render( ); expect( container.firstChild ).toBeNull(); } ); - - it( 'should render when currency differs', () => { - render( - - ); - - expect( screen.queryByText( 'Requires currency' ) ).toBeInTheDocument(); - } ); } ); diff --git a/client/settings/general-settings-section/__tests__/general-settings-section.test.js b/client/settings/general-settings-section/__tests__/general-settings-section.test.js index 1286636666..c069aea64f 100644 --- a/client/settings/general-settings-section/__tests__/general-settings-section.test.js +++ b/client/settings/general-settings-section/__tests__/general-settings-section.test.js @@ -13,10 +13,7 @@ import { useGetOrderedPaymentMethodIds, useIsPMCEnabled, } from 'wcstripe/data'; -import { - usePaymentMethodCurrencies, - getPaymentMethodCurrencies, -} from 'utils/use-payment-method-currencies'; +import getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason'; import { useAccount, useGetCapabilities } from 'wcstripe/data/account'; import { PAYMENT_METHOD_ALIPAY, @@ -25,6 +22,7 @@ import { PAYMENT_METHOD_LINK, PAYMENT_METHOD_SEPA, PAYMENT_METHOD_SOFORT, + PAYMENT_METHOD_UNAVAILABLE_REASONS, } from 'wcstripe/stripe-utils/constants'; jest.mock( 'wcstripe/data', () => ( { @@ -38,10 +36,7 @@ jest.mock( 'wcstripe/data', () => ( { useGetOrderedPaymentMethodIds: jest.fn(), useIsPMCEnabled: jest.fn(), } ) ); -jest.mock( 'utils/use-payment-method-currencies', () => ( { - usePaymentMethodCurrencies: jest.fn().mockReturnValue( [] ), - getPaymentMethodCurrencies: jest.fn().mockReturnValue( [] ), -} ) ); +jest.mock( 'utils/get-payment-method-unavailable-reason' ); jest.mock( 'wcstripe/data/account', () => ( { useAccount: jest.fn(), useGetCapabilities: jest.fn(), @@ -84,6 +79,7 @@ describe( 'GeneralSettingsSection', () => { alipay_payments: 'active', } ); useManualCapture.mockReturnValue( [ false ] ); + getPaymentMethodUnavailableReason.mockReturnValue( null ); useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_LINK, @@ -351,7 +347,7 @@ describe( 'GeneralSettingsSection', () => { expect( updateEnabledMethodsMock ).toHaveBeenCalled(); } ); - it( 'does not display the payment method checkbox when currency is not supprted', () => { + it( 'does not display the payment method checkbox when currency is not supported', () => { mockCurrencyCode( 'USD' ); useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, @@ -534,14 +530,29 @@ describe( 'GeneralSettingsSection', () => { } ); it( 'should disable the payment method checkbox and show the requires currency notice when currency is not supported', () => { + useEnabledPaymentMethodIds.mockReturnValue( [ + [ PAYMENT_METHOD_CARD ], + ] ); useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, ] ); - useEnabledPaymentMethodIds.mockReturnValue( [ - [ PAYMENT_METHOD_CARD ], - ] ); - usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] ); + useGetOrderedPaymentMethodIds.mockReturnValue( { + orderedPaymentMethodIds: [ + PAYMENT_METHOD_CARD, + PAYMENT_METHOD_ALIPAY, + ], + setOrderedPaymentMethodIds: jest.fn(), + saveOrderedPaymentMethodIds: jest.fn(), + } ); + getPaymentMethodUnavailableReason.mockImplementation( + ( { paymentMethodId } ) => { + if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { + return PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY; + } + return null; + } + ); mockCurrencyCode( 'EUR' ); render( @@ -553,20 +564,18 @@ describe( 'GeneralSettingsSection', () => { screen.queryByRole( 'checkbox', { name: /Credit card/, } ) + ).toBeEnabled(); + + expect( + screen.queryByRole( 'checkbox', { + name: 'Alipay', + } ) ).toBeDisabled(); expect( screen.queryByText( 'Requires currency' ) ).toBeVisible(); } ); it( 'should enable the payment method checkbox and not show the requires currency notice when currency is supported', () => { - useGetAvailablePaymentMethodIds.mockReturnValue( [ - PAYMENT_METHOD_CARD, - PAYMENT_METHOD_ALIPAY, - ] ); - useEnabledPaymentMethodIds.mockReturnValue( [ - [ PAYMENT_METHOD_CARD ], - ] ); - usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] ); mockCurrencyCode( 'USD' ); render( @@ -601,21 +610,16 @@ describe( 'GeneralSettingsSection', () => { saveOrderedPaymentMethodIds: jest.fn(), } ); - usePaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { - if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { - return [ 'USD' ]; + getPaymentMethodUnavailableReason.mockImplementation( + ( { paymentMethodId } ) => { + if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { + return PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY; + } + return null; } - return [ 'EUR' ]; - } ); + ); mockCurrencyCode( 'EUR' ); - getPaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { - if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { - return [ 'USD' ]; - } - return [ 'EUR' ]; - } ); - render( diff --git a/client/settings/general-settings-section/payment-method.js b/client/settings/general-settings-section/payment-method.js index e511c9ffde..9476e53fae 100644 --- a/client/settings/general-settings-section/payment-method.js +++ b/client/settings/general-settings-section/payment-method.js @@ -12,10 +12,9 @@ import { PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_AFTERPAY_CLEARPAY, PAYMENT_METHOD_CARD, - PAYMENT_METHOD_KLARNA, } from 'wcstripe/stripe-utils/constants'; import PaymentMethodFeesPill from 'wcstripe/components/payment-method-fees-pill'; -import { usePaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; +import usePaymentMethodUnavailableReason from 'utils/use-payment-method-unavailable-reason'; const ListElement = styled.li` display: flex; @@ -107,7 +106,9 @@ const StyledFees = styled( PaymentMethodFeesPill )` const PaymentMethod = ( { method, data } ) => { const [ isOCEnabled ] = useIsOCEnabled(); const [ isManualCaptureEnabled ] = useManualCapture(); - const paymentMethodCurrencies = usePaymentMethodCurrencies( method ); + const paymentMethodUnavailableReason = usePaymentMethodUnavailableReason( + method + ); const { Icon, @@ -127,16 +128,7 @@ const PaymentMethod = ( { method, data } ) => { wc_stripe_settings_params.are_apms_deprecated && method !== PAYMENT_METHOD_CARD; - const storeCurrency = window?.wcSettings?.currency?.code; - const isDisabled = - ( paymentMethodCurrencies.length && - ! paymentMethodCurrencies.includes( storeCurrency ) ) || - ( PAYMENT_METHOD_AFFIRM === method && - // eslint-disable-next-line camelcase - wc_stripe_settings_params.has_affirm_gateway_plugin ) || - ( PAYMENT_METHOD_KLARNA === method && - // eslint-disable-next-line camelcase - wc_stripe_settings_params.has_klarna_gateway_plugin ); + const isDisabled = paymentMethodUnavailableReason !== null; const isDisabledButChecked = PAYMENT_METHOD_CARD === method && isOCEnabled; From 3089fce9eeb440d086d4b0b73952b0f90c38a78c Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Fri, 15 Aug 2025 16:47:45 +0200 Subject: [PATCH 11/13] Add tests for getPaymentMethodUnavailableReason --- ...-payment-method-unavailable-reason.test.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 client/utils/__tests__/get-payment-method-unavailable-reason.test.js diff --git a/client/utils/__tests__/get-payment-method-unavailable-reason.test.js b/client/utils/__tests__/get-payment-method-unavailable-reason.test.js new file mode 100644 index 0000000000..b90aea3304 --- /dev/null +++ b/client/utils/__tests__/get-payment-method-unavailable-reason.test.js @@ -0,0 +1,115 @@ +import { + PAYMENT_METHOD_AFFIRM, + PAYMENT_METHOD_CARD, + PAYMENT_METHOD_KLARNA, + PAYMENT_METHOD_SEPA, + PAYMENT_METHOD_UNAVAILABLE_REASONS, +} from 'wcstripe/stripe-utils/constants'; +import getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason'; +import { getPaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; + +jest.mock( 'utils/use-payment-method-currencies', () => ( { + getPaymentMethodCurrencies: jest.fn(), +} ) ); + +describe( 'getPaymentMethodUnavailableReason', () => { + beforeEach( () => { + global.wc_stripe_settings_params = { + has_klarna_gateway_plugin: false, + has_affirm_gateway_plugin: false, + }; + getPaymentMethodCurrencies.mockImplementation( ( paymentMethodId ) => { + if ( paymentMethodId === PAYMENT_METHOD_CARD ) { + return []; + } + if ( paymentMethodId === PAYMENT_METHOD_SEPA ) { + return [ 'EUR' ]; + } + return [ 'USD' ]; + } ); + } ); + + it( 'should return null if UPE is disabled', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_CARD, + isUpeEnabled: false, + storeCurrencyCode: 'USD', + } ) + ).toBeNull(); + } ); + + it( 'should return null if the store currency is not set', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_CARD, + storeCurrencyCode: null, + } ) + ).toBeNull(); + } ); + + it( 'should return null when UPE is disabled and the store currency is not set', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_CARD, + isUpeEnabled: false, + storeCurrencyCode: null, + } ) + ).toBeNull(); + } ); + + it( 'should return null for a payment method that supports all currencies when store is in USD', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_CARD, + storeCurrencyCode: 'USD', + } ) + ).toBeNull(); + } ); + it( 'should return null for a payment method that supports all currencies when store is in EUR', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_CARD, + storeCurrencyCode: 'EUR', + } ) + ).toBeNull(); + } ); + + it( 'should return OFFICIAL_PLUGIN_CONFLICT when Klarna is unavailable due to a conflict with an official plugin', () => { + global.wc_stripe_settings_params.has_klarna_gateway_plugin = true; + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_KLARNA, + storeCurrencyCode: 'USD', + } ) + ).toBe( PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT ); + } ); + + it( 'should return OFFICIAL_PLUGIN_CONFLICT when Affirm is unavailable due to a conflict with an official plugin', () => { + global.wc_stripe_settings_params.has_affirm_gateway_plugin = true; + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_AFFIRM, + storeCurrencyCode: 'USD', + } ) + ).toBe( PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT ); + } ); + + it( 'should return UNSUPPORTED_CURRENCY when the payment method is unavailable due to an unsupported currency - EUR needed; store in USD', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_SEPA, + storeCurrencyCode: 'USD', + } ) + ).toBe( PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY ); + } ); + + it( 'should return UNSUPPORTED_CURRENCY when the payment method is unavailable due to an unsupported currency - USD needed; store in EUR', () => { + expect( + getPaymentMethodUnavailableReason( { + paymentMethodId: PAYMENT_METHOD_AFFIRM, + storeCurrencyCode: 'EUR', + } ) + ).toBe( PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY ); + } ); +} ); From a13b5c6696085521e58bc953536449055a94111d Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Mon, 18 Aug 2025 09:15:01 +0200 Subject: [PATCH 12/13] Sort plugin conflicts before currency issues --- .../general-settings-section.test.js | 22 +++++++++++-- .../payment-methods-list.js | 32 +++++++++++-------- .../get-payment-method-unavailable-reason.js | 8 ++--- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/client/settings/general-settings-section/__tests__/general-settings-section.test.js b/client/settings/general-settings-section/__tests__/general-settings-section.test.js index c069aea64f..56f2613c9a 100644 --- a/client/settings/general-settings-section/__tests__/general-settings-section.test.js +++ b/client/settings/general-settings-section/__tests__/general-settings-section.test.js @@ -16,6 +16,7 @@ import { import getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason'; import { useAccount, useGetCapabilities } from 'wcstripe/data/account'; import { + PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_ALIPAY, PAYMENT_METHOD_CARD, PAYMENT_METHOD_EPS, @@ -594,16 +595,18 @@ describe( 'GeneralSettingsSection', () => { ).not.toBeInTheDocument(); } ); - it( 'should show the payment method with supported currencies at the top of the list', () => { + it( 'should show the payment method with supported currencies before plugin conflicts and unsupported currencies', () => { useGetAvailablePaymentMethodIds.mockReturnValue( [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, + PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_SEPA, ] ); useGetOrderedPaymentMethodIds.mockReturnValue( { orderedPaymentMethodIds: [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_ALIPAY, + PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_SEPA, ], setOrderedPaymentMethodIds: jest.fn(), @@ -615,6 +618,9 @@ describe( 'GeneralSettingsSection', () => { if ( paymentMethodId === PAYMENT_METHOD_ALIPAY ) { return PAYMENT_METHOD_UNAVAILABLE_REASONS.UNSUPPORTED_CURRENCY; } + if ( paymentMethodId === PAYMENT_METHOD_AFFIRM ) { + return PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT; + } return null; } ); @@ -632,12 +638,16 @@ describe( 'GeneralSettingsSection', () => { const alipayElement = screen.getByRole( 'checkbox', { name: 'Alipay', } ); + const affirmElement = screen.getByRole( 'checkbox', { + name: 'Affirm', + } ); const sepaElement = screen.getByRole( 'checkbox', { name: 'Direct debit payment', } ); expect( cardElement ).toBeEnabled(); expect( alipayElement ).not.toBeEnabled(); + expect( affirmElement ).not.toBeEnabled(); expect( sepaElement ).toBeEnabled(); // Card should be first @@ -648,9 +658,17 @@ describe( 'GeneralSettingsSection', () => { Node.DOCUMENT_POSITION_FOLLOWING ); - // SEPA should be before AliPay + // SEPA should be before AliPay and Affirm expect( sepaElement.compareDocumentPosition( alipayElement ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); + expect( sepaElement.compareDocumentPosition( affirmElement ) ).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ); + + // Affirm should be before AliPay + expect( affirmElement.compareDocumentPosition( alipayElement ) ).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ); } ); } ); diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index 74dd86ed46..80d2813ddb 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -25,6 +25,7 @@ import { PAYMENT_METHOD_CARD, PAYMENT_METHOD_GIROPAY, PAYMENT_METHOD_SOFORT, + PAYMENT_METHOD_UNAVAILABLE_REASONS, } from 'wcstripe/stripe-utils/constants'; const List = styled.ul` @@ -136,21 +137,15 @@ const StyledFees = styled( PaymentMethodFeesPill )` * @param {string[]} orderedPaymentMethodIds Ordered payment method IDs. * @return {string[]} Sorted payment method IDs. */ -const usePaymentMethodsSortedByStoreCurrencySupport = ( - orderedPaymentMethodIds -) => { +const usePaymentMethodsSortedByAvailability = ( orderedPaymentMethodIds ) => { const { isUpeEnabled } = useContext( UpeToggleContext ); const storeCurrencyCode = getSetting( 'currency' )?.code; - // When we don't have a store currency or UPE is disabled, we put all methods in the supported list. const sortedPaymentMethodIds = useMemo( () => { - if ( ! storeCurrencyCode || ! isUpeEnabled ) { - return orderedPaymentMethodIds; - } - - const supportedPaymentMethodIds = []; - const unsupportedPaymentMethodIds = []; + const availablePaymentMethodIds = []; + const pluginConflictPaymentMethodIds = []; + const unavailablePaymentMethodIds = []; orderedPaymentMethodIds.forEach( ( paymentMethodId ) => { const unavailableReason = getPaymentMethodUnavailableReason( { @@ -159,13 +154,22 @@ const usePaymentMethodsSortedByStoreCurrencySupport = ( storeCurrencyCode, } ); if ( unavailableReason === null ) { - supportedPaymentMethodIds.push( paymentMethodId ); + availablePaymentMethodIds.push( paymentMethodId ); + } else if ( + unavailableReason === + PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT + ) { + pluginConflictPaymentMethodIds.push( paymentMethodId ); } else { - unsupportedPaymentMethodIds.push( paymentMethodId ); + unavailablePaymentMethodIds.push( paymentMethodId ); } } ); - return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ]; + return [ + ...availablePaymentMethodIds, + ...pluginConflictPaymentMethodIds, + ...unavailablePaymentMethodIds, + ]; }, [ orderedPaymentMethodIds, storeCurrencyCode, isUpeEnabled ] ); return sortedPaymentMethodIds; @@ -235,7 +239,7 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => { setOrderedPaymentMethodIds( newOrderedPaymentMethodIds ); }; - const sortedPaymentMethodIds = usePaymentMethodsSortedByStoreCurrencySupport( + const sortedPaymentMethodIds = usePaymentMethodsSortedByAvailability( availablePaymentMethods ); diff --git a/client/utils/get-payment-method-unavailable-reason.js b/client/utils/get-payment-method-unavailable-reason.js index 48c417aa35..b9fb7fbda6 100644 --- a/client/utils/get-payment-method-unavailable-reason.js +++ b/client/utils/get-payment-method-unavailable-reason.js @@ -20,10 +20,6 @@ const getPaymentMethodUnavailableReason = ( { isUpeEnabled = true, storeCurrencyCode, } ) => { - if ( ! storeCurrencyCode || ! isUpeEnabled ) { - return null; - } - if ( paymentMethodId === PAYMENT_METHOD_KLARNA && window?.wc_stripe_settings_params?.has_klarna_gateway_plugin @@ -38,6 +34,10 @@ const getPaymentMethodUnavailableReason = ( { return PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT; } + if ( ! storeCurrencyCode || ! isUpeEnabled ) { + return null; + } + const paymentMethodCurrencies = getPaymentMethodCurrencies( paymentMethodId, true From f74b2bf71eeaf384ab313a5c783f724ff97ec77f Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Tue, 19 Aug 2025 12:05:36 +0200 Subject: [PATCH 13/13] Fix changelog entry location --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 50113df96d..5cd9c2f73c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -11,13 +11,13 @@ * Tweak - Small improvements to e2e tests * Fix - Fix unnecessary Stripe API calls when rendering subscription details * Add - Adds a new action (`wc_stripe_webhook_received`) to allow additional actions to be taken for webhook notifications from Stripe +* Update - Show all available payment methods before unavailable payment methods = 9.8.1 - 2025-08-15 = * Fix - Remove connection type requirement from PMC sync migration attempt * Fix - Relax customer validation that was preventing payments from the pay for order page * Fix - Prevent the PMC migration to run when the plugin is not connected to Stripe * Fix - Fixes a fatal error in the OC inbox note when the new checkout is disabled -* Update - Show all available payment methods before unavailable payment methods = 9.8.0 - 2025-08-11 = * Add - Adds the current setting value for the Optimized Checkout to the Stripe System Status Report data