Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Update Express Payments Loading UI #4228

Merged
merged 32 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c4a6dae
Separate button spinner to separate component for reuse
mikejolley May 18, 2021
df3a5db
Use block checkout spinner in loading mask
mikejolley May 18, 2021
8046fbb
Block pointer events within loading mask
mikejolley May 18, 2021
b89adac
Give the useRef within useShallowEqual a default value
mikejolley May 18, 2021
2a45339
State setter and dispatch are stable
mikejolley May 18, 2021
aaefe62
Prevent re-renders of children when using loading mask.
mikejolley May 18, 2021
1a47376
Use memoization to to prevent excessive express payment rerenders
mikejolley May 18, 2021
e4c006f
Wrap express payment in loading mask
mikejolley May 18, 2021
c88dfa6
Show loading state after submission
mikejolley May 18, 2021
47e005a
remove eslint exclusion
mikejolley May 18, 2021
c28052e
Move spinner to base components so it's available outside of the chec…
mikejolley May 18, 2021
ff03baa
Avoid extra is-loading classname
mikejolley May 18, 2021
a93e800
Update snaps/fix tests
mikejolley May 18, 2021
df6149f
Remove memorization of payment method content due to stale data
mikejolley May 28, 2021
80203c9
Express payment error handling
mikejolley Jun 9, 2021
2647867
Split up payment method context to make it more manageable
mikejolley Jun 11, 2021
05cd597
Add blocking logic to cart
mikejolley Jun 11, 2021
1c0bce4
Update snap
mikejolley Jun 11, 2021
2585395
Restore useRef
mikejolley Jun 11, 2021
3ab9be8
Fix missing function removed by accident
mikejolley Jun 11, 2021
df23858
Fix setActivePaymentMethod and started status (so saved methods still…
mikejolley Jun 11, 2021
8a7e07d
Loading Mask Todo
mikejolley Jun 15, 2021
989f02e
Remove boolean shallow equals
mikejolley Jun 15, 2021
5dc39a8
Missing dep
mikejolley Jun 15, 2021
b8ba959
Memoize typo
mikejolley Jun 15, 2021
fb51e95
Document changes in useStoreEvents
mikejolley Jun 15, 2021
9d34dae
Replace expressPaymentMethodActive
mikejolley Jun 15, 2021
7eb865b
setExpressPaymentError deprecation
mikejolley Jun 15, 2021
407ab91
Only change status if an error is passed
mikejolley Jun 15, 2021
dc19123
Track disabled state via useCheckoutSubmit
mikejolley Jun 15, 2021
c66854a
useCallback on error message functions
mikejolley Jun 16, 2021
a8c4905
Fix mocks in test
mikejolley Jun 16, 2021
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
32 changes: 15 additions & 17 deletions assets/js/base/components/loading-mask/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Spinner } from 'wordpress-components';

/**
* Internal dependencies
*/
import './style.scss';
import Spinner from '../spinner';
mikejolley marked this conversation as resolved.
Show resolved Hide resolved

