initial commit
This commit is contained in:
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useExpressPaymentMethods,
|
||||
usePaymentMethodInterface,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import {
|
||||
useEditorContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PaymentMethodErrorBoundary from './payment-method-error-boundary';
|
||||
|
||||
const ExpressPaymentMethods = () => {
|
||||
const { isEditor } = useEditorContext();
|
||||
const {
|
||||
setActivePaymentMethod,
|
||||
setExpressPaymentError,
|
||||
activePaymentMethod,
|
||||
paymentMethodData,
|
||||
setPaymentStatus,
|
||||
} = usePaymentMethodDataContext();
|
||||
const paymentMethodInterface = usePaymentMethodInterface();
|
||||
const { paymentMethods } = useExpressPaymentMethods();
|
||||
const previousActivePaymentMethod = useRef( activePaymentMethod );
|
||||
const previousPaymentMethodData = useRef( paymentMethodData );
|
||||
|
||||
/**
|
||||
* onExpressPaymentClick should be triggered when the express payment button is clicked.
|
||||
*
|
||||
* This will store the previous active payment method, set the express method as active, and set the payment status to started.
|
||||
*/
|
||||
const onExpressPaymentClick = useCallback(
|
||||
( paymentMethodId ) => () => {
|
||||
previousActivePaymentMethod.current = activePaymentMethod;
|
||||
previousPaymentMethodData.current = paymentMethodData;
|
||||
setPaymentStatus().started( {} );
|
||||
setActivePaymentMethod( paymentMethodId );
|
||||
},
|
||||
[
|
||||
activePaymentMethod,
|
||||
paymentMethodData,
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* onExpressPaymentClose should be triggered when the express payment process is cancelled or closed.
|
||||
*
|
||||
* This restores the active method and returns the state to pristine.
|
||||
*/
|
||||
const onExpressPaymentClose = useCallback( () => {
|
||||
setPaymentStatus().pristine();
|
||||
setActivePaymentMethod( previousActivePaymentMethod.current );
|
||||
if ( previousPaymentMethodData.current.isSavedToken ) {
|
||||
setPaymentStatus().started( previousPaymentMethodData.current );
|
||||
}
|
||||
}, [ setActivePaymentMethod, setPaymentStatus ] );
|
||||
|
||||
/**
|
||||
* onExpressPaymentError should be triggered when the express payment process errors.
|
||||
*
|
||||
* This shows an error message then restores the active method and returns the state to pristine.
|
||||
*/
|
||||
const onExpressPaymentError = useCallback(
|
||||
( errorMessage ) => {
|
||||
setPaymentStatus().error( errorMessage );
|
||||
setExpressPaymentError( errorMessage );
|
||||
setActivePaymentMethod( previousActivePaymentMethod.current );
|
||||
if ( previousPaymentMethodData.current.isSavedToken ) {
|
||||
setPaymentStatus().started( previousPaymentMethodData.current );
|
||||
}
|
||||
},
|
||||
[ setActivePaymentMethod, setPaymentStatus, setExpressPaymentError ]
|
||||
);
|
||||
|
||||
/**
|
||||
* Calling setExpressPaymentError directly is deprecated.
|
||||
*/
|
||||
const deprecatedSetExpressPaymentError = useCallback(
|
||||
( errorMessage = '' ) => {
|
||||
deprecated(
|
||||
'Express Payment Methods should use the provided onError handler instead.',
|
||||
{
|
||||
alternative: 'onError',
|
||||
plugin: 'woocommerce-gutenberg-products-block',
|
||||
link:
|
||||
'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4228',
|
||||
}
|
||||
);
|
||||
if ( errorMessage ) {
|
||||
onExpressPaymentError( errorMessage );
|
||||
} else {
|
||||
setExpressPaymentError( '' );
|
||||
}
|
||||
},
|
||||
[ setExpressPaymentError, onExpressPaymentError ]
|
||||
);
|
||||
|
||||
/**
|
||||
* @todo Find a way to Memoize Express Payment Method Content
|
||||
*
|
||||
* Payment method content could potentially become a bottleneck if lots of logic is ran in the content component. It
|
||||
* Currently re-renders excessively but is not easy to useMemo because paymentMethodInterface could become stale.
|
||||
* paymentMethodInterface itself also updates on most renders.
|
||||
*/
|
||||
const entries = Object.entries( paymentMethods );
|
||||
const content =
|
||||
entries.length > 0 ? (
|
||||
entries.map( ( [ id, paymentMethod ] ) => {
|
||||
const expressPaymentMethod = isEditor
|
||||
? paymentMethod.edit
|
||||
: paymentMethod.content;
|
||||
return isValidElement( expressPaymentMethod ) ? (
|
||||
<li key={ id } id={ `express-payment-method-${ id }` }>
|
||||
{ cloneElement( expressPaymentMethod, {
|
||||
...paymentMethodInterface,
|
||||
onClick: onExpressPaymentClick( id ),
|
||||
onClose: onExpressPaymentClose,
|
||||
onError: onExpressPaymentError,
|
||||
setExpressPaymentError: deprecatedSetExpressPaymentError,
|
||||
} ) }
|
||||
</li>
|
||||
) : null;
|
||||
} )
|
||||
) : (
|
||||
<li key="noneRegistered">
|
||||
{ __(
|
||||
'No registered Payment Methods',
|
||||
'woocommerce'
|
||||
) }
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<PaymentMethodErrorBoundary isEditor={ isEditor }>
|
||||
<ul className="wc-block-components-express-payment__event-buttons">
|
||||
{ content }
|
||||
</ul>
|
||||
</PaymentMethodErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpressPaymentMethods;
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useEmitResponse,
|
||||
useExpressPaymentMethods,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
StoreNoticesProvider,
|
||||
useCheckoutContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ExpressPaymentMethods from '../express-payment-methods';
|
||||
import './style.scss';
|
||||
|
||||
const CartExpressPayment = () => {
|
||||
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
||||
const { noticeContexts } = useEmitResponse();
|
||||
const {
|
||||
isCalculating,
|
||||
isProcessing,
|
||||
isAfterProcessing,
|
||||
isBeforeProcessing,
|
||||
isComplete,
|
||||
hasError,
|
||||
} = useCheckoutContext();
|
||||
const { currentStatus: paymentStatus } = usePaymentMethodDataContext();
|
||||
|
||||
if (
|
||||
! isInitialized ||
|
||||
( isInitialized && Object.keys( paymentMethods ).length === 0 )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set loading state for express payment methods when payment or checkout is in progress.
|
||||
const checkoutProcessing =
|
||||
isProcessing ||
|
||||
isAfterProcessing ||
|
||||
isBeforeProcessing ||
|
||||
( isComplete && ! hasError );
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingMask
|
||||
isLoading={
|
||||
isCalculating ||
|
||||
checkoutProcessing ||
|
||||
paymentStatus.isDoingExpressPayment
|
||||
}
|
||||
>
|
||||
<div className="wc-block-components-express-payment wc-block-components-express-payment--cart">
|
||||
<div className="wc-block-components-express-payment__content">
|
||||
<StoreNoticesProvider
|
||||
context={ noticeContexts.EXPRESS_PAYMENTS }
|
||||
>
|
||||
<ExpressPaymentMethods />
|
||||
</StoreNoticesProvider>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingMask>
|
||||
<div className="wc-block-components-express-payment-continue-rule wc-block-components-express-payment-continue-rule--cart">
|
||||
{ /* translators: Shown in the Cart block between the express payment methods and the Proceed to Checkout button */ }
|
||||
{ __( 'Or', 'woocommerce' ) }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartExpressPayment;
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useEmitResponse,
|
||||
useExpressPaymentMethods,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
StoreNoticesProvider,
|
||||
useCheckoutContext,
|
||||
usePaymentMethodDataContext,
|
||||
useEditorContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ExpressPaymentMethods from '../express-payment-methods';
|
||||
import './style.scss';
|
||||
|
||||
const CheckoutExpressPayment = () => {
|
||||
const {
|
||||
isCalculating,
|
||||
isProcessing,
|
||||
isAfterProcessing,
|
||||
isBeforeProcessing,
|
||||
isComplete,
|
||||
hasError,
|
||||
} = useCheckoutContext();
|
||||
const { currentStatus: paymentStatus } = usePaymentMethodDataContext();
|
||||
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
||||
const { isEditor } = useEditorContext();
|
||||
const { noticeContexts } = useEmitResponse();
|
||||
|
||||
if (
|
||||
! isInitialized ||
|
||||
( isInitialized && Object.keys( paymentMethods ).length === 0 )
|
||||
) {
|
||||
// Make sure errors are shown in the editor and for admins. For example,
|
||||
// when a payment method fails to register.
|
||||
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
|
||||
return (
|
||||
<StoreNoticesProvider
|
||||
context={ noticeContexts.EXPRESS_PAYMENTS }
|
||||
></StoreNoticesProvider>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set loading state for express payment methods when payment or checkout is in progress.
|
||||
const checkoutProcessing =
|
||||
isProcessing ||
|
||||
isAfterProcessing ||
|
||||
isBeforeProcessing ||
|
||||
( isComplete && ! hasError );
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingMask
|
||||
isLoading={
|
||||
isCalculating ||
|
||||
checkoutProcessing ||
|
||||
paymentStatus.isDoingExpressPayment
|
||||
}
|
||||
>
|
||||
<div className="wc-block-components-express-payment wc-block-components-express-payment--checkout">
|
||||
<div className="wc-block-components-express-payment__title-container">
|
||||
<Title
|
||||
className="wc-block-components-express-payment__title"
|
||||
headingLevel="2"
|
||||
>
|
||||
{ __(
|
||||
'Express checkout',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Title>
|
||||
</div>
|
||||
<div className="wc-block-components-express-payment__content">
|
||||
<StoreNoticesProvider
|
||||
context={ noticeContexts.EXPRESS_PAYMENTS }
|
||||
>
|
||||
<p>
|
||||
{ __(
|
||||
'In a hurry? Use one of our express checkout options:',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<ExpressPaymentMethods />
|
||||
</StoreNoticesProvider>
|
||||
</div>
|
||||
</div>
|
||||
</LoadingMask>
|
||||
<div className="wc-block-components-express-payment-continue-rule wc-block-components-express-payment-continue-rule--checkout">
|
||||
{ __( 'Or continue below', 'woocommerce' ) }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckoutExpressPayment;
|
@ -0,0 +1,2 @@
|
||||
export { default as CartExpressPayment } from './cart-express-payment.js';
|
||||
export { default as CheckoutExpressPayment } from './checkout-express-payment.js';
|
@ -0,0 +1,157 @@
|
||||
$border-width: 1px;
|
||||
$border-radius: 5px;
|
||||
|
||||
.wc-block-components-express-payment {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment--checkout {
|
||||
margin-top: $border-radius;
|
||||
|
||||
.wc-block-components-express-payment__title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -$border-radius;
|
||||
vertical-align: middle;
|
||||
|
||||
// Pseudo-elements used to show the border before and after the title.
|
||||
&::before {
|
||||
border-left: $border-width solid currentColor;
|
||||
border-top: $border-width solid currentColor;
|
||||
border-radius: $border-radius 0 0 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: $border-radius - $border-width;
|
||||
margin-right: $gap-small;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
width: #{$gap-large - $gap-small - $border-width * 2};
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-right: $border-width solid currentColor;
|
||||
border-top: $border-width solid currentColor;
|
||||
border-radius: 0 $border-radius 0 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: $border-radius - $border-width;
|
||||
margin-left: $gap-small;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment__title {
|
||||
flex-grow: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment__content {
|
||||
@include with-translucent-border(0 $border-width $border-width);
|
||||
padding: em($gap-large) #{$gap-large - $border-width};
|
||||
|
||||
&::after {
|
||||
border-radius: 0 0 $border-radius $border-radius;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin-bottom: em($gap);
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
> li {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
> li:nth-child(even) {
|
||||
padding-left: $gap-smaller;
|
||||
}
|
||||
|
||||
> li:nth-child(odd) {
|
||||
padding-right: $gap-smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment--cart {
|
||||
.wc-block-components-express-payment__event-buttons {
|
||||
> li {
|
||||
padding-bottom: $gap;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment-continue-rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 $gap-large;
|
||||
margin: $gap-large 0;
|
||||
|
||||
&::before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&::before,
|
||||
&::after {
|
||||
content: " ";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment-continue-rule--cart {
|
||||
margin: $gap 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.theme-twentynineteen {
|
||||
.wc-block-components-express-payment__title::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// For Twenty Twenty we need to increase specificity of the title.
|
||||
.theme-twentytwenty {
|
||||
.wc-block-components-express-payment .wc-block-components-express-payment__title {
|
||||
padding-left: $gap-small;
|
||||
padding-right: $gap-small;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export { default as PaymentMethods } from './payment-methods';
|
||||
export { default as ExpressPaymentMethods } from './express-payment-methods';
|
||||
export { CartExpressPayment, CheckoutExpressPayment } from './express-payment';
|
||||
export { default as SavedPaymentMethodOptions } from './saved-payment-method-options';
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Placeholder, Button, Notice } from 'wordpress-components';
|
||||
import { Icon, card } from '@woocommerce/icons';
|
||||
import { ADMIN_URL } from '@woocommerce/settings';
|
||||
import { useEditorContext } from '@woocommerce/base-context';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Render content when no payment methods are found depending on context.
|
||||
*/
|
||||
const NoPaymentMethods = () => {
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
return isEditor ? (
|
||||
<NoPaymentMethodsPlaceholder />
|
||||
) : (
|
||||
<NoPaymentMethodsNotice />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a placeholder in the editor.
|
||||
*/
|
||||
const NoPaymentMethodsPlaceholder = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
icon={ <Icon srcElement={ card } /> }
|
||||
label={ __( 'Payment methods', 'woocommerce' ) }
|
||||
className="wc-block-checkout__no-payment-methods-placeholder"
|
||||
>
|
||||
<span className="wc-block-checkout__no-payment-methods-placeholder-description">
|
||||
{ __(
|
||||
'Your store does not have any payment methods configured that support the checkout block. Once you have configured a compatible payment method (e.g. Stripe) it will be shown here.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
<Button
|
||||
isSecondary
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout` }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ __(
|
||||
'Configure Payment Methods',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Button>
|
||||
</Placeholder>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a notice on the frontend.
|
||||
*/
|
||||
const NoPaymentMethodsNotice = () => {
|
||||
return (
|
||||
<Notice
|
||||
isDismissible={ false }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__no-payment-methods-notice',
|
||||
'woocommerce-message',
|
||||
'woocommerce-error'
|
||||
) }
|
||||
>
|
||||
{ __(
|
||||
'There are no payment methods available. This may be an error on our side. Please contact us if you need any help placing your order.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Notice>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoPaymentMethods;
|
@ -0,0 +1,25 @@
|
||||
.components-placeholder.wc-block-checkout__no-payment-methods-placeholder {
|
||||
margin-bottom: $gap;
|
||||
|
||||
* {
|
||||
pointer-events: all; // Overrides parent disabled component in editor context
|
||||
}
|
||||
|
||||
.components-placeholder__fieldset {
|
||||
display: block;
|
||||
|
||||
.components-button {
|
||||
background-color: $gray-900;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.wc-block-checkout__no-payment-methods-placeholder-description {
|
||||
display: block;
|
||||
margin: 0.25em 0 1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-notice.wc-block-checkout__no-payment-methods-notice {
|
||||
margin-bottom: $gap;
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useCheckoutContext,
|
||||
useEditorContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PaymentMethodErrorBoundary from './payment-method-error-boundary';
|
||||
|
||||
/**
|
||||
* Component used to render the contents of a payment method card.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.showSaveOption Whether that payment method allows saving
|
||||
* the data for future purchases.
|
||||
* @param {Object} props.children Content of the payment method card.
|
||||
*
|
||||
* @return {*} The rendered component.
|
||||
*/
|
||||
const PaymentMethodCard = ( { children, showSaveOption } ) => {
|
||||
const { isEditor } = useEditorContext();
|
||||
const {
|
||||
shouldSavePayment,
|
||||
setShouldSavePayment,
|
||||
} = usePaymentMethodDataContext();
|
||||
const { customerId } = useCheckoutContext();
|
||||
|
||||
return (
|
||||
<PaymentMethodErrorBoundary isEditor={ isEditor }>
|
||||
{ children }
|
||||
{ customerId > 0 && showSaveOption && (
|
||||
<CheckboxControl
|
||||
className="wc-block-components-payment-methods__save-card-info"
|
||||
label={ __(
|
||||
'Save payment information to my account for future purchases.',
|
||||
'woocommerce'
|
||||
) }
|
||||
checked={ shouldSavePayment }
|
||||
onChange={ () =>
|
||||
setShouldSavePayment( ! shouldSavePayment )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</PaymentMethodErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
PaymentMethodCard.propTypes = {
|
||||
showSaveOption: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PaymentMethodCard;
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/base-context';
|
||||
|
||||
class PaymentMethodErrorBoundary extends Component {
|
||||
state = { errorMessage: '', hasError: false };
|
||||
|
||||
static getDerivedStateFromError( error ) {
|
||||
return {
|
||||
errorMessage: error.message,
|
||||
hasError: true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasError, errorMessage } = this.state;
|
||||
const { isEditor } = this.props;
|
||||
|
||||
if ( hasError ) {
|
||||
let errorText = __(
|
||||
'This site is experiencing difficulties with this payment method. Please contact the owner of the site for assistance.',
|
||||
'woocommerce'
|
||||
);
|
||||
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
|
||||
if ( errorMessage ) {
|
||||
errorText = errorMessage;
|
||||
} else {
|
||||
errorText = __(
|
||||
"There was an error with this payment method. Please verify it's configured correctly.",
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
const notices = [
|
||||
{
|
||||
id: '0',
|
||||
content: errorText,
|
||||
isDismissible: false,
|
||||
status: 'error',
|
||||
},
|
||||
];
|
||||
return <StoreNoticesContainer notices={ notices } />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentMethodErrorBoundary.propTypes = {
|
||||
isEditor: PropTypes.bool,
|
||||
};
|
||||
|
||||
PaymentMethodErrorBoundary.defaultProps = {
|
||||
isEditor: false,
|
||||
};
|
||||
|
||||
export default PaymentMethodErrorBoundary;
|
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
usePaymentMethods,
|
||||
usePaymentMethodInterface,
|
||||
useEmitResponse,
|
||||
useStoreNotices,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { cloneElement } from '@wordpress/element';
|
||||
import {
|
||||
useEditorContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import classNames from 'classnames';
|
||||
import RadioControlAccordion from '@woocommerce/base-components/radio-control-accordion';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PaymentMethodCard from './payment-method-card';
|
||||
|
||||
/**
|
||||
* Component used to render all non-saved payment method options.
|
||||
*
|
||||
* @return {*} The rendered component.
|
||||
*/
|
||||
const PaymentMethodOptions = () => {
|
||||
const {
|
||||
setActivePaymentMethod,
|
||||
activeSavedToken,
|
||||
setActiveSavedToken,
|
||||
isExpressPaymentMethodActive,
|
||||
customerPaymentMethods,
|
||||
} = usePaymentMethodDataContext();
|
||||
const { paymentMethods } = usePaymentMethods();
|
||||
const {
|
||||
activePaymentMethod,
|
||||
...paymentMethodInterface
|
||||
} = usePaymentMethodInterface();
|
||||
const { noticeContexts } = useEmitResponse();
|
||||
const { removeNotice } = useStoreNotices();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
const options = Object.keys( paymentMethods ).map( ( name ) => {
|
||||
const { edit, content, label, supports } = paymentMethods[ name ];
|
||||
const component = isEditor ? edit : content;
|
||||
return {
|
||||
value: name,
|
||||
label:
|
||||
typeof label === 'string'
|
||||
? label
|
||||
: cloneElement( label, {
|
||||
components: paymentMethodInterface.components,
|
||||
} ),
|
||||
name: `wc-saved-payment-method-token-${ name }`,
|
||||
content: (
|
||||
<PaymentMethodCard showSaveOption={ supports.showSaveOption }>
|
||||
{ cloneElement( component, {
|
||||
activePaymentMethod,
|
||||
...paymentMethodInterface,
|
||||
} ) }
|
||||
</PaymentMethodCard>
|
||||
),
|
||||
};
|
||||
} );
|
||||
|
||||
const updateToken = ( value ) => {
|
||||
setActivePaymentMethod( value );
|
||||
setActiveSavedToken( '' );
|
||||
removeNotice( 'wc-payment-error', noticeContexts.PAYMENTS );
|
||||
};
|
||||
|
||||
const isSinglePaymentMethod =
|
||||
Object.keys( customerPaymentMethods ).length === 0 &&
|
||||
Object.keys( paymentMethods ).length === 1;
|
||||
|
||||
const singleOptionClass = classNames( {
|
||||
'disable-radio-control': isSinglePaymentMethod,
|
||||
} );
|
||||
|
||||
return isExpressPaymentMethodActive ? null : (
|
||||
<RadioControlAccordion
|
||||
id={ 'wc-payment-method-options' }
|
||||
className={ singleOptionClass }
|
||||
selected={ activeSavedToken ? null : activePaymentMethod }
|
||||
onChange={ updateToken }
|
||||
options={ options }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodOptions;
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { usePaymentMethods } from '@woocommerce/base-context/hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { usePaymentMethodDataContext } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import NoPaymentMethods from './no-payment-methods';
|
||||
import PaymentMethodOptions from './payment-method-options';
|
||||
import SavedPaymentMethodOptions from './saved-payment-method-options';
|
||||
|
||||
/**
|
||||
* PaymentMethods component.
|
||||
*
|
||||
* @return {*} The rendered component.
|
||||
*/
|
||||
const PaymentMethods = () => {
|
||||
const { isInitialized, paymentMethods } = usePaymentMethods();
|
||||
const { customerPaymentMethods } = usePaymentMethodDataContext();
|
||||
|
||||
if ( isInitialized && Object.keys( paymentMethods ).length === 0 ) {
|
||||
return <NoPaymentMethods />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SavedPaymentMethodOptions />
|
||||
{ Object.keys( customerPaymentMethods ).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',
|
||||
],
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
<PaymentMethodOptions />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethods;
|
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
cloneElement,
|
||||
} from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { usePaymentMethodDataContext } from '@woocommerce/base-context';
|
||||
import RadioControl from '@woocommerce/base-components/radio-control';
|
||||
import {
|
||||
usePaymentMethodInterface,
|
||||
usePaymentMethods,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { getPaymentMethods } from '@woocommerce/blocks-registry';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the option object for a cc or echeck saved payment method token.
|
||||
*
|
||||
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||
* @param {function(string):void} setActivePaymentMethod
|
||||
* @param {PaymentStatusDispatch} setPaymentStatus
|
||||
* @return {Object} An option objects to use for RadioControl.
|
||||
*/
|
||||
const getCcOrEcheckPaymentMethodOption = (
|
||||
{ method, expires, tokenId },
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus
|
||||
) => {
|
||||
return {
|
||||
value: tokenId + '',
|
||||
label: sprintf(
|
||||
/* translators: %1$s is referring to the payment method brand, %2$s is referring to the last 4 digits of the payment card, %3$s is referring to the expiry date. */
|
||||
__(
|
||||
'%1$s ending in %2$s (expires %3$s)',
|
||||
'woo-gutenberg-product-blocks'
|
||||
),
|
||||
method.brand,
|
||||
method.last4,
|
||||
expires
|
||||
),
|
||||
name: `wc-saved-payment-method-token-${ tokenId }`,
|
||||
onChange: ( token ) => {
|
||||
const savedTokenKey = `wc-${ method.gateway }-payment-token`;
|
||||
setActivePaymentMethod( method.gateway );
|
||||
setPaymentStatus().started( {
|
||||
payment_method: method.gateway,
|
||||
[ savedTokenKey ]: token + '',
|
||||
isSavedToken: true,
|
||||
} );
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the option object for any non specific saved payment method.
|
||||
*
|
||||
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||
* @param {function(string):void} setActivePaymentMethod
|
||||
* @param {PaymentStatusDispatch} setPaymentStatus
|
||||
*
|
||||
* @return {Object} An option objects to use for RadioControl.
|
||||
*/
|
||||
const getDefaultPaymentMethodOptions = (
|
||||
{ method, tokenId },
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus
|
||||
) => {
|
||||
return {
|
||||
value: tokenId + '',
|
||||
label: sprintf(
|
||||
/* translators: %s is the name of the payment method gateway. */
|
||||
__( 'Saved token for %s', 'woocommerce' ),
|
||||
method.gateway
|
||||
),
|
||||
name: `wc-saved-payment-method-token-${ tokenId }`,
|
||||
onChange: ( token ) => {
|
||||
const savedTokenKey = `wc-${ method.gateway }-payment-token`;
|
||||
setActivePaymentMethod( method.gateway );
|
||||
setPaymentStatus().started( {
|
||||
payment_method: method.gateway,
|
||||
[ savedTokenKey ]: token + '',
|
||||
isSavedToken: true,
|
||||
} );
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const SavedPaymentMethodOptions = () => {
|
||||
const {
|
||||
setPaymentStatus,
|
||||
customerPaymentMethods,
|
||||
activePaymentMethod,
|
||||
setActivePaymentMethod,
|
||||
activeSavedToken,
|
||||
setActiveSavedToken,
|
||||
} = usePaymentMethodDataContext();
|
||||
const standardMethods = getPaymentMethods();
|
||||
const { paymentMethods } = usePaymentMethods();
|
||||
const paymentMethodInterface = usePaymentMethodInterface();
|
||||
|
||||
/**
|
||||
* @type {Object} Options
|
||||
* @property {Array} current The current options on the type.
|
||||
*/
|
||||
const currentOptions = useRef( [] );
|
||||
|
||||
const updateToken = useCallback(
|
||||
( token ) => {
|
||||
setActiveSavedToken( token );
|
||||
},
|
||||
[ setActiveSavedToken ]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
const types = Object.keys( customerPaymentMethods );
|
||||
const options = types
|
||||
.flatMap( ( type ) => {
|
||||
const typeMethods = customerPaymentMethods[ type ];
|
||||
return typeMethods.map( ( paymentMethod ) => {
|
||||
const option =
|
||||
type === 'cc' || type === 'echeck'
|
||||
? getCcOrEcheckPaymentMethodOption(
|
||||
paymentMethod,
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus
|
||||
)
|
||||
: getDefaultPaymentMethodOptions(
|
||||
paymentMethod,
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus
|
||||
);
|
||||
if (
|
||||
! activePaymentMethod &&
|
||||
paymentMethod.is_default &&
|
||||
activeSavedToken === ''
|
||||
) {
|
||||
updateToken( paymentMethod.tokenId + '' );
|
||||
option.onChange( paymentMethod.tokenId );
|
||||
}
|
||||
return option;
|
||||
} );
|
||||
} )
|
||||
.filter( Boolean );
|
||||
currentOptions.current = options;
|
||||
}, [
|
||||
customerPaymentMethods,
|
||||
updateToken,
|
||||
activeSavedToken,
|
||||
activePaymentMethod,
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus,
|
||||
standardMethods,
|
||||
] );
|
||||
|
||||
const savedPaymentMethodHandler =
|
||||
!! activeSavedToken &&
|
||||
paymentMethods[ activePaymentMethod ] &&
|
||||
paymentMethods[ activePaymentMethod ]?.savedTokenComponent
|
||||
? cloneElement(
|
||||
paymentMethods[ activePaymentMethod ]?.savedTokenComponent,
|
||||
{ token: activeSavedToken, ...paymentMethodInterface }
|
||||
)
|
||||
: null;
|
||||
|
||||
return currentOptions.current.length > 0 ? (
|
||||
<>
|
||||
<RadioControl
|
||||
id={ 'wc-payment-method-saved-tokens' }
|
||||
selected={ activeSavedToken }
|
||||
onChange={ updateToken }
|
||||
options={ currentOptions.current }
|
||||
/>
|
||||
{ savedPaymentMethodHandler }
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SavedPaymentMethodOptions;
|
@ -0,0 +1,254 @@
|
||||
.wc-block-card-elements {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.wc-block-components-validation-error {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-gateway-container {
|
||||
position: relative;
|
||||
margin-bottom: em($gap-large);
|
||||
white-space: nowrap;
|
||||
|
||||
&.wc-card-number-element {
|
||||
flex-basis: 15em;
|
||||
flex-grow: 1;
|
||||
// Currently, min() CSS function calls need to be wrapped with unquote.
|
||||
min-width: unquote("min(15em, 60%)");
|
||||
}
|
||||
|
||||
&.wc-card-expiry-element {
|
||||
flex-basis: 7em;
|
||||
margin-left: $gap-small;
|
||||
min-width: unquote("min(7em, calc(24% - #{$gap-small}))");
|
||||
}
|
||||
|
||||
&.wc-card-cvc-element {
|
||||
flex-basis: 7em;
|
||||
margin-left: $gap-small;
|
||||
// Notice the min width ems value is smaller than flex-basis. That's because
|
||||
// by default we want it to have the same width as `expiry-element`, but
|
||||
// if available space is scarce, `cvc-element` should get smaller faster.
|
||||
min-width: unquote("min(5em, calc(16% - #{$gap-small}))");
|
||||
}
|
||||
|
||||
.wc-block-gateway-input {
|
||||
@include font-size(regular);
|
||||
line-height: 1.375; // =22px when font-size is 16px.
|
||||
background-color: #fff;
|
||||
padding: em($gap-small) 0 em($gap-small) $gap;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $input-border-gray;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
height: 3em;
|
||||
color: $input-text-active;
|
||||
cursor: text;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
label {
|
||||
@include reset-typography();
|
||||
@include font-size(regular);
|
||||
line-height: 1.375; // =22px when font-size is 16px.
|
||||
position: absolute;
|
||||
transform: translateY(0.75em);
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: top left;
|
||||
color: $gray-700;
|
||||
transition: transform 200ms ease;
|
||||
margin: 0 0 0 #{$gap + 1px};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - #{$gap + $gap-smaller});
|
||||
cursor: text;
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.wc-inline-card-element {
|
||||
label {
|
||||
// $gap is the padding of the input box, 1.5em the width of the card
|
||||
// icon and $gap-smaller the space between the card
|
||||
// icon and the label.
|
||||
margin-left: calc(#{$gap + $gap-smaller} + 1.5em);
|
||||
}
|
||||
.wc-block-gateway-input.focused.empty,
|
||||
.wc-block-gateway-input:not(.empty) {
|
||||
& + label {
|
||||
margin-left: $gap;
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
}
|
||||
& + .wc-block-components-validation-error {
|
||||
position: static;
|
||||
margin-top: -$gap-large;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-gateway-input.focused.empty,
|
||||
.wc-block-gateway-input:not(.empty) {
|
||||
padding: em($gap-large) 0 em($gap-smallest) $gap;
|
||||
& + label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-gateway-input.has-error {
|
||||
border-color: $alert-red;
|
||||
&:focus {
|
||||
outline-color: $alert-red;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-gateway-input.has-error + label {
|
||||
color: $alert-red;
|
||||
}
|
||||
}
|
||||
|
||||
// These elements have available space below, so we can display errors with a
|
||||
// larger line height.
|
||||
.is-medium,
|
||||
.is-large {
|
||||
.wc-card-expiry-element,
|
||||
.wc-card-cvc-element {
|
||||
.wc-block-components-validation-error > p {
|
||||
line-height: 16px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-mobile,
|
||||
.is-small {
|
||||
.wc-card-expiry-element,
|
||||
.wc-card-cvc-element {
|
||||
.wc-block-components-validation-error > p {
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-payment-methods * {
|
||||
pointer-events: all; // Overrides parent disabled component in editor context
|
||||
}
|
||||
|
||||
.is-mobile,
|
||||
.is-small {
|
||||
.wc-block-card-elements {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wc-block-gateway-container.wc-card-number-element {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.wc-block-gateway-container.wc-card-expiry-element {
|
||||
flex-basis: calc(50% - #{$gap-smaller});
|
||||
margin-left: 0;
|
||||
margin-right: $gap-smaller;
|
||||
}
|
||||
|
||||
.wc-block-gateway-container.wc-card-cvc-element {
|
||||
flex-basis: calc(50% - #{$gap-smaller});
|
||||
margin-left: $gap-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-checkout__payment-method {
|
||||
.wc-block-components-radio-control__option {
|
||||
padding-left: 56px;
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__input {
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to add the first-child and last-child pseudoclasses for specificity.
|
||||
.wc-block-components-radio-control__option,
|
||||
.wc-block-components-radio-control__option:first-child,
|
||||
.wc-block-components-radio-control__option:last-child {
|
||||
margin: 0;
|
||||
padding-bottom: em($gap);
|
||||
padding-top: em($gap);
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__option-checked {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control-accordion-option,
|
||||
.wc-block-components-radio-control__option {
|
||||
@include with-translucent-border(1px 1px 0 1px);
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__option:last-child::after,
|
||||
.wc-block-components-radio-control-accordion-option:last-child::after {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control-accordion-option {
|
||||
.wc-block-components-radio-control__option::after {
|
||||
border-width: 0;
|
||||
}
|
||||
.wc-block-components-radio-control__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.wc-block-components-radio-control__label img {
|
||||
height: 24px;
|
||||
max-height: 24px;
|
||||
object-fit: contain;
|
||||
object-position: left;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control.disable-radio-control {
|
||||
.wc-block-components-radio-control__option {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__description-payments-aligned {
|
||||
padding-top: 14px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control-accordion-content {
|
||||
padding: 0 $gap em($gap) $gap;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-checkout__order-notes {
|
||||
.wc-block-components-checkout-step__content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
registerPaymentMethod,
|
||||
__experimentalDeRegisterPaymentMethod,
|
||||
} from '@woocommerce/blocks-registry';
|
||||
import {
|
||||
PaymentMethodDataProvider,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import * as useStoreCartHook from '../../../../base/context/hooks/cart/use-store-cart';
|
||||
|
||||
// Somewhere in your test case or test suite
|
||||
useStoreCartHook.useStoreCart = jest
|
||||
.fn()
|
||||
.mockReturnValue( useStoreCartHook.defaultCartData );
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PaymentMethods from '../payment-methods';
|
||||
|
||||
jest.mock( '../saved-payment-method-options', () => ( { onChange } ) => {
|
||||
return (
|
||||
<>
|
||||
<span>Saved payment method options</span>
|
||||
<button onClick={ () => onChange( '0' ) }>Select saved</button>
|
||||
</>
|
||||
);
|
||||
} );
|
||||
|
||||
jest.mock(
|
||||
'@woocommerce/base-components/radio-control-accordion',
|
||||
() => ( { onChange } ) => (
|
||||
<>
|
||||
<span>Payment method options</span>
|
||||
<button onClick={ () => onChange( 'stripe' ) }>
|
||||
Select new payment
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
const registerMockPaymentMethods = () => {
|
||||
[ 'stripe' ].forEach( ( name ) => {
|
||||
registerPaymentMethod( {
|
||||
name,
|
||||
label: name,
|
||||
content: <div>A payment method</div>,
|
||||
edit: <div>A payment method</div>,
|
||||
icons: null,
|
||||
canMakePayment: () => true,
|
||||
supports: {
|
||||
showSavedCards: true,
|
||||
showSaveOption: true,
|
||||
features: [ 'products' ],
|
||||
},
|
||||
ariaLabel: name,
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
const resetMockPaymentMethods = () => {
|
||||
[ 'stripe' ].forEach( ( name ) => {
|
||||
__experimentalDeRegisterPaymentMethod( name );
|
||||
} );
|
||||
};
|
||||
|
||||
describe( 'PaymentMethods', () => {
|
||||
test( 'should show no payment methods component when there are no payment methods', async () => {
|
||||
render(
|
||||
<PaymentMethodDataProvider>
|
||||
<PaymentMethods />
|
||||
</PaymentMethodDataProvider>
|
||||
);
|
||||
|
||||
await waitFor( () => {
|
||||
const noPaymentMethods = screen.queryAllByText(
|
||||
/no payment methods available/
|
||||
);
|
||||
// We might get more than one match because the `speak()` function
|
||||
// creates an extra `div` with the notice contents used for a11y.
|
||||
expect( noPaymentMethods.length ).toBeGreaterThanOrEqual( 1 );
|
||||
} );
|
||||
} );
|
||||
|
||||
test( 'selecting new payment method', async () => {
|
||||
const ShowActivePaymentMethod = () => {
|
||||
const {
|
||||
activePaymentMethod,
|
||||
activeSavedToken,
|
||||
} = usePaymentMethodDataContext();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{ 'Active Payment Method: ' + activePaymentMethod }
|
||||
</div>
|
||||
<div>{ 'Active Saved Token: ' + activeSavedToken }</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerMockPaymentMethods();
|
||||
render(
|
||||
<PaymentMethodDataProvider>
|
||||
<PaymentMethods />
|
||||
<ShowActivePaymentMethod />
|
||||
</PaymentMethodDataProvider>
|
||||
);
|
||||
|
||||
await waitFor( () => {
|
||||
const savedPaymentMethodOptions = screen.queryByText(
|
||||
/Saved payment method options/
|
||||
);
|
||||
const paymentMethodOptions = screen.queryByText(
|
||||
/Payment method options/
|
||||
);
|
||||
expect( savedPaymentMethodOptions ).not.toBeNull();
|
||||
expect( paymentMethodOptions ).not.toBeNull();
|
||||
const savedToken = screen.queryByText(
|
||||
/Active Payment Method: stripe/
|
||||
);
|
||||
expect( savedToken ).toBeNull();
|
||||
} );
|
||||
|
||||
fireEvent.click( screen.getByText( 'Select new payment' ) );
|
||||
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: stripe/
|
||||
);
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
} );
|
||||
|
||||
resetMockPaymentMethods();
|
||||
} );
|
||||
} );
|
Reference in New Issue
Block a user