diff --git a/changelog.txt b/changelog.txt
index ad2a48a1ba..5cd9c2f73c 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -11,6 +11,7 @@
* 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
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/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/__tests__/general-settings-section.test.js b/client/settings/general-settings-section/__tests__/general-settings-section.test.js
index 2defb652e2..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
@@ -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';
@@ -12,15 +13,17 @@ import {
useGetOrderedPaymentMethodIds,
useIsPMCEnabled,
} from 'wcstripe/data';
-import { usePaymentMethodCurrencies } 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_AFFIRM,
PAYMENT_METHOD_ALIPAY,
PAYMENT_METHOD_CARD,
PAYMENT_METHOD_EPS,
PAYMENT_METHOD_LINK,
PAYMENT_METHOD_SEPA,
PAYMENT_METHOD_SOFORT,
+ PAYMENT_METHOD_UNAVAILABLE_REASONS,
} from 'wcstripe/stripe-utils/constants';
jest.mock( 'wcstripe/data', () => ( {
@@ -34,13 +37,14 @@ jest.mock( 'wcstripe/data', () => ( {
useGetOrderedPaymentMethodIds: jest.fn(),
useIsPMCEnabled: jest.fn(),
} ) );
-jest.mock( 'utils/use-payment-method-currencies', () => ( {
- usePaymentMethodCurrencies: jest.fn().mockReturnValue( [] ),
-} ) );
+jest.mock( 'utils/get-payment-method-unavailable-reason' );
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(),
@@ -58,14 +62,25 @@ 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',
alipay_payments: 'active',
} );
useManualCapture.mockReturnValue( [ false ] );
+ getPaymentMethodUnavailableReason.mockReturnValue( null );
useGetAvailablePaymentMethodIds.mockReturnValue( [
PAYMENT_METHOD_CARD,
PAYMENT_METHOD_LINK,
@@ -333,8 +348,8 @@ describe( 'GeneralSettingsSection', () => {
expect( updateEnabledMethodsMock ).toHaveBeenCalled();
} );
- it( 'does not display the payment method checkbox when currency is not supprted', () => {
- global.wcSettings = { currency: { code: 'USD' } };
+ it( 'does not display the payment method checkbox when currency is not supported', () => {
+ mockCurrencyCode( 'USD' );
useGetAvailablePaymentMethodIds.mockReturnValue( [
PAYMENT_METHOD_CARD,
PAYMENT_METHOD_ALIPAY,
@@ -515,16 +530,31 @@ 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', () => {
+ useEnabledPaymentMethodIds.mockReturnValue( [
+ [ PAYMENT_METHOD_CARD ],
+ ] );
useGetAvailablePaymentMethodIds.mockReturnValue( [
PAYMENT_METHOD_CARD,
PAYMENT_METHOD_ALIPAY,
] );
- useEnabledPaymentMethodIds.mockReturnValue( [
- [ PAYMENT_METHOD_CARD ],
- ] );
- usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] );
- window.wcSettings = { currency: { code: 'EUR' } };
+ 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(
@@ -535,19 +565,19 @@ 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 when currency is supported', () => {
- useGetAvailablePaymentMethodIds.mockReturnValue( [
- PAYMENT_METHOD_CARD,
- PAYMENT_METHOD_ALIPAY,
- ] );
- useEnabledPaymentMethodIds.mockReturnValue( [
- [ PAYMENT_METHOD_CARD ],
- ] );
- usePaymentMethodCurrencies.mockReturnValue( [ 'USD' ] );
- window.wcSettings = { currency: { code: 'USD' } };
+ it( 'should enable the payment method checkbox and not show the requires currency notice when currency is supported', () => {
+ mockCurrencyCode( 'USD' );
render(
@@ -559,5 +589,86 @@ describe( 'GeneralSettingsSection', () => {
name: /Credit card/,
} )
).toBeEnabled();
+
+ expect(
+ screen.queryByText( 'Requires currency' )
+ ).not.toBeInTheDocument();
+ } );
+
+ 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(),
+ saveOrderedPaymentMethodIds: jest.fn(),
+ } );
+
+ getPaymentMethodUnavailableReason.mockImplementation(
+ ( { paymentMethodId } ) => {
+ 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;
+ }
+ );
+ mockCurrencyCode( 'EUR' );
+
+ render(
+
+
+
+ );
+
+ const cardElement = screen.getByRole( 'checkbox', {
+ name: /Credit card/,
+ } );
+ 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
+ expect( cardElement.compareDocumentPosition( alipayElement ) ).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect( cardElement.compareDocumentPosition( sepaElement ) ).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+
+ // 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-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;
diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js
index e1ad8b6e6f..80d2813ddb 100644
--- a/client/settings/general-settings-section/payment-methods-list.js
+++ b/client/settings/general-settings-section/payment-methods-list.js
@@ -1,14 +1,17 @@
/* global wc_stripe_settings_params */
+import { getSetting } from '@woocommerce/settings';
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 getPaymentMethodUnavailableReason from 'utils/get-payment-method-unavailable-reason';
import {
useEnabledPaymentMethodIds,
useGetOrderedPaymentMethodIds,
@@ -22,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`
@@ -126,6 +130,51 @@ 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 usePaymentMethodsSortedByAvailability = ( orderedPaymentMethodIds ) => {
+ const { isUpeEnabled } = useContext( UpeToggleContext );
+
+ const storeCurrencyCode = getSetting( 'currency' )?.code;
+
+ const sortedPaymentMethodIds = useMemo( () => {
+ const availablePaymentMethodIds = [];
+ const pluginConflictPaymentMethodIds = [];
+ const unavailablePaymentMethodIds = [];
+
+ orderedPaymentMethodIds.forEach( ( paymentMethodId ) => {
+ const unavailableReason = getPaymentMethodUnavailableReason( {
+ paymentMethodId,
+ isUpeEnabled,
+ storeCurrencyCode,
+ } );
+ if ( unavailableReason === null ) {
+ availablePaymentMethodIds.push( paymentMethodId );
+ } else if (
+ unavailableReason ===
+ PAYMENT_METHOD_UNAVAILABLE_REASONS.OFFICIAL_PLUGIN_CONFLICT
+ ) {
+ pluginConflictPaymentMethodIds.push( paymentMethodId );
+ } else {
+ unavailablePaymentMethodIds.push( paymentMethodId );
+ }
+ } );
+
+ return [
+ ...availablePaymentMethodIds,
+ ...pluginConflictPaymentMethodIds,
+ ...unavailablePaymentMethodIds,
+ ];
+ }, [ orderedPaymentMethodIds, storeCurrencyCode, isUpeEnabled ] );
+
+ return sortedPaymentMethodIds;
+};
+
/**
* Formats the payment method description with the account default currency.
*
@@ -190,13 +239,17 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => {
setOrderedPaymentMethodIds( newOrderedPaymentMethodIds );
};
+ const sortedPaymentMethodIds = usePaymentMethodsSortedByAvailability(
+ 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 +311,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/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/__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 );
+ } );
+} );
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..b9fb7fbda6
--- /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 (
+ 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;
+ }
+
+ if ( ! storeCurrencyCode || ! isUpeEnabled ) {
+ return null;
+ }
+
+ 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-currencies.js b/client/utils/use-payment-method-currencies.js
index afe7a29876..ec8871fa87 100644
--- a/client/utils/use-payment-method-currencies.js
+++ b/client/utils/use-payment-method-currencies.js
@@ -230,9 +230,15 @@ const getAmazonPayCurrencies = () => {
}
};
-export const usePaymentMethodCurrencies = ( paymentMethodId ) => {
- const { isUpeEnabled } = useContext( UpeToggleContext );
-
+/**
+ * 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:
return getAliPayCurrencies( isUpeEnabled );
@@ -247,4 +253,17 @@ export const usePaymentMethodCurrencies = ( paymentMethodId ) => {
}
};
+/**
+ * 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 );
+
+ return getPaymentMethodCurrencies( paymentMethodId, isUpeEnabled );
+};
+
export default usePaymentMethodCurrencies;
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;
diff --git a/readme.txt b/readme.txt
index c52dd85d93..43a7d09b77 100644
--- a/readme.txt
+++ b/readme.txt
@@ -119,7 +119,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Fix - Ensure all Javascript strings use the correct text domain for translation
* Tweak - Use more specific selector in express checkout e2e tests
* 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
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).