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:
parent
8d81b632f2
commit
e5ca2eb011
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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 /> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Add loading placeholder and payment method toggle to block checkout
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue