Skip to content

Show unsupported payment methods at the end of the payment method list #4523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c2565ec
Show payment methods without currency support at the end of the payme…
daledupreez Jul 24, 2025
dcaba9a
Add JSDoc
daledupreez Jul 24, 2025
7e0455d
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Jul 24, 2025
7377fbf
Memoize and refactor sorting
daledupreez Jul 24, 2025
b3956c5
Fix JS unit tests
daledupreez Jul 24, 2025
4b1506e
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Jul 31, 2025
bf373e3
Add changelog
daledupreez Jul 31, 2025
f1b8d38
Use getSetting() instead of global reference; rename variable
daledupreez Jul 31, 2025
c966d88
Update unit tests to mock getSetting() and centralise currency mocking
daledupreez Jul 31, 2025
9f3e05e
Return early when UPE is disabled; update notes
daledupreez Jul 31, 2025
a0f6d46
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Aug 14, 2025
f3a981a
Refactor logic into central helper function
daledupreez Aug 15, 2025
28c4eb3
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Aug 15, 2025
a2230e5
Update unit tests
daledupreez Aug 15, 2025
3089fce
Add tests for getPaymentMethodUnavailableReason
daledupreez Aug 15, 2025
a13b5c6
Sort plugin conflicts before currency issues
daledupreez Aug 18, 2025
8416544
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Aug 18, 2025
0e5a1fe
Merge branch 'develop' into try/relevance-sort-in-payment-method-page
daledupreez Aug 19, 2025
f74b2bf
Fix changelog entry location
daledupreez Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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(
<UpeToggleContext.Provider value={ { isUpeEnabled: true } }>
<GeneralSettingsSection />
</UpeToggleContext.Provider>
);

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
);
} );
} );
63 changes: 59 additions & 4 deletions client/settings/general-settings-section/payment-methods-list.js
Original file line number Diff line number Diff line change
@@ -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 { getPaymentMethodCurrencies } from 'utils/use-payment-method-currencies';
import {
useEnabledPaymentMethodIds,
useGetOrderedPaymentMethodIds,
Expand Down Expand Up @@ -126,6 +129,54 @@ 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 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 ( ! storeCurrencyCode ) {
return orderedPaymentMethodIds;
}

const supportedPaymentMethodIds = [];
const unsupportedPaymentMethodIds = [];

orderedPaymentMethodIds.forEach( ( paymentMethodId ) => {
const paymentMethodCurrencies = getPaymentMethodCurrencies(
paymentMethodId,
isUpeEnabled
);

if (
paymentMethodCurrencies.length === 0 ||
paymentMethodCurrencies.includes( storeCurrencyCode )
) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is this check sufficient? Are there more conditions we should be using (or attaching to the data) to push payment methods into the trailing section?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also the case of payment methods disabled because of a plugin conflict (Affirm/Klarna):

Screenshot 2025-07-31 at 11 21 31

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am torn on this one, to be honest. If they disable the other payment gateway, the payment method will be available immediately, so it could "move" unexpectedly for that case. So that case feels subtly different to me, and feels like adding it could lead to even weirder UX. 😐

supportedPaymentMethodIds.push( paymentMethodId );
} else {
unsupportedPaymentMethodIds.push( paymentMethodId );
}
} );

return [ ...supportedPaymentMethodIds, ...unsupportedPaymentMethodIds ];
}, [ orderedPaymentMethodIds, storeCurrencyCode, isUpeEnabled ] );

return sortedPaymentMethodIds;
};

/**
* Formats the payment method description with the account default currency.
*
Expand Down Expand Up @@ -190,13 +241,17 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => {
setOrderedPaymentMethodIds( newOrderedPaymentMethodIds );
};

const sortedPaymentMethodIds = usePaymentMethodsSortedByStoreCurrencySupport(
availablePaymentMethods
);

return isChangingDisplayOrder ? (
<DraggableList
axis="y"
values={ availablePaymentMethods }
values={ sortedPaymentMethodIds }
onReorder={ onReorder }
>
{ availablePaymentMethods.map( ( method ) => {
{ sortedPaymentMethodIds.map( ( method ) => {
// Skip giropay as it was deprecated by Jun, 30th 2024.
if ( method === PAYMENT_METHOD_GIROPAY ) {
return null;
Expand Down Expand Up @@ -258,7 +313,7 @@ const GeneralSettingsSection = ( { isChangingDisplayOrder } ) => {
</DraggableList>
) : (
<List>
{ availablePaymentMethods.map( ( method ) => {
{ sortedPaymentMethodIds.map( ( method ) => {
// Skip giropay as it was deprecated by Jun, 30th 2024.
if ( method === PAYMENT_METHOD_GIROPAY ) {
return null;
Expand Down
25 changes: 22 additions & 3 deletions client/utils/use-payment-method-currencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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;
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Loading