const LoadingMask = ( {
children,
Expand All @@ -18,29 +18,27 @@ const LoadingMask = ( {
showSpinner = false,
isLoading = true,
} ) => {
// If nothing is loading, just pass through the children.
if ( ! isLoading ) {
return children;
}

return (
<div
className={ classNames(
className,
'wc-block-components-loading-mask'
) }
className={ classNames( className, {
'wc-block-components-loading-mask': isLoading,
} ) }
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
>
{ showSpinner && <Spinner /> }
{ isLoading && showSpinner && <Spinner /> }
<div
className="wc-block-components-loading-mask__children"
aria-hidden={ true }
className={ classNames( {
'wc-block-components-loading-mask__children': isLoading,
} ) }
aria-hidden={ isLoading }
>
{ children }
</div>
<span className="screen-reader-text">
{ screenReaderLabel ||
__( 'Loading…', 'woo-gutenberg-products-block' ) }
</span>
{ isLoading && (
<span className="screen-reader-text">
{ screenReaderLabel ||
__( 'Loading…', 'woo-gutenberg-products-block' ) }
</span>
) }
</div>
);
};
Expand Down
7 changes: 4 additions & 3 deletions assets/js/base/components/loading-mask/style.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.wc-block-components-loading-mask {
position: relative;
min-height: 18px + $gap;
pointer-events: none;
mikejolley marked this conversation as resolved.
Show resolved Hide resolved

.components-spinner {
position: absolute;
Expand All @@ -9,8 +10,8 @@
left: 50%;
transform: translate(-50%, -50%);
}
}

.wc-block-components-loading-mask__children {
opacity: 0.5;
.wc-block-components-loading-mask__children {
opacity: 0.25;
}
}
10 changes: 10 additions & 0 deletions assets/js/base/components/spinner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Internal dependencies
*/
import './style.scss';

const Spinner = (): JSX.Element => {
return <span className="wc-block-components-spinner" aria-hidden="true" />;
};

export default Spinner;
37 changes: 37 additions & 0 deletions assets/js/base/components/spinner/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.wc-block-components-spinner {
position: absolute;
width: 100%;
height: 100%;
color: inherit;
box-sizing: content-box;
text-align: center;
font-size: 1.25em;

&::after {
content: " ";
position: absolute;
top: 50%;
left: 50%;
margin: -0.5em 0 0 -0.5em;
width: 1em;
height: 1em;
box-sizing: border-box;
transform-origin: 50% 50%;
transform: translateZ(0) scale(0.5);
backface-visibility: hidden;
border-radius: 50%;
border: 0.2em solid currentColor;
border-left-color: transparent;
animation: wc-block-components-spinner__animation 1s infinite linear;
}
}

@keyframes wc-block-components-spinner__animation {
0% {
animation-timing-function: cubic-bezier(0.5856, 0.0703, 0.4143, 0.9297);
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import { useShallowEqual } from '@woocommerce/base-hooks';

/**
* Internal dependencies
*/
Expand All @@ -10,12 +15,26 @@ const usePaymentMethodState = ( express = false ) => {
paymentMethodsInitialized,
expressPaymentMethodsInitialized,
} = usePaymentMethodDataContext();
return express
? {
paymentMethods: expressPaymentMethods,
isInitialized: expressPaymentMethodsInitialized,
}
: { paymentMethods, isInitialized: paymentMethodsInitialized };

const currentPaymentMethods = useShallowEqual( paymentMethods );
const currentExpressPaymentMethods = useShallowEqual(
expressPaymentMethods
);
const currentPaymentMethodsInitialized = useShallowEqual(
paymentMethodsInitialized
);
const currentExpressPaymentMethodsInitialized = useShallowEqual(
expressPaymentMethodsInitialized
);
mikejolley marked this conversation as resolved.
Show resolved Hide resolved

return {
paymentMethods: express
? currentExpressPaymentMethods
: currentPaymentMethods,
isInitialized: express
? currentExpressPaymentMethodsInitialized
: currentPaymentMethodsInitialized,
};
};

export const usePaymentMethods = () => usePaymentMethodState();
Expand Down
13 changes: 10 additions & 3 deletions assets/js/base/context/hooks/use-store-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { doAction } from '@wordpress/hooks';
import { useCallback } from '@wordpress/element';
import { useCallback, useRef, useEffect } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -22,6 +22,11 @@ export const useStoreEvents = (): {
dispatchCheckoutEvent: StoreEvent;
} => {
const storeCart = useStoreCart();
const currentStoreCart = useRef( storeCart );

useEffect( () => {
currentStoreCart.current = storeCart;
}, [ storeCart ] );

const dispatchStoreEvent = useCallback( ( eventName, eventParams = {} ) => {
try {
Expand All @@ -39,11 +44,13 @@ export const useStoreEvents = (): {
const dispatchCheckoutEvent = useCallback(
( eventName, eventParams = {} ) => {
try {
const cartParam = currentStoreCart.current;

doAction(
`experimental__woocommerce_blocks-checkout-${ eventName }`,
{
...eventParams,
storeCart,
cartParam,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a significant change and testing notes don't cover it (and afaict there are no automated tests covering this - although I could be mistaken). Should there be some mention of coverage in testing notes and/or add some automated tests around behaviour?

I do see how this improves the memoization of dispatchCheckoutEvent given the high volatility of store cart value. On the surface this seems like there shouldn't be any unintended side-effects of the change given cartParam should always reflect the current store state when dispatchCheckoutEvent is invoked.

Copy link
Member Author

@mikejolley mikejolley Jun 15, 2021

Choose a reason for hiding this comment

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

I know why I did this (it's used as a dependency in various places and I didn't want to trigger the effects when the cart changed).

The change was to store the cart as a ref so the callback function can use the ref instead of using cart as a dependency. I did a quick test to make sure I understand refs and callbacks so it will have the latest cart object available (https://codesandbox.io/s/clever-brook-i8fuy?file=/src/app.js).

I know tests would be good, but I'm still not confident writing tests from scratch and I know it will take me the rest of the week to fathom.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've restored the storeCart parameter name here; that was an oversight on my part. It's still using the ref.

Copy link
Contributor

Choose a reason for hiding this comment

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

I know tests would be good, but I'm still not confident writing tests from scratch and I know it will take me the rest of the week to fathom.

I'm okay with it not blocking the PR, but I do think it'd be worthwhile doing this as a followup. We need more confidence when changing things and automated tests are a good way to get that.

);
} catch ( e ) {
Expand All @@ -52,7 +59,7 @@ export const useStoreEvents = (): {
console.error( e );
}
},
[ storeCart ]
[]
);

return { dispatchStoreEvent, dispatchCheckoutEvent };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,106 +2,77 @@
* Internal dependencies
*/
import { ACTION, STATUS } from './constants';
import type { PaymentMethods } from './types';
import type { PaymentMethods, ExpressPaymentMethods } from './types';

export interface ActionType {
type: ACTION | STATUS;
errorMessage?: string;
paymentMethodData?: Record< string, unknown >;
paymentMethods?: PaymentMethods;
paymentMethods?: PaymentMethods | ExpressPaymentMethods;
shouldSavePaymentMethod?: boolean;
}

/**
* Used to dispatch a status update only for the given type.
* All the actions that can be dispatched for payment methods.
*/
export const statusOnly = ( type: STATUS ): { type: STATUS } => ( { type } );
export const actions = {
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
statusOnly: ( type: STATUS ): { type: STATUS } => ( { type } as const ),
error: ( errorMessage: string ): ActionType =>
( {
type: STATUS.ERROR,
errorMessage,
} as const ),
failed: ( {
errorMessage,
paymentMethodData,
}: {
errorMessage: string;
paymentMethodData: Record< string, unknown >;
} ): ActionType =>
( {
type: STATUS.FAILED,
errorMessage,
paymentMethodData,
} as const ),
success: ( {
paymentMethodData,
}: {
paymentMethodData?: Record< string, unknown >;
} ): ActionType =>
( {
type: STATUS.SUCCESS,
paymentMethodData,
} as const ),
started: ( {
paymentMethodData,
}: {
paymentMethodData?: Record< string, unknown >;
} ): ActionType =>
( {
type: STATUS.STARTED,
paymentMethodData,
} as const ),
setRegisteredPaymentMethods: (
paymentMethods: PaymentMethods
): ActionType =>
( {
type: ACTION.SET_REGISTERED_PAYMENT_METHODS,
paymentMethods,
} as const ),
setRegisteredExpressPaymentMethods: (
paymentMethods: ExpressPaymentMethods
): ActionType =>
( {
type: ACTION.SET_REGISTERED_EXPRESS_PAYMENT_METHODS,
paymentMethods,
} as const ),
setShouldSavePaymentMethod: (
shouldSavePaymentMethod: boolean
): ActionType =>
( {
type: ACTION.SET_SHOULD_SAVE_PAYMENT_METHOD,
shouldSavePaymentMethod,
} as const ),
};

/**
* Used to dispatch an error message along with setting current payment status to ERROR.
*
* @param {string} errorMessage Whatever error message accompanying the error condition.
* @return {ActionType} The action object.
*/
export const error = ( errorMessage: string ): ActionType => ( {
type: STATUS.ERROR,
errorMessage,
} );

/**
* Used to dispatch a payment failed status update.
*/
export const failed = ( {
errorMessage,
paymentMethodData,
}: {
errorMessage: string;
paymentMethodData: Record< string, unknown >;
} ): ActionType => ( {
type: STATUS.FAILED,
errorMessage,
paymentMethodData,
} );

/**
* Used to dispatch a payment success status update.
*/
export const success = ( {
paymentMethodData,
}: {
paymentMethodData?: Record< string, unknown >;
} ): ActionType => ( {
type: STATUS.SUCCESS,
paymentMethodData,
} );

/**
* Used to dispatch a payment started status update.
*/
export const started = ( {
paymentMethodData,
}: {
paymentMethodData?: Record< string, unknown >;
} ): ActionType => ( {
type: STATUS.STARTED,
paymentMethodData,
} );

/**
* Used to dispatch an action for updating a registered payment method in the state.
*
* @param {Object} paymentMethods Payment methods to register.
* @return {Object} An action object.
*/
export const setRegisteredPaymentMethods = (
paymentMethods: PaymentMethods
): ActionType => ( {
type: ACTION.SET_REGISTERED_PAYMENT_METHODS,
paymentMethods,
} );

/**
* Used to dispatch an action for updating a registered express payment method in the state.
*
* @param {Object} paymentMethods Payment methods to register.
* @return {Object} An action object.
*/
export const setRegisteredExpressPaymentMethods = (
paymentMethods: PaymentMethods
): ActionType => ( {
type: ACTION.SET_REGISTERED_EXPRESS_PAYMENT_METHODS,
paymentMethods,
} );

/**
* Set a flag indicating that the payment method info (e.g. a payment card) should be saved to user account after order completion.
*
* @param {boolean} shouldSavePaymentMethod
* @return {Object} An action object.
*/
export const setShouldSavePaymentMethod = (
shouldSavePaymentMethod: boolean
): ActionType => ( {
type: ACTION.SET_SHOULD_SAVE_PAYMENT_METHOD,
shouldSavePaymentMethod,
} );
export default actions;
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const DEFAULT_PAYMENT_DATA_CONTEXT_STATE: PaymentMethodDataContextState =

export const DEFAULT_PAYMENT_METHOD_DATA: PaymentMethodDataContextType = {
setPaymentStatus: () => ( {
pristine: () => void null,
started: () => void null,
processing: () => void null,
completed: () => void null,
Expand All @@ -54,6 +55,7 @@ export const DEFAULT_PAYMENT_METHOD_DATA: PaymentMethodDataContextType = {
hasError: false,
hasFailed: false,
isSuccessful: false,
isDoingExpressPayment: false,
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
},
paymentStatuses: STATUS,
paymentMethodData: {},
Expand Down
Loading