Checkout: Add loading placeholder and payment method toggle (#52044)

* Create loading state for payment methods on block checkout

* Tidy and type fix checkPaymentMethodsCanPay

* Disable place order button before payments are Initialized

* Toggle and loading state

* changelog

* Reduce scope of change in check payment methods

* saved methods are hidden when empty
This commit is contained in:
Mike Jolley 2024-10-29 11:45:40 +00:00 committed by GitHub
parent 8d81b632f2
commit e5ca2eb011
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 187 additions and 96 deletions

View File

@ -33,17 +33,19 @@ export const useCheckoutSubmit = () => {
hasError: store.hasError(),
};
} );
const { activePaymentMethod, isExpressPaymentMethodActive } = useSelect(
( select ) => {
const store = select( PAYMENT_STORE_KEY );
const {
activePaymentMethod,
isExpressPaymentMethodActive,
isPaymentMethodsInitialized,
} = useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activePaymentMethod: store.getActivePaymentMethod(),
isExpressPaymentMethodActive:
store.isExpressPaymentMethodActive(),
};
}
);
return {
activePaymentMethod: store.getActivePaymentMethod(),
isExpressPaymentMethodActive: store.isExpressPaymentMethodActive(),
isPaymentMethodsInitialized: store.paymentMethodsInitialized(),
};
} );
const { onSubmit } = useCheckoutEventsContext();
@ -58,7 +60,10 @@ export const useCheckoutSubmit = () => {
paymentMethodButtonLabel,
onSubmit,
isCalculating,
isDisabled: isProcessing || isExpressPaymentMethodActive,
isDisabled:
isProcessing ||
isExpressPaymentMethodActive ||
! isPaymentMethodsInitialized,
waitingForProcessing,
waitingForRedirect,
};

View File

@ -11,6 +11,7 @@ import clsx from 'clsx';
import { RadioControlAccordion } from '@woocommerce/blocks-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { getPaymentMethods } from '@woocommerce/blocks-registry';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
@ -28,7 +29,6 @@ const PaymentMethodOptions = () => {
const {
activeSavedToken,
activePaymentMethod,
isExpressPaymentMethodActive,
savedPaymentMethods,
availablePaymentMethods,
} = useSelect( ( select ) => {
@ -36,7 +36,6 @@ const PaymentMethodOptions = () => {
return {
activeSavedToken: store.getActiveSavedToken(),
activePaymentMethod: store.getActivePaymentMethod(),
isExpressPaymentMethodActive: store.isExpressPaymentMethodActive(),
savedPaymentMethods: store.getSavedPaymentMethods(),
availablePaymentMethods: store.getAvailablePaymentMethods(),
};
@ -62,7 +61,9 @@ const PaymentMethodOptions = () => {
} ),
name: `wc-saved-payment-method-token-${ name }`,
content: (
<PaymentMethodCard showSaveOption={ supports.showSaveOption }>
<PaymentMethodCard
showSaveOption={ !! supports.showSaveOption }
>
{ cloneElement( component, {
__internalSetActivePaymentMethod,
...paymentMethodInterface,
@ -94,7 +95,22 @@ const PaymentMethodOptions = () => {
const singleOptionClass = clsx( {
'disable-radio-control': isSinglePaymentMethod,
} );
return isExpressPaymentMethodActive ? null : (
const globalPaymentMethods = getSetting( 'globalPaymentMethods' );
if ( Object.keys( options ).length === 0 ) {
return (
<div
className="wc-payment-method-options-placeholder"
style={ {
minHeight:
Object.keys( globalPaymentMethods ).length * 3 + 'em',
} }
></div>
);
}
return (
<RadioControlAccordion
highlightChecked={ true }
id={ 'wc-payment-method-options' }

View File

@ -2,9 +2,10 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Label } from '@woocommerce/blocks-components';
import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
import { Button } from '@ariakit/react';
/**
* Internal dependencies
@ -20,19 +21,31 @@ import './style.scss';
* @return {*} The rendered component.
*/
const PaymentMethods = () => {
const [ showPaymentMethodsToggle, setShowPaymentMethodsToggle ] =
useState( false );
const {
paymentMethodsInitialized,
availablePaymentMethods,
savedPaymentMethods,
hasSavedPaymentMethods,
isExpressPaymentMethodActive,
activeSavedToken,
} = useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
paymentMethodsInitialized: store.paymentMethodsInitialized(),
activeSavedToken: store.getActiveSavedToken(),
availablePaymentMethods: store.getAvailablePaymentMethods(),
savedPaymentMethods: store.getSavedPaymentMethods(),
hasSavedPaymentMethods:
Object.keys( store.getSavedPaymentMethods() || {} ).length > 0,
isExpressPaymentMethodActive: store.isExpressPaymentMethodActive(),
};
} );
// If using an express payment method, don't show the regular payment methods.
if ( isExpressPaymentMethodActive ) {
return null;
}
if (
paymentMethodsInitialized &&
Object.keys( availablePaymentMethods ).length === 0
@ -40,25 +53,43 @@ const PaymentMethods = () => {
return <NoPaymentMethods />;
}
// Show payment methods if the toggle is on or if there are no saved payment methods, or if the active saved token is not set.
const showPaymentMethods =
showPaymentMethodsToggle ||
! hasSavedPaymentMethods ||
( paymentMethodsInitialized && ! activeSavedToken );
return (
<>
<SavedPaymentMethodOptions />
{ Object.keys( savedPaymentMethods ).length > 0 && (
<Label
label={ __( 'Use another payment method.', 'woocommerce' ) }
screenReaderLabel={ __(
'Other available payment methods',
'woocommerce'
) }
wrapperElement="p"
wrapperProps={ {
className: [
'wc-block-components-checkout-step__description wc-block-components-checkout-step__description-payments-aligned',
],
} }
/>
{ hasSavedPaymentMethods && (
<>
<SavedPaymentMethodOptions />
<p className="wc-block-components-checkout-step__description wc-block-components-checkout-step__description-payments-aligned">
<Button
render={ <span /> }
type="button"
className="wc-block-components-show-payment-methods__link"
onClick={ ( e ) => {
e.preventDefault();
setShowPaymentMethodsToggle(
! showPaymentMethodsToggle
);
} }
aria-label={ __(
'Use another payment method',
'woocommerce'
) }
aria-expanded={ showPaymentMethodsToggle }
>
{ __(
'Use another payment method',
'woocommerce'
) }
</Button>
</p>
</>
) }
<PaymentMethodOptions />
{ showPaymentMethods && <PaymentMethodOptions /> }
</>
);
};

View File

@ -69,15 +69,20 @@ const getDefaultLabel = ( {
};
const SavedPaymentMethodOptions = () => {
const { activeSavedToken, activePaymentMethod, savedPaymentMethods } =
useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activeSavedToken: store.getActiveSavedToken(),
activePaymentMethod: store.getActivePaymentMethod(),
savedPaymentMethods: store.getSavedPaymentMethods(),
};
} );
const {
activeSavedToken,
activePaymentMethod,
savedPaymentMethods,
paymentMethodsInitialized,
} = useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activeSavedToken: store.getActiveSavedToken(),
activePaymentMethod: store.getActivePaymentMethod(),
savedPaymentMethods: store.getSavedPaymentMethods(),
paymentMethodsInitialized: store.paymentMethodsInitialized(),
};
} );
const { __internalSetActivePaymentMethod } =
useDispatch( PAYMENT_STORE_KEY );
const canMakePaymentArg = getCanMakePaymentArg();
@ -153,6 +158,7 @@ const SavedPaymentMethodOptions = () => {
dispatchCheckoutEvent,
canMakePaymentArg,
] );
const savedPaymentMethodHandler =
!! activeSavedToken &&
paymentMethods[ activePaymentMethod ] &&
@ -167,12 +173,16 @@ const SavedPaymentMethodOptions = () => {
)
: null;
const selected = paymentMethodsInitialized
? activeSavedToken
: options[ 0 ].value;
return options.length > 0 ? (
<>
<RadioControl
highlightChecked={ true }
id={ 'wc-payment-method-saved-tokens' }
selected={ activeSavedToken }
selected={ selected }
options={ options }
onChange={ () => void 0 }
/>

View File

@ -149,6 +149,27 @@
pointer-events: all; // Overrides parent disabled component in editor context
}
.wc-payment-method-options-placeholder {
display: block;
height: 100px;
width: 100%;
@include placeholder();
}
.wc-block-components-show-payment-methods__link {
font-weight: normal;
background: none;
border: none;
padding: 0;
text-decoration: underline;
cursor: pointer;
text-align: left;
&[aria-expanded="true"] {
text-decoration: none;
}
}
.is-mobile,
.is-small {
.wc-block-card-elements {

View File

@ -173,13 +173,6 @@ describe( 'PaymentMethods', () => {
</>
);
await waitFor( () => {
const savedPaymentMethodOptions = screen.queryByText(
/Saved payment method options/
);
expect( savedPaymentMethodOptions ).not.toBeNull();
} );
await waitFor( () => {
const paymentMethodOptions = screen.queryByText(
/Payment method options/

View File

@ -5,6 +5,8 @@ import {
CanMakePaymentArgument,
ExpressPaymentMethodConfigInstance,
PaymentMethodConfigInstance,
PlainExpressPaymentMethods,
PlainPaymentMethods,
} from '@woocommerce/types';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
import { dispatch, select } from '@wordpress/data';
@ -151,8 +153,23 @@ const registrationErrorNotice = (
} );
};
const compareAvailablePaymentMethods = (
paymentMethods: PlainPaymentMethods | PlainExpressPaymentMethods,
availablePaymentMethods: PlainPaymentMethods | PlainExpressPaymentMethods
) => {
const compareKeys1 = Object.keys( paymentMethods );
const compareKeys2 = Object.keys( availablePaymentMethods );
return (
compareKeys1.length === compareKeys2.length &&
compareKeys1.every( ( current ) => compareKeys2.includes( current ) )
);
};
export const checkPaymentMethodsCanPay = async ( express = false ) => {
let availablePaymentMethods = {};
const availablePaymentMethods:
| PlainPaymentMethods
| PlainExpressPaymentMethods = {};
const paymentMethods = express
? getExpressPaymentMethods()
@ -167,30 +184,24 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
const { name, title, description, gatewayId, supports } =
paymentMethod as ExpressPaymentMethodConfigInstance;
availablePaymentMethods = {
...availablePaymentMethods,
[ paymentMethod.name ]: {
name,
title,
description,
gatewayId,
supportsStyle: supports?.style,
},
availablePaymentMethods[ name ] = {
name,
title,
description,
gatewayId,
supportsStyle: supports?.style || [],
};
} else {
const { name } = paymentMethod as PaymentMethodConfigInstance;
availablePaymentMethods = {
...availablePaymentMethods,
[ paymentMethod.name ]: {
name,
},
availablePaymentMethods[ name ] = {
name,
};
}
};
// Order payment methods.
const paymentMethodsOrder = express
const sortedPaymentMethods = express
? Object.keys( paymentMethods )
: Array.from(
new Set( [
@ -202,9 +213,9 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
const cartPaymentMethods = canPayArgument.paymentMethods as string[];
const isEditor = !! select( 'core/editor' );
for ( let i = 0; i < paymentMethodsOrder.length; i++ ) {
const paymentMethodName = paymentMethodsOrder[ i ];
const paymentMethod = paymentMethods[ paymentMethodName ];
for ( let i = 0; i < sortedPaymentMethods.length; i++ ) {
const paymentMethodName = sortedPaymentMethods[ i ];
const paymentMethod = paymentMethods[ paymentMethodName ] || {};
if ( ! paymentMethod ) {
continue;
@ -224,7 +235,7 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
) );
if ( canPay ) {
if ( typeof canPay === 'object' && canPay.error ) {
if ( typeof canPay === 'object' && canPay?.error ) {
throw new Error( canPay.error.message );
}
addAvailablePaymentMethod( paymentMethod );
@ -236,31 +247,31 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
}
}
const availablePaymentMethodNames = Object.keys( availablePaymentMethods );
const currentlyAvailablePaymentMethods = express
const currentPaymentMethods = express
? select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods()
: select( PAYMENT_STORE_KEY ).getAvailablePaymentMethods();
if (
Object.keys( currentlyAvailablePaymentMethods ).length ===
availablePaymentMethodNames.length &&
Object.keys( currentlyAvailablePaymentMethods ).every( ( current ) =>
availablePaymentMethodNames.includes( current )
! compareAvailablePaymentMethods(
availablePaymentMethods,
currentPaymentMethods
)
) {
// All the names are the same, no need to dispatch more actions.
return true;
const {
__internalSetAvailablePaymentMethods,
__internalSetAvailableExpressPaymentMethods,
} = dispatch( PAYMENT_STORE_KEY );
if ( express ) {
__internalSetAvailableExpressPaymentMethods(
availablePaymentMethods as PlainExpressPaymentMethods
);
} else {
__internalSetAvailablePaymentMethods(
availablePaymentMethods as PlainPaymentMethods
);
}
}
const {
__internalSetAvailablePaymentMethods,
__internalSetAvailableExpressPaymentMethods,
} = dispatch( PAYMENT_STORE_KEY );
const setCallback = express
? __internalSetAvailableExpressPaymentMethods
: __internalSetAvailablePaymentMethods;
setCallback( availablePaymentMethods );
return true;
};

View File

@ -150,7 +150,12 @@ export type PaymentMethods =
/**
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
*/
export type PlainPaymentMethods = Record<
export type PlainPaymentMethods = Record< string, { name: string } >;
/**
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
*/
export type PlainExpressPaymentMethods = Record<
string,
{
name: string;
@ -161,11 +166,6 @@ export type PlainPaymentMethods = Record<
}
>;
/**
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
*/
export type PlainExpressPaymentMethods = PlainPaymentMethods;
export type ExpressPaymentMethods =
| Record< string, ExpressPaymentMethodConfigInstance >
| EmptyObjectType;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Add loading placeholder and payment method toggle to block checkout

View File

@ -410,7 +410,7 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
if ( ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
// These are used to show options in the sidebar. We want to get the full list of enabled payment methods,
// not just the ones that are available for the current cart (which may not exist yet).
$payment_methods = $this->get_enabled_payment_gateways();