rebase from live enviornment
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ALLOWED_COUNTRIES } from '@woocommerce/block-settings';
|
||||
import type {
|
||||
CartShippingAddress,
|
||||
CartBillingAddress,
|
||||
} from '@woocommerce/types';
|
||||
import { AddressFields, AddressField } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const AddressCard = ( {
|
||||
address,
|
||||
onEdit,
|
||||
target,
|
||||
fieldConfig,
|
||||
}: {
|
||||
address: CartShippingAddress | CartBillingAddress;
|
||||
onEdit: () => void;
|
||||
target: string;
|
||||
fieldConfig: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
} ): JSX.Element | null => {
|
||||
return (
|
||||
<div className="wc-block-components-address-card">
|
||||
<address>
|
||||
<span className="wc-block-components-address-card__address-section">
|
||||
{ address.first_name + ' ' + address.last_name }
|
||||
</span>
|
||||
<div className="wc-block-components-address-card__address-section">
|
||||
{ [
|
||||
address.address_1,
|
||||
! fieldConfig.address_2.hidden && address.address_2,
|
||||
address.city,
|
||||
address.state,
|
||||
address.postcode,
|
||||
ALLOWED_COUNTRIES[ address.country ]
|
||||
? ALLOWED_COUNTRIES[ address.country ]
|
||||
: address.country,
|
||||
]
|
||||
.filter( ( field ) => !! field )
|
||||
.map( ( field, index ) => (
|
||||
<span key={ `address-` + index }>{ field }</span>
|
||||
) ) }
|
||||
</div>
|
||||
{ address.phone && ! fieldConfig.phone.hidden ? (
|
||||
<div
|
||||
key={ `address-phone` }
|
||||
className="wc-block-components-address-card__address-section"
|
||||
>
|
||||
{ address.phone }
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
) }
|
||||
</address>
|
||||
{ onEdit && (
|
||||
<a
|
||||
role="button"
|
||||
href={ '#' + target }
|
||||
className="wc-block-components-address-card__edit"
|
||||
aria-label={ __(
|
||||
'Edit address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
onClick={ ( e ) => {
|
||||
onEdit();
|
||||
e.preventDefault();
|
||||
} }
|
||||
>
|
||||
{ __( 'Edit', 'woo-gutenberg-products-block' ) }
|
||||
</a>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressCard;
|
||||
@@ -0,0 +1,41 @@
|
||||
.wc-block-components-address-card {
|
||||
border: 1px solid $universal-border;
|
||||
@include font-size(regular);
|
||||
padding: em($gap);
|
||||
margin: 0;
|
||||
border-radius: $universal-border-radius;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
address {
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
|
||||
.wc-block-components-address-card__address-section {
|
||||
display: block;
|
||||
margin: 0 0 2px 0;
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 0 4px 0 0;
|
||||
&::after {
|
||||
content: ", ";
|
||||
}
|
||||
&:last-child::after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.wc-block-components-address-card__edit {
|
||||
margin: 0 0 0 auto;
|
||||
text-decoration: none;
|
||||
@include font-size(small);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Wrapper for address fields which handles the edit/preview transition. Form fields are always rendered so that
|
||||
* validation can occur.
|
||||
*/
|
||||
export const AddressWrapper = ( {
|
||||
isEditing = false,
|
||||
addressCard,
|
||||
addressForm,
|
||||
}: {
|
||||
isEditing: boolean;
|
||||
addressCard: () => JSX.Element;
|
||||
addressForm: () => JSX.Element;
|
||||
} ): JSX.Element | null => {
|
||||
const wrapperClasses = classnames(
|
||||
'wc-block-components-address-address-wrapper',
|
||||
{
|
||||
'is-editing': isEditing,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ wrapperClasses }>
|
||||
<div className="wc-block-components-address-card-wrapper">
|
||||
{ addressCard() }
|
||||
</div>
|
||||
<div className="wc-block-components-address-form-wrapper">
|
||||
{ addressForm() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressWrapper;
|
||||
@@ -0,0 +1,32 @@
|
||||
.wc-block-components-address-address-wrapper {
|
||||
position: relative;
|
||||
|
||||
.wc-block-components-address-card-wrapper,
|
||||
.wc-block-components-address-form-wrapper {
|
||||
transition: all 300ms ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.is-editing {
|
||||
.wc-block-components-address-form-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
.wc-block-components-address-card-wrapper {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-editing) {
|
||||
.wc-block-components-address-form-wrapper {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
}
|
||||
.wc-block-components-address-card-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
export const blockName = 'woocommerce/checkout';
|
||||
export const blockAttributes = {
|
||||
hasDarkControls: {
|
||||
type: 'boolean',
|
||||
default: getSetting( 'hasDarkEditorStyleSupport', false ),
|
||||
},
|
||||
showRateAfterTaxName: {
|
||||
type: 'boolean',
|
||||
default: getSetting( 'displayCartPricesIncludingTax', false ),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated here for v1 migration support
|
||||
*/
|
||||
export const deprecatedAttributes = {
|
||||
showOrderNotes: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
showPolicyLinks: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
showReturnToCart: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
cartPageId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "woocommerce/checkout",
|
||||
"version": "1.0.0",
|
||||
"title": "Checkout",
|
||||
"description": "Display a checkout form so your customers can submit orders.",
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"supports": {
|
||||
"align": [ "wide" ],
|
||||
"html": false,
|
||||
"multiple": false
|
||||
},
|
||||
"example": {
|
||||
"attributes": {
|
||||
"isPreview": true
|
||||
},
|
||||
"viewportWidth": 800
|
||||
},
|
||||
"attributes": {
|
||||
"isPreview": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"save": false
|
||||
},
|
||||
"showCompanyField": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"requireCompanyField": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showApartmentField": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"showPhoneField": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"requirePhoneField": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"align": {
|
||||
"type": "string",
|
||||
"default": "wide"
|
||||
}
|
||||
},
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { createInterpolateElement, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
useStoreCart,
|
||||
useShowShippingTotalWarning,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import {
|
||||
CHECKOUT_STORE_KEY,
|
||||
VALIDATION_STORE_KEY,
|
||||
} from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './styles/style.scss';
|
||||
import EmptyCart from './empty-cart';
|
||||
import CheckoutOrderError from './checkout-order-error';
|
||||
import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils';
|
||||
import type { Attributes } from './types';
|
||||
import { CheckoutBlockContext } from './context';
|
||||
|
||||
const MustLoginPrompt = () => {
|
||||
return (
|
||||
<div className="wc-block-must-login-prompt">
|
||||
{ __(
|
||||
'You must be logged in to checkout.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }{ ' ' }
|
||||
<a href={ LOGIN_TO_CHECKOUT_URL }>
|
||||
{ __(
|
||||
'Click here to log in.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Checkout = ( {
|
||||
attributes,
|
||||
children,
|
||||
}: {
|
||||
attributes: Attributes;
|
||||
children: React.ReactChildren;
|
||||
} ): JSX.Element => {
|
||||
const { hasOrder, customerId } = useSelect( ( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
return {
|
||||
hasOrder: store.hasOrder(),
|
||||
customerId: store.getCustomerId(),
|
||||
};
|
||||
} );
|
||||
const { cartItems, cartIsLoading } = useStoreCart();
|
||||
|
||||
const {
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
} = attributes;
|
||||
|
||||
if ( ! cartIsLoading && cartItems.length === 0 ) {
|
||||
return <EmptyCart />;
|
||||
}
|
||||
|
||||
if ( ! hasOrder ) {
|
||||
return <CheckoutOrderError />;
|
||||
}
|
||||
|
||||
/**
|
||||
* If checkout requires an account (guest checkout is turned off), render
|
||||
* a notice and prevent access to the checkout, unless we explicitly allow
|
||||
* account creation during the checkout flow.
|
||||
*/
|
||||
if (
|
||||
isLoginRequired( customerId ) &&
|
||||
! getSetting( 'checkoutAllowsSignup', false )
|
||||
) {
|
||||
return <MustLoginPrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckoutBlockContext.Provider
|
||||
value={
|
||||
{
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
} as Attributes
|
||||
}
|
||||
>
|
||||
{ children }
|
||||
</CheckoutBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ScrollOnError = ( {
|
||||
scrollToTop,
|
||||
}: {
|
||||
scrollToTop: ( props: Record< string, unknown > ) => void;
|
||||
} ): null => {
|
||||
const { hasError: checkoutHasError, isIdle: checkoutIsIdle } = useSelect(
|
||||
( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
return {
|
||||
isIdle: store.isIdle(),
|
||||
hasError: store.hasError(),
|
||||
};
|
||||
}
|
||||
);
|
||||
const { hasValidationErrors } = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return {
|
||||
hasValidationErrors: store.hasValidationErrors(),
|
||||
};
|
||||
} );
|
||||
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
const hasErrorsToDisplay =
|
||||
checkoutIsIdle && checkoutHasError && hasValidationErrors;
|
||||
|
||||
useEffect( () => {
|
||||
let scrollToTopTimeout: number;
|
||||
if ( hasErrorsToDisplay ) {
|
||||
showAllValidationErrors();
|
||||
// Scroll after a short timeout to allow a re-render. This will allow focusableSelector to match updated components.
|
||||
scrollToTopTimeout = window.setTimeout( () => {
|
||||
scrollToTop( {
|
||||
focusableSelector: 'input:invalid, .has-error input',
|
||||
} );
|
||||
}, 50 );
|
||||
}
|
||||
return () => {
|
||||
clearTimeout( scrollToTopTimeout );
|
||||
};
|
||||
}, [ hasErrorsToDisplay, scrollToTop, showAllValidationErrors ] );
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Block = ( {
|
||||
attributes,
|
||||
children,
|
||||
scrollToTop,
|
||||
}: {
|
||||
attributes: Attributes;
|
||||
children: React.ReactChildren;
|
||||
scrollToTop: ( props: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
useShowShippingTotalWarning();
|
||||
return (
|
||||
<BlockErrorBoundary
|
||||
header={ __(
|
||||
'Something went wrong. Please contact us for assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
text={ createInterpolateElement(
|
||||
__(
|
||||
'The checkout has encountered an unexpected error. <button>Try reloading the page</button>. If the error persists, please get in touch with us so we can assist.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
{
|
||||
button: (
|
||||
<button
|
||||
className="wc-block-link-button"
|
||||
onClick={ reloadPage }
|
||||
/>
|
||||
),
|
||||
}
|
||||
) }
|
||||
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
||||
>
|
||||
<StoreNoticesContainer
|
||||
context={ [ noticeContexts.CHECKOUT, noticeContexts.CART ] }
|
||||
/>
|
||||
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
||||
<SlotFillProvider>
|
||||
<CheckoutProvider>
|
||||
<SidebarLayout
|
||||
className={ classnames( 'wc-block-checkout', {
|
||||
'has-dark-controls': attributes.hasDarkControls,
|
||||
} ) }
|
||||
>
|
||||
<Checkout attributes={ attributes }>
|
||||
{ children }
|
||||
</Checkout>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</SlotFillProvider>
|
||||
</BlockErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default withScrollToTop( Block );
|
||||
@@ -0,0 +1,8 @@
|
||||
export const PRODUCT_OUT_OF_STOCK = 'woocommerce_rest_product_out_of_stock';
|
||||
export const PRODUCT_NOT_PURCHASABLE =
|
||||
'woocommerce_rest_product_not_purchasable';
|
||||
export const PRODUCT_NOT_ENOUGH_STOCK =
|
||||
'woocommerce_rest_product_partially_out_of_stock';
|
||||
export const PRODUCT_SOLD_INDIVIDUALLY =
|
||||
'woocommerce_rest_product_too_many_in_cart';
|
||||
export const GENERIC_CART_ITEM_ERROR = 'woocommerce_rest_cart_item_error';
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { CART_URL } from '@woocommerce/block-settings';
|
||||
import { removeCart } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import {
|
||||
PRODUCT_OUT_OF_STOCK,
|
||||
PRODUCT_NOT_PURCHASABLE,
|
||||
PRODUCT_NOT_ENOUGH_STOCK,
|
||||
PRODUCT_SOLD_INDIVIDUALLY,
|
||||
GENERIC_CART_ITEM_ERROR,
|
||||
} from './constants';
|
||||
|
||||
const cartItemErrorCodes = [
|
||||
PRODUCT_OUT_OF_STOCK,
|
||||
PRODUCT_NOT_PURCHASABLE,
|
||||
PRODUCT_NOT_ENOUGH_STOCK,
|
||||
PRODUCT_SOLD_INDIVIDUALLY,
|
||||
GENERIC_CART_ITEM_ERROR,
|
||||
];
|
||||
|
||||
const preloadedCheckoutData = getSetting( 'checkoutData', {} );
|
||||
|
||||
/**
|
||||
* When an order was not created for the checkout, for example, when an item
|
||||
* was out of stock, this component will be shown instead of the checkout form.
|
||||
*
|
||||
* The error message is derived by the hydrated API request passed to the
|
||||
* checkout block.
|
||||
*/
|
||||
const CheckoutOrderError = () => {
|
||||
const checkoutData = {
|
||||
code: '',
|
||||
message: '',
|
||||
...( preloadedCheckoutData || {} ),
|
||||
};
|
||||
|
||||
const errorData = {
|
||||
code: checkoutData.code || 'unknown',
|
||||
message:
|
||||
decodeEntities( checkoutData.message ) ||
|
||||
__(
|
||||
'There was a problem checking out. Please try again. If the problem persists, please get in touch with us so we can assist.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wc-block-checkout-error">
|
||||
<Icon
|
||||
className="wc-block-checkout-error__image"
|
||||
icon={ removeCart }
|
||||
size={ 100 }
|
||||
/>
|
||||
<ErrorTitle errorData={ errorData } />
|
||||
<ErrorMessage errorData={ errorData } />
|
||||
<ErrorButton errorData={ errorData } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the error message to display.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.errorData Object containing code and message.
|
||||
*/
|
||||
const ErrorTitle = ( { errorData } ) => {
|
||||
let heading = __( 'Checkout error', 'woocommerce' );
|
||||
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
heading = __(
|
||||
'There is a problem with your cart',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<strong className="wc-block-checkout-error_title">{ heading }</strong>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the error message to display.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.errorData Object containing code and message.
|
||||
*/
|
||||
const ErrorMessage = ( { errorData } ) => {
|
||||
let message = errorData.message;
|
||||
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
message =
|
||||
message +
|
||||
' ' +
|
||||
__(
|
||||
'Please edit your cart and try again.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
return <p className="wc-block-checkout-error__description">{ message }</p>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the CTA button to display.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.errorData Object containing code and message.
|
||||
*/
|
||||
const ErrorButton = ( { errorData } ) => {
|
||||
let buttonText = __( 'Retry', 'woocommerce' );
|
||||
let buttonUrl = 'javascript:window.location.reload(true)';
|
||||
|
||||
if ( cartItemErrorCodes.includes( errorData.code ) ) {
|
||||
buttonText = __( 'Edit your cart', 'woocommerce' );
|
||||
buttonUrl = CART_URL;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="wp-block-button">
|
||||
<a href={ buttonUrl } className="wp-block-button__link">
|
||||
{ buttonText }
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckoutOrderError;
|
||||
@@ -0,0 +1,22 @@
|
||||
.wc-block-checkout-error {
|
||||
padding: $gap-largest;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
.wc-block-checkout-error__image {
|
||||
max-width: 150px;
|
||||
margin: 0 auto 1em;
|
||||
display: block;
|
||||
color: inherit;
|
||||
fill: currentColor;
|
||||
}
|
||||
.wc-block-checkout-error__title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.wc-block-checkout-error__description {
|
||||
display: block;
|
||||
margin: 0.25em 0 1em 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createContext, useContext } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Context consumed by inner blocks.
|
||||
*/
|
||||
export type CheckoutBlockContextProps = {
|
||||
showCompanyField: boolean;
|
||||
showApartmentField: boolean;
|
||||
showPhoneField: boolean;
|
||||
requireCompanyField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
showOrderNotes: boolean;
|
||||
showPolicyLinks: boolean;
|
||||
showReturnToCart: boolean;
|
||||
cartPageId: number;
|
||||
showRateAfterTaxName: boolean;
|
||||
};
|
||||
|
||||
export type CheckoutBlockControlsContextProps = {
|
||||
addressFieldControls: () => JSX.Element | null;
|
||||
};
|
||||
|
||||
export const CheckoutBlockContext: React.Context< CheckoutBlockContextProps > =
|
||||
createContext< CheckoutBlockContextProps >( {
|
||||
showCompanyField: false,
|
||||
showApartmentField: false,
|
||||
showPhoneField: false,
|
||||
requireCompanyField: false,
|
||||
requirePhoneField: false,
|
||||
showOrderNotes: true,
|
||||
showPolicyLinks: true,
|
||||
showReturnToCart: true,
|
||||
cartPageId: 0,
|
||||
showRateAfterTaxName: false,
|
||||
} );
|
||||
|
||||
export const CheckoutBlockControlsContext: React.Context< CheckoutBlockControlsContextProps > =
|
||||
createContext< CheckoutBlockControlsContextProps >( {
|
||||
addressFieldControls: () => null,
|
||||
} );
|
||||
|
||||
export const useCheckoutBlockContext = (): CheckoutBlockContextProps => {
|
||||
return useContext( CheckoutBlockContext );
|
||||
};
|
||||
|
||||
export const useCheckoutBlockControlsContext =
|
||||
(): CheckoutBlockControlsContextProps => {
|
||||
return useContext( CheckoutBlockControlsContext );
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
InnerBlocks,
|
||||
useBlockProps,
|
||||
InspectorControls,
|
||||
} from '@wordpress/block-editor';
|
||||
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { CheckoutProvider, EditorProvider } from '@woocommerce/base-context';
|
||||
import {
|
||||
previewCart,
|
||||
previewSavedPaymentMethods,
|
||||
} from '@woocommerce/resource-previews';
|
||||
import {
|
||||
PanelBody,
|
||||
ToggleControl,
|
||||
CheckboxControl,
|
||||
} from '@wordpress/components';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import type { TemplateArray } from '@wordpress/blocks';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { getQueryArg } from '@wordpress/url';
|
||||
import { dispatch, select } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './inner-blocks';
|
||||
import './styles/editor.scss';
|
||||
import {
|
||||
addClassToBody,
|
||||
BlockSettings,
|
||||
useBlockPropsWithLocking,
|
||||
} from '../cart-checkout-shared';
|
||||
import '../cart-checkout-shared/sidebar-notices';
|
||||
import { CheckoutBlockContext, CheckoutBlockControlsContext } from './context';
|
||||
import type { Attributes } from './types';
|
||||
|
||||
// This is adds a class to body to signal if the selected block is locked
|
||||
addClassToBody();
|
||||
|
||||
// Array of allowed block names.
|
||||
const ALLOWED_BLOCKS: string[] = [
|
||||
'woocommerce/checkout-fields-block',
|
||||
'woocommerce/checkout-totals-block',
|
||||
];
|
||||
|
||||
export const Edit = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: {
|
||||
clientId: string;
|
||||
attributes: Attributes;
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => undefined;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
showOrderNotes,
|
||||
showPolicyLinks,
|
||||
showReturnToCart,
|
||||
showRateAfterTaxName,
|
||||
cartPageId,
|
||||
isPreview = false,
|
||||
} = attributes;
|
||||
|
||||
// This focuses on the block when a certain query param is found. This is used on the link from the task list.
|
||||
const focus = useRef( getQueryArg( window.location.href, 'focus' ) );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
focus.current === 'checkout' &&
|
||||
! select( 'core/block-editor' ).hasSelectedBlock()
|
||||
) {
|
||||
dispatch( 'core/block-editor' ).selectBlock( clientId );
|
||||
dispatch( 'core/interface' ).enableComplementaryArea(
|
||||
'core/edit-site',
|
||||
'edit-site/block-inspector'
|
||||
);
|
||||
}
|
||||
}, [ clientId ] );
|
||||
|
||||
const defaultTemplate = [
|
||||
[ 'woocommerce/checkout-fields-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-totals-block', {}, [] ],
|
||||
] as TemplateArray;
|
||||
|
||||
const toggleAttribute = ( key: keyof Attributes ): void => {
|
||||
const newAttributes = {} as Partial< Attributes >;
|
||||
newAttributes[ key ] = ! ( attributes[ key ] as boolean );
|
||||
setAttributes( newAttributes );
|
||||
};
|
||||
|
||||
const addressFieldControls = (): JSX.Element => (
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __( 'Address Fields', 'woo-gutenberg-products-block' ) }
|
||||
>
|
||||
<p className="wc-block-checkout__controls-text">
|
||||
{ __(
|
||||
'Show or hide fields in the checkout address forms.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</p>
|
||||
<ToggleControl
|
||||
label={ __( 'Company', 'woo-gutenberg-products-block' ) }
|
||||
checked={ showCompanyField }
|
||||
onChange={ () => toggleAttribute( 'showCompanyField' ) }
|
||||
/>
|
||||
{ showCompanyField && (
|
||||
<CheckboxControl
|
||||
label={ __(
|
||||
'Require company name?',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ requireCompanyField }
|
||||
onChange={ () =>
|
||||
toggleAttribute( 'requireCompanyField' )
|
||||
}
|
||||
className="components-base-control--nested"
|
||||
/>
|
||||
) }
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Apartment, suite, etc.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ showApartmentField }
|
||||
onChange={ () => toggleAttribute( 'showApartmentField' ) }
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __( 'Phone', 'woo-gutenberg-products-block' ) }
|
||||
checked={ showPhoneField }
|
||||
onChange={ () => toggleAttribute( 'showPhoneField' ) }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<CheckboxControl
|
||||
label={ __(
|
||||
'Require phone number?',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ requirePhoneField }
|
||||
onChange={ () =>
|
||||
toggleAttribute( 'requirePhoneField' )
|
||||
}
|
||||
className="components-base-control--nested"
|
||||
/>
|
||||
) }
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
const blockProps = useBlockPropsWithLocking();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<InspectorControls>
|
||||
<BlockSettings
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
/>
|
||||
</InspectorControls>
|
||||
<EditorProvider
|
||||
isPreview={ isPreview }
|
||||
previewData={ { previewCart, previewSavedPaymentMethods } }
|
||||
>
|
||||
<SlotFillProvider>
|
||||
<CheckoutProvider>
|
||||
<SidebarLayout
|
||||
className={ classnames( 'wc-block-checkout', {
|
||||
'has-dark-controls': attributes.hasDarkControls,
|
||||
} ) }
|
||||
>
|
||||
<CheckoutBlockControlsContext.Provider
|
||||
value={ { addressFieldControls } }
|
||||
>
|
||||
<CheckoutBlockContext.Provider
|
||||
value={ {
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
showOrderNotes,
|
||||
showPolicyLinks,
|
||||
showReturnToCart,
|
||||
cartPageId,
|
||||
showRateAfterTaxName,
|
||||
} }
|
||||
>
|
||||
<InnerBlocks
|
||||
allowedBlocks={ ALLOWED_BLOCKS }
|
||||
template={ defaultTemplate }
|
||||
templateLock="insert"
|
||||
/>
|
||||
</CheckoutBlockContext.Provider>
|
||||
</CheckoutBlockControlsContext.Provider>
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</SlotFillProvider>
|
||||
</EditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
{ ...useBlockProps.save( {
|
||||
className: 'wc-block-checkout is-loading',
|
||||
} ) }
|
||||
>
|
||||
<InnerBlocks.Content />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SHOP_URL } from '@woocommerce/block-settings';
|
||||
import { cart } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const EmptyCart = () => {
|
||||
return (
|
||||
<div className="wc-block-checkout-empty">
|
||||
<Icon
|
||||
className="wc-block-checkout-empty__image"
|
||||
icon={ cart }
|
||||
size={ 100 }
|
||||
/>
|
||||
<strong className="wc-block-checkout-empty__title">
|
||||
{ __(
|
||||
'Your cart is currently empty!',
|
||||
'woocommerce'
|
||||
) }
|
||||
</strong>
|
||||
<p className="wc-block-checkout-empty__description">
|
||||
{ __(
|
||||
"Checkout is not available whilst your cart is empty—please take a look through our store and come back when you're ready to place an order.",
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
{ SHOP_URL && (
|
||||
<span className="wp-block-button">
|
||||
<a href={ SHOP_URL } className="wp-block-button__link">
|
||||
{ __( 'Browse store', 'woocommerce' ) }
|
||||
</a>
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyCart;
|
||||
@@ -0,0 +1,21 @@
|
||||
.wc-block-checkout-empty {
|
||||
padding: $gap-largest;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
.wc-block-checkout-empty__image {
|
||||
max-width: 150px;
|
||||
margin: 0 auto 1em;
|
||||
display: block;
|
||||
color: inherit;
|
||||
}
|
||||
.wc-block-checkout-empty__title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.wc-block-checkout-empty__description {
|
||||
display: block;
|
||||
margin: 0.25em 0 1em 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './editor.scss';
|
||||
import { useForcedLayout, getAllowedBlocks } from '../../cart-checkout-shared';
|
||||
|
||||
export const AdditionalFields = ( {
|
||||
block,
|
||||
}: {
|
||||
// Name of the parent block.
|
||||
block: string;
|
||||
} ): JSX.Element => {
|
||||
const { 'data-block': clientId } = useBlockProps();
|
||||
const allowedBlocks = getAllowedBlocks( block );
|
||||
|
||||
useForcedLayout( {
|
||||
clientId,
|
||||
registeredBlocks: allowedBlocks,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className="wc-block-checkout__additional_fields">
|
||||
<InnerBlocks allowedBlocks={ allowedBlocks } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdditionalFieldsContent = (): JSX.Element => (
|
||||
<InnerBlocks.Content />
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const attributes = ( {
|
||||
defaultTitle = __( 'Step', 'woo-gutenberg-products-block' ),
|
||||
defaultDescription = __(
|
||||
'Step description text.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
defaultShowStepNumber = true,
|
||||
}: {
|
||||
defaultTitle: string;
|
||||
defaultDescription: string;
|
||||
defaultShowStepNumber?: boolean;
|
||||
} ): Record< string, Record< string, unknown > > => ( {
|
||||
title: {
|
||||
type: 'string',
|
||||
default: defaultTitle,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
default: defaultDescription,
|
||||
},
|
||||
showStepNumber: {
|
||||
type: 'boolean',
|
||||
default: defaultShowStepNumber,
|
||||
},
|
||||
} );
|
||||
|
||||
export default attributes;
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
.wc-block-checkout__additional_fields {
|
||||
margin: 1.5em 0 0;
|
||||
}
|
||||
.wc-block-components-checkout-step__description-placeholder {
|
||||
opacity: 0.5;
|
||||
display: none;
|
||||
.is-selected & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.wc-block-components-checkout-step__title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.block-editor-plain-text {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
PlainText,
|
||||
InspectorControls,
|
||||
useBlockProps,
|
||||
} from '@wordpress/block-editor';
|
||||
import { PanelBody, ToggleControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FormStepHeading from './form-step-heading';
|
||||
export interface FormStepBlockProps {
|
||||
attributes: { title: string; description: string; showStepNumber: boolean };
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
lock?: { move: boolean; remove: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Form Step Block for use in the editor.
|
||||
*/
|
||||
export const FormStepBlock = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
className = '',
|
||||
children,
|
||||
}: FormStepBlockProps ): JSX.Element => {
|
||||
const { title = '', description = '', showStepNumber = true } = attributes;
|
||||
const blockProps = useBlockProps( {
|
||||
className: classnames( 'wc-block-components-checkout-step', className, {
|
||||
'wc-block-components-checkout-step--with-step-number':
|
||||
showStepNumber,
|
||||
} ),
|
||||
} );
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Form Step Options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Show step number',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ showStepNumber }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
showStepNumber: ! showStepNumber,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<FormStepHeading>
|
||||
<PlainText
|
||||
className={ '' }
|
||||
value={ title }
|
||||
onChange={ ( value ) => setAttributes( { title: value } ) }
|
||||
style={ { backgroundColor: 'transparent' } }
|
||||
/>
|
||||
</FormStepHeading>
|
||||
<div className="wc-block-components-checkout-step__container">
|
||||
<p className="wc-block-components-checkout-step__description">
|
||||
<PlainText
|
||||
className={
|
||||
! description
|
||||
? 'wc-block-components-checkout-step__description-placeholder'
|
||||
: ''
|
||||
}
|
||||
value={ description }
|
||||
placeholder={ __(
|
||||
'Optional text for this form step.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( {
|
||||
description: value,
|
||||
} )
|
||||
}
|
||||
style={ { backgroundColor: 'transparent' } }
|
||||
/>
|
||||
</p>
|
||||
<div className="wc-block-components-checkout-step__content">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Title } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Step Heading Component
|
||||
*/
|
||||
const FormStepHeading = ( {
|
||||
children,
|
||||
stepHeadingContent,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
stepHeadingContent?: JSX.Element;
|
||||
} ): JSX.Element => (
|
||||
<div className="wc-block-components-checkout-step__heading">
|
||||
<Title
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-checkout-step__title"
|
||||
headingLevel="2"
|
||||
>
|
||||
{ children }
|
||||
</Title>
|
||||
{ !! stepHeadingContent && (
|
||||
<span className="wc-block-components-checkout-step__heading-content">
|
||||
{ stepHeadingContent }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FormStepHeading;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './attributes';
|
||||
export * from './form-step-block';
|
||||
export * from './form-step-heading';
|
||||
export * from './additional-fields';
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Children, cloneElement, isValidElement } from '@wordpress/element';
|
||||
import { getValidBlockAttributes } from '@woocommerce/base-utils';
|
||||
import { useStoreCart } from '@woocommerce/base-context';
|
||||
import {
|
||||
useCheckoutExtensionData,
|
||||
useValidation,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
|
||||
import { renderParentBlock } from '@woocommerce/atomic-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './inner-blocks/register-components';
|
||||
import Block from './block';
|
||||
import { blockName, blockAttributes } from './attributes';
|
||||
import metadata from './block.json';
|
||||
|
||||
const getProps = ( el: Element ) => {
|
||||
return {
|
||||
attributes: getValidBlockAttributes(
|
||||
{ ...metadata.attributes, ...blockAttributes },
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
( el instanceof HTMLElement ? el.dataset : {} ) as any
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const Wrapper = ( {
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactChildren;
|
||||
} ): React.ReactNode => {
|
||||
// we need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { extensions, receiveCart, ...cart } = useStoreCart();
|
||||
const checkoutExtensionData = useCheckoutExtensionData();
|
||||
const validation = useValidation();
|
||||
return Children.map( children, ( child ) => {
|
||||
if ( isValidElement( child ) ) {
|
||||
const componentProps = {
|
||||
extensions,
|
||||
cart,
|
||||
checkoutExtensionData,
|
||||
validation,
|
||||
};
|
||||
return cloneElement( child, componentProps );
|
||||
}
|
||||
return child;
|
||||
} );
|
||||
};
|
||||
|
||||
renderParentBlock( {
|
||||
Block,
|
||||
blockName,
|
||||
selector: '.wp-block-woocommerce-checkout',
|
||||
getProps,
|
||||
blockMap: getRegisteredBlockComponents( blockName ) as Record<
|
||||
string,
|
||||
React.ReactNode
|
||||
>,
|
||||
blockWrapper: Wrapper,
|
||||
} );
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { fields } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType, createBlock } from '@wordpress/blocks';
|
||||
import type { BlockInstance } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import { blockAttributes, deprecatedAttributes } from './attributes';
|
||||
import './inner-blocks';
|
||||
import metadata from './block.json';
|
||||
|
||||
const settings = {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ fields }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
...blockAttributes,
|
||||
...deprecatedAttributes,
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
transforms: {
|
||||
to: [
|
||||
{
|
||||
type: 'block',
|
||||
blocks: [ 'woocommerce/classic-shortcode' ],
|
||||
transform: ( attributes ) => {
|
||||
return createBlock(
|
||||
'woocommerce/classic-shortcode',
|
||||
{
|
||||
shortcode: 'checkout',
|
||||
align: attributes.align,
|
||||
},
|
||||
[]
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Migrates v1 to v2 checkout.
|
||||
deprecated: [
|
||||
{
|
||||
attributes: {
|
||||
...metadata.attributes,
|
||||
...blockAttributes,
|
||||
...deprecatedAttributes,
|
||||
},
|
||||
save( { attributes }: { attributes: { className: string } } ) {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'is-loading',
|
||||
attributes.className
|
||||
) }
|
||||
/>
|
||||
);
|
||||
},
|
||||
migrate: ( attributes: {
|
||||
showOrderNotes: boolean;
|
||||
showPolicyLinks: boolean;
|
||||
showReturnToCart: boolean;
|
||||
cartPageId: number;
|
||||
} ) => {
|
||||
const {
|
||||
showOrderNotes,
|
||||
showPolicyLinks,
|
||||
showReturnToCart,
|
||||
cartPageId,
|
||||
} = attributes;
|
||||
return [
|
||||
attributes,
|
||||
[
|
||||
createBlock(
|
||||
'woocommerce/checkout-fields-block',
|
||||
{},
|
||||
[
|
||||
createBlock(
|
||||
'woocommerce/checkout-express-payment-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
createBlock(
|
||||
'woocommerce/checkout-contact-information-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
createBlock(
|
||||
'woocommerce/checkout-shipping-address-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
createBlock(
|
||||
'woocommerce/checkout-billing-address-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
createBlock(
|
||||
'woocommerce/checkout-shipping-methods-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
createBlock(
|
||||
'woocommerce/checkout-payment-block',
|
||||
{},
|
||||
[]
|
||||
),
|
||||
showOrderNotes
|
||||
? createBlock(
|
||||
'woocommerce/checkout-order-note-block',
|
||||
{},
|
||||
[]
|
||||
)
|
||||
: false,
|
||||
showPolicyLinks
|
||||
? createBlock(
|
||||
'woocommerce/checkout-terms-block',
|
||||
{},
|
||||
[]
|
||||
)
|
||||
: false,
|
||||
createBlock(
|
||||
'woocommerce/checkout-actions-block',
|
||||
{
|
||||
showReturnToCart,
|
||||
cartPageId,
|
||||
},
|
||||
[]
|
||||
),
|
||||
].filter( Boolean ) as BlockInstance[]
|
||||
),
|
||||
createBlock( 'woocommerce/checkout-totals-block', {} ),
|
||||
],
|
||||
];
|
||||
},
|
||||
isEligible: (
|
||||
attributes: Record< string, unknown >,
|
||||
innerBlocks: BlockInstance[]
|
||||
) => {
|
||||
return ! innerBlocks.some(
|
||||
( block: { name: string } ) =>
|
||||
block.name === 'woocommerce/checkout-fields-block'
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
registerBlockType( metadata, settings );
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultPlaceOrderButtonLabel } from './constants';
|
||||
|
||||
export default {
|
||||
cartPageId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
showReturnToCart: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
className: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
lock: {
|
||||
type: 'object',
|
||||
default: {
|
||||
move: true,
|
||||
remove: true,
|
||||
},
|
||||
},
|
||||
placeOrderButtonLabel: {
|
||||
type: 'string',
|
||||
default: defaultPlaceOrderButtonLabel,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-actions-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Actions",
|
||||
"description": "Allow customers to place their order.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-fields-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import {
|
||||
PlaceOrderButton,
|
||||
ReturnToCartButton,
|
||||
} from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutSubmit } from '@woocommerce/base-context/hooks';
|
||||
import { noticeContexts } from '@woocommerce/base-context';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultPlaceOrderButtonLabel } from './constants';
|
||||
import './style.scss';
|
||||
|
||||
const Block = ( {
|
||||
cartPageId,
|
||||
showReturnToCart,
|
||||
className,
|
||||
placeOrderButtonLabel,
|
||||
}: {
|
||||
cartPageId: number;
|
||||
showReturnToCart: boolean;
|
||||
className?: string;
|
||||
placeOrderButtonLabel: string;
|
||||
} ): JSX.Element => {
|
||||
const { paymentMethodButtonLabel } = useCheckoutSubmit();
|
||||
const label = applyCheckoutFilter( {
|
||||
filterName: 'placeOrderButtonLabel',
|
||||
defaultValue:
|
||||
paymentMethodButtonLabel ||
|
||||
placeOrderButtonLabel ||
|
||||
defaultPlaceOrderButtonLabel,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames( 'wc-block-checkout__actions', className ) }
|
||||
>
|
||||
<StoreNoticesContainer
|
||||
context={ noticeContexts.CHECKOUT_ACTIONS }
|
||||
/>
|
||||
<div className="wc-block-checkout__actions_row">
|
||||
{ showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting( 'page-' + cartPageId, false ) }
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton
|
||||
label={ label }
|
||||
fullWidth={ ! showReturnToCart }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const defaultPlaceOrderButtonLabel = __(
|
||||
'Place Order',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
|
||||
import PageSelector from '@woocommerce/editor-components/page-selector';
|
||||
import { PanelBody, ToggleControl } from '@wordpress/components';
|
||||
import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { ReturnToCartButton } from '@woocommerce/base-components/cart-checkout';
|
||||
import EditableButton from '@woocommerce/editor-components/editable-button';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultPlaceOrderButtonLabel } from './constants';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: {
|
||||
attributes: {
|
||||
showReturnToCart: boolean;
|
||||
cartPageId: number;
|
||||
placeOrderButtonLabel: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const blockProps = useBlockProps();
|
||||
const {
|
||||
cartPageId = 0,
|
||||
showReturnToCart = true,
|
||||
placeOrderButtonLabel,
|
||||
} = attributes;
|
||||
const { current: savedCartPageId } = useRef( cartPageId );
|
||||
const currentPostId = useSelect(
|
||||
( select ) => {
|
||||
if ( ! savedCartPageId ) {
|
||||
const store = select( 'core/editor' );
|
||||
return store.getCurrentPostId();
|
||||
}
|
||||
return savedCartPageId;
|
||||
},
|
||||
[ savedCartPageId ]
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Account options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Show a "Return to Cart" link',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ showReturnToCart }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
showReturnToCart: ! showReturnToCart,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
{ showReturnToCart &&
|
||||
! (
|
||||
currentPostId === CHECKOUT_PAGE_ID &&
|
||||
savedCartPageId === 0
|
||||
) && (
|
||||
<PageSelector
|
||||
pageId={ cartPageId }
|
||||
setPageId={ ( id: number ) =>
|
||||
setAttributes( { cartPageId: id } )
|
||||
}
|
||||
labels={ {
|
||||
title: __(
|
||||
'Return to Cart button',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
default: __(
|
||||
'WooCommerce Cart Page',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</InspectorControls>
|
||||
<div className="wc-block-checkout__actions">
|
||||
<div className="wc-block-checkout__actions_row">
|
||||
<Noninteractive>
|
||||
{ showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting(
|
||||
'page-' + cartPageId,
|
||||
false
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
</Noninteractive>
|
||||
<EditableButton
|
||||
className={ classnames(
|
||||
'wc-block-cart__submit-button',
|
||||
'wc-block-components-checkout-place-order-button',
|
||||
{
|
||||
'wc-block-components-checkout-place-order-button--full-width':
|
||||
! showReturnToCart,
|
||||
}
|
||||
) }
|
||||
value={ placeOrderButtonLabel }
|
||||
placeholder={ defaultPlaceOrderButtonLabel }
|
||||
onChange={ ( content ) => {
|
||||
setAttributes( {
|
||||
placeOrderButtonLabel: content,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import attributes from './attributes';
|
||||
|
||||
export default withFilteredAttributes( attributes )( Block );
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, button } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import type { BlockConfiguration } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import attributes from './attributes';
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
const blockConfig: BlockConfiguration = {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ button }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes,
|
||||
save: Save,
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-actions-block', blockConfig );
|
||||
@@ -0,0 +1,43 @@
|
||||
.wc-block-checkout__actions {
|
||||
&_row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.wc-block-components-checkout-place-order-button {
|
||||
width: 50%;
|
||||
padding: 1em;
|
||||
height: auto;
|
||||
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-components-button__text {
|
||||
> svg {
|
||||
fill: $white;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-mobile {
|
||||
.wc-block-checkout__actions {
|
||||
.wc-block-components-checkout-return-to-cart-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-place-order-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-large {
|
||||
.wc-block-checkout__actions {
|
||||
@include with-translucent-border(1px 0 0);
|
||||
padding: em($gap-large) 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import formStepAttributes from '../../form-step/attributes';
|
||||
import { DEFAULT_TITLE, DEFAULT_DESCRIPTION } from './constants';
|
||||
|
||||
const attributes: BlockAttributes = {
|
||||
...formStepAttributes( {
|
||||
defaultTitle: DEFAULT_TITLE,
|
||||
defaultDescription: DEFAULT_DESCRIPTION,
|
||||
} ),
|
||||
className: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
lock: {
|
||||
type: 'object',
|
||||
default: {
|
||||
move: true,
|
||||
remove: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
export default attributes;
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-billing-address-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Billing Address",
|
||||
"description": "Collect your customer's billing address.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-fields-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo, Fragment } from '@wordpress/element';
|
||||
import { useEffectOnce } from 'usehooks-ts';
|
||||
import {
|
||||
useCheckoutAddress,
|
||||
useEditorContext,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import type {
|
||||
ShippingAddress,
|
||||
AddressField,
|
||||
AddressFields,
|
||||
} from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CustomerAddress from './customer-address';
|
||||
|
||||
const Block = ( {
|
||||
showCompanyField = false,
|
||||
showApartmentField = false,
|
||||
showPhoneField = false,
|
||||
requireCompanyField = false,
|
||||
requirePhoneField = false,
|
||||
}: {
|
||||
showCompanyField: boolean;
|
||||
showApartmentField: boolean;
|
||||
showPhoneField: boolean;
|
||||
requireCompanyField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
shippingAddress,
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
useBillingAsShipping,
|
||||
} = useCheckoutAddress();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
||||
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
|
||||
useEffectOnce( () => {
|
||||
if ( useBillingAsShipping ) {
|
||||
const { email, ...addressValues } = billingAddress;
|
||||
const syncValues: Partial< ShippingAddress > = {
|
||||
...addressValues,
|
||||
};
|
||||
|
||||
if ( ! showPhoneField ) {
|
||||
delete syncValues.phone;
|
||||
}
|
||||
|
||||
if ( showCompanyField ) {
|
||||
delete syncValues.company;
|
||||
}
|
||||
|
||||
setShippingAddress( syncValues );
|
||||
}
|
||||
} );
|
||||
|
||||
const addressFieldsConfig = useMemo( () => {
|
||||
return {
|
||||
company: {
|
||||
hidden: ! showCompanyField,
|
||||
required: requireCompanyField,
|
||||
},
|
||||
address_2: {
|
||||
hidden: ! showApartmentField,
|
||||
},
|
||||
phone: {
|
||||
hidden: ! showPhoneField,
|
||||
required: requirePhoneField,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const WrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
const noticeContext = useBillingAsShipping
|
||||
? [ noticeContexts.BILLING_ADDRESS, noticeContexts.SHIPPING_ADDRESS ]
|
||||
: [ noticeContexts.BILLING_ADDRESS ];
|
||||
|
||||
const { cartDataLoaded } = useSelect( ( select ) => {
|
||||
const store = select( CART_STORE_KEY );
|
||||
return {
|
||||
cartDataLoaded: store.hasFinishedResolution( 'getCartData' ),
|
||||
};
|
||||
} );
|
||||
|
||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
||||
const hasAddress = !! (
|
||||
billingAddress.address_1 &&
|
||||
( billingAddress.first_name || billingAddress.last_name )
|
||||
);
|
||||
const { email, ...billingAddressWithoutEmail } = billingAddress;
|
||||
const billingMatchesShipping = isShallowEqual(
|
||||
billingAddressWithoutEmail,
|
||||
shippingAddress
|
||||
);
|
||||
const defaultEditingAddress =
|
||||
isEditor || ! hasAddress || billingMatchesShipping;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StoreNoticesContainer context={ noticeContext } />
|
||||
<WrapperComponent>
|
||||
{ cartDataLoaded ? (
|
||||
<CustomerAddress
|
||||
addressFieldsConfig={ addressFieldsConfig }
|
||||
defaultEditing={ defaultEditingAddress }
|
||||
/>
|
||||
) : null }
|
||||
</WrapperComponent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const DEFAULT_TITLE = __(
|
||||
'Billing address',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
export const DEFAULT_DESCRIPTION = __(
|
||||
'Enter the billing address that matches your payment method.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
|
||||
export const DEFAULT_FORCED_BILLING_TITLE = __(
|
||||
'Billing and shipping address',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
export const DEFAULT_FORCED_BILLING_DESCRIPTION = __(
|
||||
'Enter the billing and shipping address that matches your payment method.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||
import type {
|
||||
BillingAddress,
|
||||
AddressField,
|
||||
AddressFields,
|
||||
} from '@woocommerce/settings';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import AddressWrapper from '../../address-wrapper';
|
||||
import AddressCard from '../../address-card';
|
||||
|
||||
const CustomerAddress = ( {
|
||||
addressFieldsConfig,
|
||||
defaultEditing = false,
|
||||
}: {
|
||||
addressFieldsConfig: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
defaultEditing?: boolean;
|
||||
} ) => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
useBillingAsShipping,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const [ editing, setEditing ] = useState( defaultEditing );
|
||||
|
||||
// Forces editing state if store has errors.
|
||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return {
|
||||
hasValidationErrors: store.hasValidationErrors(),
|
||||
invalidProps: Object.keys( billingAddress )
|
||||
.filter( ( key ) => {
|
||||
return (
|
||||
key !== 'email' &&
|
||||
store.getValidationError( 'billing_' + key ) !==
|
||||
undefined
|
||||
);
|
||||
} )
|
||||
.filter( Boolean ),
|
||||
};
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( invalidProps.length > 0 && editing === false ) {
|
||||
setEditing( true );
|
||||
}
|
||||
}, [ editing, hasValidationErrors, invalidProps.length ] );
|
||||
|
||||
const addressFieldKeys = Object.keys(
|
||||
defaultAddressFields
|
||||
) as ( keyof AddressFields )[];
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: Partial< BillingAddress > ) => {
|
||||
setBillingAddress( values );
|
||||
if ( useBillingAsShipping ) {
|
||||
setShippingAddress( values );
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
},
|
||||
[
|
||||
dispatchCheckoutEvent,
|
||||
setBillingAddress,
|
||||
setShippingAddress,
|
||||
useBillingAsShipping,
|
||||
]
|
||||
);
|
||||
|
||||
const renderAddressCardComponent = useCallback(
|
||||
() => (
|
||||
<AddressCard
|
||||
address={ billingAddress }
|
||||
target="billing"
|
||||
onEdit={ () => {
|
||||
setEditing( true );
|
||||
} }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
),
|
||||
[ billingAddress, addressFieldsConfig ]
|
||||
);
|
||||
|
||||
const renderAddressFormComponent = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<AddressForm
|
||||
id="billing"
|
||||
type="billing"
|
||||
onChange={ onChangeAddress }
|
||||
values={ billingAddress }
|
||||
fields={ addressFieldKeys }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
</>
|
||||
),
|
||||
[
|
||||
addressFieldKeys,
|
||||
addressFieldsConfig,
|
||||
billingAddress,
|
||||
onChangeAddress,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AddressWrapper
|
||||
isEditing={ editing }
|
||||
addressCard={ renderAddressCardComponent }
|
||||
addressForm={ renderAddressFormComponent }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerAddress;
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
|
||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||
import type { BlockAttributes } from '@wordpress/blocks';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
FormStepBlock,
|
||||
AdditionalFields,
|
||||
AdditionalFieldsContent,
|
||||
} from '../../form-step';
|
||||
import {
|
||||
useCheckoutBlockContext,
|
||||
useCheckoutBlockControlsContext,
|
||||
} from '../../context';
|
||||
import Block from './block';
|
||||
import {
|
||||
getBillingAddresssBlockTitle,
|
||||
getBillingAddresssBlockDescription,
|
||||
} from './utils';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: {
|
||||
attributes: {
|
||||
title: string;
|
||||
description: string;
|
||||
showStepNumber: boolean;
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: BlockAttributes ) => void;
|
||||
} ): JSX.Element | null => {
|
||||
const {
|
||||
showCompanyField,
|
||||
showApartmentField,
|
||||
requireCompanyField,
|
||||
showPhoneField,
|
||||
requirePhoneField,
|
||||
} = useCheckoutBlockContext();
|
||||
const { addressFieldControls: Controls } =
|
||||
useCheckoutBlockControlsContext();
|
||||
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
|
||||
useCheckoutAddress();
|
||||
|
||||
if ( ! showBillingFields && ! useBillingAsShipping ) {
|
||||
return null;
|
||||
}
|
||||
attributes.title = getBillingAddresssBlockTitle(
|
||||
attributes.title,
|
||||
forcedBillingAddress
|
||||
);
|
||||
attributes.description = getBillingAddresssBlockDescription(
|
||||
attributes.description,
|
||||
forcedBillingAddress
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStepBlock
|
||||
setAttributes={ setAttributes }
|
||||
attributes={ attributes }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__billing-fields',
|
||||
attributes?.className
|
||||
) }
|
||||
>
|
||||
<Controls />
|
||||
<Block
|
||||
showCompanyField={ showCompanyField }
|
||||
showApartmentField={ showApartmentField }
|
||||
requireCompanyField={ requireCompanyField }
|
||||
showPhoneField={ showPhoneField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
/>
|
||||
<AdditionalFields block={ innerBlockAreas.BILLING_ADDRESS } />
|
||||
</FormStepBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<AdditionalFieldsContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
|
||||
import { FormStep } from '@woocommerce/blocks-components';
|
||||
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import attributes from './attributes';
|
||||
import { useCheckoutBlockContext } from '../../context';
|
||||
import {
|
||||
getBillingAddresssBlockTitle,
|
||||
getBillingAddresssBlockDescription,
|
||||
} from './utils';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
title,
|
||||
description,
|
||||
showStepNumber,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
showStepNumber: boolean;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
} ): JSX.Element | null => {
|
||||
const checkoutIsProcessing = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).isProcessing()
|
||||
);
|
||||
const {
|
||||
requireCompanyField,
|
||||
requirePhoneField,
|
||||
showApartmentField,
|
||||
showCompanyField,
|
||||
showPhoneField,
|
||||
} = useCheckoutBlockContext();
|
||||
const { showBillingFields, forcedBillingAddress, useBillingAsShipping } =
|
||||
useCheckoutAddress();
|
||||
|
||||
if ( ! showBillingFields && ! useBillingAsShipping ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
title = getBillingAddresssBlockTitle( title, forcedBillingAddress );
|
||||
description = getBillingAddresssBlockDescription(
|
||||
description,
|
||||
forcedBillingAddress
|
||||
);
|
||||
return (
|
||||
<FormStep
|
||||
id="billing-fields"
|
||||
disabled={ checkoutIsProcessing }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__billing-fields',
|
||||
className
|
||||
) }
|
||||
title={ title }
|
||||
description={ description }
|
||||
showStepNumber={ showStepNumber }
|
||||
>
|
||||
<Block
|
||||
requireCompanyField={ requireCompanyField }
|
||||
showApartmentField={ showApartmentField }
|
||||
showCompanyField={ showCompanyField }
|
||||
showPhoneField={ showPhoneField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
/>
|
||||
{ children }
|
||||
</FormStep>
|
||||
);
|
||||
};
|
||||
|
||||
export default withFilteredAttributes( attributes )( FrontendBlock );
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, mapMarker } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-billing-address-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ mapMarker }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes,
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
DEFAULT_TITLE,
|
||||
DEFAULT_DESCRIPTION,
|
||||
DEFAULT_FORCED_BILLING_DESCRIPTION,
|
||||
DEFAULT_FORCED_BILLING_TITLE,
|
||||
} from './constants';
|
||||
|
||||
export const getBillingAddresssBlockTitle = (
|
||||
title: string,
|
||||
forcedBillingAddress: boolean
|
||||
): string => {
|
||||
if ( forcedBillingAddress ) {
|
||||
// Returns default forced billing title when forced billing address is enabled and there is no title set.
|
||||
return title === DEFAULT_TITLE ? DEFAULT_FORCED_BILLING_TITLE : title;
|
||||
}
|
||||
// Returns default title when forced billing address is disabled and there is no title set.
|
||||
return title === DEFAULT_FORCED_BILLING_TITLE ? DEFAULT_TITLE : title;
|
||||
};
|
||||
|
||||
export const getBillingAddresssBlockDescription = (
|
||||
description: string,
|
||||
forcedBillingAddress: boolean
|
||||
): string => {
|
||||
if ( forcedBillingAddress ) {
|
||||
// Returns default forced billing description when forced billing address is enabled and there is no description set.
|
||||
return description === DEFAULT_DESCRIPTION
|
||||
? DEFAULT_FORCED_BILLING_DESCRIPTION
|
||||
: description;
|
||||
}
|
||||
|
||||
// Returns default description when forced billing address is disabled and there is no description set.
|
||||
return description === DEFAULT_FORCED_BILLING_DESCRIPTION
|
||||
? DEFAULT_DESCRIPTION
|
||||
: description;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import formStepAttributes from '../../form-step/attributes';
|
||||
|
||||
export default {
|
||||
...formStepAttributes( {
|
||||
defaultTitle: __(
|
||||
'Contact information',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
defaultDescription: __(
|
||||
"We'll use this email to send you details and updates about your order.",
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
} ),
|
||||
className: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
lock: {
|
||||
type: 'object',
|
||||
default: {
|
||||
remove: true,
|
||||
move: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-contact-information-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Contact Information",
|
||||
"description": "Collect your customer's contact information.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-fields-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useCheckoutAddress,
|
||||
useStoreEvents,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
StoreNoticesContainer,
|
||||
ValidatedTextInput,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { isEmail } from '@wordpress/url';
|
||||
|
||||
const Block = (): JSX.Element => {
|
||||
const { customerId, shouldCreateAccount } = useSelect( ( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
return {
|
||||
customerId: store.getCustomerId(),
|
||||
shouldCreateAccount: store.getShouldCreateAccount(),
|
||||
};
|
||||
} );
|
||||
|
||||
const { __internalSetShouldCreateAccount } =
|
||||
useDispatch( CHECKOUT_STORE_KEY );
|
||||
const { billingAddress, setEmail } = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const onChangeEmail = ( value: string ) => {
|
||||
setEmail( value );
|
||||
dispatchCheckoutEvent( 'set-email-address' );
|
||||
};
|
||||
|
||||
const createAccountUI = ! customerId &&
|
||||
getSetting( 'checkoutAllowsGuest', false ) &&
|
||||
getSetting( 'checkoutAllowsSignup', false ) && (
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__create-account"
|
||||
label={ __(
|
||||
'Create an account?',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ shouldCreateAccount }
|
||||
onChange={ ( value ) =>
|
||||
__internalSetShouldCreateAccount( value )
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StoreNoticesContainer
|
||||
context={ noticeContexts.CONTACT_INFORMATION }
|
||||
/>
|
||||
<ValidatedTextInput
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
errorId={ 'billing_email' }
|
||||
label={ __( 'Email address', 'woo-gutenberg-products-block' ) }
|
||||
value={ billingAddress.email }
|
||||
required={ true }
|
||||
onChange={ onChangeEmail }
|
||||
customValidation={ ( inputObject: HTMLInputElement ) => {
|
||||
if ( ! isEmail( inputObject.value ) ) {
|
||||
inputObject.setCustomValidity(
|
||||
__(
|
||||
'Please enter a valid email address',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} }
|
||||
/>
|
||||
{ createAccountUI }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
|
||||
import { PanelBody, ExternalLink } from '@wordpress/components';
|
||||
import { ADMIN_URL } from '@woocommerce/settings';
|
||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
FormStepBlock,
|
||||
AdditionalFields,
|
||||
AdditionalFieldsContent,
|
||||
} from '../../form-step';
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: {
|
||||
attributes: {
|
||||
title: string;
|
||||
description: string;
|
||||
showStepNumber: boolean;
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
return (
|
||||
<FormStepBlock
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__contact-fields',
|
||||
attributes?.className
|
||||
) }
|
||||
>
|
||||
<InspectorControls>
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Account creation and guest checkout',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<p className="wc-block-checkout__controls-text">
|
||||
{ __(
|
||||
'Account creation and guest checkout settings can be managed in your store settings.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</p>
|
||||
<ExternalLink
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=account` }
|
||||
>
|
||||
{ __(
|
||||
'Manage account settings',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</ExternalLink>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<Noninteractive>
|
||||
<Block />
|
||||
</Noninteractive>
|
||||
<AdditionalFields block={ innerBlockAreas.CONTACT_INFORMATION } />
|
||||
</FormStepBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<AdditionalFieldsContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
|
||||
import { FormStep } from '@woocommerce/blocks-components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import attributes from './attributes';
|
||||
import LoginPrompt from './login-prompt';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
title,
|
||||
description,
|
||||
showStepNumber,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
showStepNumber: boolean;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
} ) => {
|
||||
const checkoutIsProcessing = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).isProcessing()
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStep
|
||||
id="contact-fields"
|
||||
disabled={ checkoutIsProcessing }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__contact-fields',
|
||||
className
|
||||
) }
|
||||
title={ title }
|
||||
description={ description }
|
||||
showStepNumber={ showStepNumber }
|
||||
stepHeadingContent={ () => <LoginPrompt /> }
|
||||
>
|
||||
<Block />
|
||||
{ children }
|
||||
</FormStep>
|
||||
);
|
||||
};
|
||||
|
||||
export default withFilteredAttributes( attributes )( FrontendBlock );
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, atSymbol } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-contact-information-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ atSymbol }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes,
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { LOGIN_URL } from '@woocommerce/block-settings';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
const LOGIN_TO_CHECKOUT_URL = `${ LOGIN_URL }?redirect_to=${ encodeURIComponent(
|
||||
window.location.href
|
||||
) }`;
|
||||
|
||||
const LoginPrompt = () => {
|
||||
const customerId = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).getCustomerId()
|
||||
);
|
||||
|
||||
if ( ! getSetting( 'checkoutShowLoginReminder', true ) || customerId ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ __(
|
||||
'Already have an account? ',
|
||||
'woocommerce'
|
||||
) }
|
||||
<a href={ LOGIN_TO_CHECKOUT_URL }>
|
||||
{ __( 'Log in.', 'woocommerce' ) }
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPrompt;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-express-payment-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Express Checkout",
|
||||
"description": "Allow customers to breeze through with quick payment options.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-fields-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CheckoutExpressPayment } from '../../../cart-checkout-shared/payment-methods';
|
||||
|
||||
const Block = ( { className }: { className?: string } ): JSX.Element | null => {
|
||||
const { cartNeedsPayment } = useStoreCart();
|
||||
|
||||
if ( ! cartNeedsPayment ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ className }>
|
||||
<CheckoutExpressPayment />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import './editor.scss';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className?: string;
|
||||
lock: {
|
||||
move: boolean;
|
||||
remove: boolean;
|
||||
};
|
||||
};
|
||||
} ): JSX.Element | null => {
|
||||
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
||||
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
|
||||
const blockProps = useBlockProps( {
|
||||
className: classnames(
|
||||
{
|
||||
'wp-block-woocommerce-checkout-express-payment-block--has-express-payment-methods':
|
||||
hasExpressPaymentMethods,
|
||||
},
|
||||
attributes?.className
|
||||
),
|
||||
attributes,
|
||||
} );
|
||||
|
||||
if ( ! isInitialized || ! hasExpressPaymentMethods ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Block />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
// Adjust padding and margins in the editor to improve selected block outlines.
|
||||
.wp-block-woocommerce-checkout-express-payment-block {
|
||||
margin: 14px 0 28px;
|
||||
|
||||
.components-placeholder__label svg {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.wc-block-components-express-payment-continue-rule--checkout {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.wp-block-woocommerce-checkout-express-payment-block--has-express-payment-methods {
|
||||
padding: 14px 0;
|
||||
margin: -14px 0 14px 0 !important;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-checkout-express-payment-block-placeholder {
|
||||
* {
|
||||
pointer-events: all; // Overrides parent disabled component in editor context
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-checkout-express-payment-block-placeholder__description {
|
||||
display: block;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import expressIcon from '../../../cart-checkout-shared/icon';
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-express-payment-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
style={ { fill: 'none' } } // this is needed for this particular svg
|
||||
icon={ expressIcon }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-fields-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Checkout Fields",
|
||||
"description": "Column containing checkout address fields.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
|
||||
import { Main } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||
import type { TemplateArray } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCheckoutBlockControlsContext } from '../../context';
|
||||
import {
|
||||
useForcedLayout,
|
||||
getAllowedBlocks,
|
||||
} from '../../../cart-checkout-shared';
|
||||
import './style.scss';
|
||||
|
||||
export const Edit = ( {
|
||||
clientId,
|
||||
attributes,
|
||||
}: {
|
||||
clientId: string;
|
||||
attributes: {
|
||||
className?: string;
|
||||
isPreview?: boolean;
|
||||
};
|
||||
} ): JSX.Element => {
|
||||
const blockProps = useBlockProps( {
|
||||
className: classnames(
|
||||
'wc-block-checkout__main',
|
||||
attributes?.className
|
||||
),
|
||||
} );
|
||||
const allowedBlocks = getAllowedBlocks( innerBlockAreas.CHECKOUT_FIELDS );
|
||||
|
||||
const { addressFieldControls: Controls } =
|
||||
useCheckoutBlockControlsContext();
|
||||
|
||||
const defaultTemplate = [
|
||||
[ 'woocommerce/checkout-express-payment-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-contact-information-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-shipping-method-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-pickup-options-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-shipping-address-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-billing-address-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-shipping-methods-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-payment-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-note-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-terms-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-actions-block', {}, [] ],
|
||||
].filter( Boolean ) as unknown as TemplateArray;
|
||||
|
||||
useForcedLayout( {
|
||||
clientId,
|
||||
registeredBlocks: allowedBlocks,
|
||||
defaultTemplate,
|
||||
} );
|
||||
|
||||
return (
|
||||
<Main { ...blockProps }>
|
||||
<Controls />
|
||||
<form className="wc-block-components-form wc-block-checkout__form">
|
||||
<InnerBlocks
|
||||
allowedBlocks={ allowedBlocks }
|
||||
templateLock={ false }
|
||||
template={ defaultTemplate }
|
||||
renderAppender={ InnerBlocks.ButtonBlockAppender }
|
||||
/>
|
||||
</form>
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<InnerBlocks.Content />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { Main } from '@woocommerce/base-components/sidebar-layout';
|
||||
import { useStoreEvents } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
} ): JSX.Element => {
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
// Ignore changes to dispatchCheckoutEvent callback so this is ran on first mount only.
|
||||
useEffect( () => {
|
||||
dispatchCheckoutEvent( 'render-checkout-form' );
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Main className={ classnames( 'wc-block-checkout__main', className ) }>
|
||||
<form className="wc-block-components-form wc-block-checkout__form">
|
||||
{ children }
|
||||
</form>
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrontendBlock;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, column } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-fields-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ column }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,42 @@
|
||||
.wc-block-checkout__form {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.is-mobile,
|
||||
.is-small,
|
||||
.is-medium {
|
||||
.wc-block-checkout__main {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
.is-small,
|
||||
.is-medium,
|
||||
.is-large {
|
||||
.wc-block-checkout__shipping-fields,
|
||||
.wc-block-checkout__billing-fields {
|
||||
.wc-block-components-address-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0 calc(#{$gap-smaller} * 2); // Required for spacing especially when using flex-grow
|
||||
|
||||
.wc-block-components-text-input,
|
||||
.wc-block-components-state-input {
|
||||
flex: 1 0 calc(50% - #{$gap-smaller}); // "flex-grow = 1" allows the input to grow to fill the space
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-of-type(2),
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-address-form__company,
|
||||
.wc-block-components-address-form__address_1,
|
||||
.wc-block-components-address-form__address_2,
|
||||
.wc-block-components-country-input {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-note-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Order Note",
|
||||
"description": "Allow customers to add a note to their order.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": false,
|
||||
"move": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-fields-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { FormStep } from '@woocommerce/blocks-components';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CheckoutOrderNotes from '../../order-notes';
|
||||
|
||||
const Block = ( { className }: { className?: string } ): JSX.Element => {
|
||||
const { needsShipping } = useShippingData();
|
||||
const { isProcessing: checkoutIsProcessing, orderNotes } = useSelect(
|
||||
( select ) => {
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
return {
|
||||
isProcessing: store.isProcessing(),
|
||||
orderNotes: store.getOrderNotes(),
|
||||
};
|
||||
}
|
||||
);
|
||||
const { __internalSetOrderNotes } = useDispatch( CHECKOUT_STORE_KEY );
|
||||
|
||||
return (
|
||||
<FormStep
|
||||
id="order-notes"
|
||||
showStepNumber={ false }
|
||||
className={ classnames(
|
||||
'wc-block-checkout__order-notes',
|
||||
className
|
||||
) }
|
||||
disabled={ checkoutIsProcessing }
|
||||
>
|
||||
<CheckoutOrderNotes
|
||||
disabled={ checkoutIsProcessing }
|
||||
onChange={ __internalSetOrderNotes }
|
||||
placeholder={
|
||||
needsShipping
|
||||
? __(
|
||||
'Notes about your order, e.g. special notes for delivery.',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
: __(
|
||||
'Notes about your order.',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
}
|
||||
value={ orderNotes }
|
||||
/>
|
||||
</FormStep>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import './editor.scss';
|
||||
|
||||
export const Edit = (): JSX.Element => {
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Noninteractive>
|
||||
<Block />
|
||||
</Noninteractive>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Adjust padding and margins in the editor to improve selected block outlines.
|
||||
.wp-block-woocommerce-checkout-order-note-block {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.wc-block-checkout__add-note {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, page } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import './style.scss';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-note-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ page }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,29 @@
|
||||
.wc-block-checkout__add-note {
|
||||
margin: em($gap-large) 0;
|
||||
}
|
||||
|
||||
.is-mobile,
|
||||
.is-small,
|
||||
.is-medium {
|
||||
.wc-block-checkout__add-note {
|
||||
@include with-translucent-border(1px 0);
|
||||
margin-bottom: em($gap);
|
||||
margin-top: em($gap);
|
||||
padding: em($gap) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-checkout__add-note .wc-block-components-textarea {
|
||||
margin-top: $gap;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
color: $input-text-active;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 1px $input-border-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-form .wc-block-checkout__order-notes.wc-block-components-checkout-step {
|
||||
padding-left: 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
className: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
lock: {
|
||||
type: 'object',
|
||||
default: {
|
||||
move: true,
|
||||
remove: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Order Summary",
|
||||
"description": "Show customers a summary of their order.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-totals-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
|
||||
import type { TemplateArray } from '@wordpress/blocks';
|
||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||
import { TotalsFooterItem } from '@woocommerce/base-components/cart-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
useForcedLayout,
|
||||
getAllowedBlocks,
|
||||
} from '../../../cart-checkout-shared';
|
||||
import { OrderMetaSlotFill } from './slotfills';
|
||||
|
||||
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
|
||||
const blockProps = useBlockProps();
|
||||
const { cartTotals } = useStoreCart();
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
const allowedBlocks = getAllowedBlocks(
|
||||
innerBlockAreas.CHECKOUT_ORDER_SUMMARY
|
||||
);
|
||||
const defaultTemplate = [
|
||||
[ 'woocommerce/checkout-order-summary-cart-items-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-coupon-form-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-subtotal-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-fee-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-discount-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-shipping-block', {}, [] ],
|
||||
[ 'woocommerce/checkout-order-summary-taxes-block', {}, [] ],
|
||||
] as TemplateArray;
|
||||
|
||||
useForcedLayout( {
|
||||
clientId,
|
||||
registeredBlocks: allowedBlocks,
|
||||
defaultTemplate,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<InnerBlocks
|
||||
allowedBlocks={ allowedBlocks }
|
||||
template={ defaultTemplate }
|
||||
/>
|
||||
<div className="wc-block-components-totals-wrapper">
|
||||
<TotalsFooterItem
|
||||
currency={ totalsCurrency }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</div>
|
||||
<OrderMetaSlotFill />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<InnerBlocks.Content />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsFooterItem } from '@woocommerce/base-components/cart-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OrderMetaSlotFill } from './slotfills';
|
||||
|
||||
const FrontendBlock = ( {
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
} ): JSX.Element | null => {
|
||||
const { cartTotals } = useStoreCart();
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
return (
|
||||
<div className={ className }>
|
||||
{ children }
|
||||
<div className="wc-block-components-totals-wrapper">
|
||||
<TotalsFooterItem
|
||||
currency={ totalsCurrency }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</div>
|
||||
<OrderMetaSlotFill />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrontendBlock;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { totals } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
import attributes from './attributes';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ totals }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
attributes,
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ExperimentalOrderMeta } from '@woocommerce/blocks-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
// @todo Consider deprecating OrderMetaSlotFill and DiscountSlotFill in favour of inner block areas.
|
||||
export const OrderMetaSlotFill = (): JSX.Element => {
|
||||
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { extensions, receiveCart, ...cart } = useStoreCart();
|
||||
const slotFillProps = {
|
||||
extensions,
|
||||
cart,
|
||||
context: 'woocommerce/checkout',
|
||||
};
|
||||
|
||||
return <ExperimentalOrderMeta.Slot { ...slotFillProps } />;
|
||||
};
|
||||
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, findByText, queryByText } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { previewCart as mockPreviewCart } from '../../../../../previews/cart';
|
||||
import {
|
||||
textContentMatcher,
|
||||
textContentMatcherAcrossSiblings,
|
||||
} from '../../../../../../../tests/utils/find-by-text';
|
||||
const baseContextHooks = jest.requireMock( '@woocommerce/base-context/hooks' );
|
||||
const woocommerceSettings = jest.requireMock( '@woocommerce/settings' );
|
||||
import SummaryBlock from '../frontend';
|
||||
import SubtotalBlock from '../../checkout-order-summary-subtotal/frontend';
|
||||
import FeeBlock from '../../checkout-order-summary-fee/frontend';
|
||||
import TaxesBlock from '../../checkout-order-summary-taxes/frontend';
|
||||
import DiscountBlock from '../../checkout-order-summary-discount/frontend';
|
||||
import CouponsBlock from '../../checkout-order-summary-coupon-form/frontend';
|
||||
import ShippingBlock from '../../checkout-order-summary-shipping/frontend';
|
||||
import CartItemsBlock from '../../checkout-order-summary-cart-items/frontend';
|
||||
|
||||
const Block = ( { showRateAfterTaxName = false } ) => (
|
||||
<SummaryBlock>
|
||||
<CartItemsBlock />
|
||||
<SubtotalBlock />
|
||||
<FeeBlock />
|
||||
<DiscountBlock />
|
||||
<CouponsBlock />
|
||||
<ShippingBlock />
|
||||
<TaxesBlock showRateAfterTaxName={ showRateAfterTaxName } />
|
||||
</SummaryBlock>
|
||||
);
|
||||
|
||||
const defaultUseStoreCartValue = {
|
||||
cartItems: mockPreviewCart.items,
|
||||
cartTotals: mockPreviewCart.totals,
|
||||
cartCoupons: mockPreviewCart.coupons,
|
||||
cartFees: mockPreviewCart.fees,
|
||||
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||
shippingRates: mockPreviewCart.shipping_rates,
|
||||
shippingAddress: mockPreviewCart.shipping_address,
|
||||
billingAddress: mockPreviewCart.billing_address,
|
||||
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||
};
|
||||
|
||||
jest.mock( '@woocommerce/base-context/hooks', () => ( {
|
||||
...jest.requireActual( '@woocommerce/base-context/hooks' ),
|
||||
|
||||
/*
|
||||
We need to redefine this here despite the defaultUseStoreCartValue above
|
||||
because jest doesn't like to set up mocks with out of scope variables
|
||||
*/
|
||||
useStoreCart: jest.fn().mockReturnValue( {
|
||||
cartItems: mockPreviewCart.items,
|
||||
cartTotals: mockPreviewCart.totals,
|
||||
cartCoupons: mockPreviewCart.coupons,
|
||||
cartFees: mockPreviewCart.fees,
|
||||
cartNeedsShipping: mockPreviewCart.needs_shipping,
|
||||
shippingRates: mockPreviewCart.shipping_rates,
|
||||
shippingAddress: mockPreviewCart.shipping_address,
|
||||
billingAddress: mockPreviewCart.billing_address,
|
||||
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
|
||||
} ),
|
||||
useShippingData: jest.fn().mockReturnValue( {
|
||||
needsShipping: true,
|
||||
shippingRates: [
|
||||
{
|
||||
package_id: 0,
|
||||
name: 'Shipping method',
|
||||
destination: {
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: 'fb0c0a746719a7596f296344b80cb2b6',
|
||||
name: 'Hoodie - Blue, Yes',
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
key: '1f0e3dad99908345f7439f8ffabdffc4',
|
||||
name: 'Beanie',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_rates: [
|
||||
{
|
||||
rate_id: 'flat_rate:1',
|
||||
name: 'Flat rate',
|
||||
description: '',
|
||||
delivery_time: '',
|
||||
price: '500',
|
||||
taxes: '0',
|
||||
instance_id: 1,
|
||||
method_id: 'flat_rate',
|
||||
meta_data: [
|
||||
{
|
||||
key: 'Items',
|
||||
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||
},
|
||||
],
|
||||
selected: false,
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
},
|
||||
{
|
||||
rate_id: 'local_pickup:2',
|
||||
name: 'Local pickup',
|
||||
description: '',
|
||||
delivery_time: '',
|
||||
price: '0',
|
||||
taxes: '0',
|
||||
instance_id: 2,
|
||||
method_id: 'local_pickup',
|
||||
meta_data: [
|
||||
{
|
||||
key: 'Items',
|
||||
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||
},
|
||||
],
|
||||
selected: false,
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
},
|
||||
{
|
||||
rate_id: 'free_shipping:5',
|
||||
name: 'Free shipping',
|
||||
description: '',
|
||||
delivery_time: '',
|
||||
price: '0',
|
||||
taxes: '0',
|
||||
instance_id: 5,
|
||||
method_id: 'free_shipping',
|
||||
meta_data: [
|
||||
{
|
||||
key: 'Items',
|
||||
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||
},
|
||||
],
|
||||
selected: true,
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} ),
|
||||
} ) );
|
||||
|
||||
jest.mock( '@woocommerce/base-context', () => ( {
|
||||
...jest.requireActual( '@woocommerce/base-context' ),
|
||||
useContainerWidthContext: jest.fn().mockReturnValue( {
|
||||
hasContainerWidth: true,
|
||||
isLarge: true,
|
||||
} ),
|
||||
} ) );
|
||||
|
||||
jest.mock( '@woocommerce/settings', () => {
|
||||
const originalModule = jest.requireActual( '@woocommerce/settings' );
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
getSetting: jest.fn().mockImplementation( ( setting, ...rest ) => {
|
||||
if ( setting === 'couponsEnabled' ) {
|
||||
return true;
|
||||
}
|
||||
return originalModule.getSetting( setting, ...rest );
|
||||
} ),
|
||||
};
|
||||
} );
|
||||
|
||||
const setUseStoreCartReturnValue = ( value = defaultUseStoreCartValue ) => {
|
||||
baseContextHooks.useStoreCart.mockReturnValue( value );
|
||||
};
|
||||
|
||||
const setGetSettingImplementation = ( implementation ) => {
|
||||
woocommerceSettings.getSetting.mockImplementation( implementation );
|
||||
};
|
||||
|
||||
const setUseShippingDataReturnValue = ( value ) => {
|
||||
baseContextHooks.useShippingData.mockReturnValue( value );
|
||||
};
|
||||
|
||||
describe( 'Checkout Order Summary', () => {
|
||||
beforeEach( () => {
|
||||
setUseStoreCartReturnValue();
|
||||
} );
|
||||
|
||||
it( 'Renders the standard preview items in the sidebar', async () => {
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText( container, 'Warm hat for winter' )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await findByText( container, 'Lightweight baseball cap' )
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Checking if variable product is rendered.
|
||||
expect(
|
||||
await findByText( container, textContentMatcher( 'Color: Yellow' ) )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await findByText( container, textContentMatcher( 'Size: Small' ) )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Renders the items subtotal correctly', async () => {
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings( 'Subtotal $40.00' )
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
// The cart_totals value of useStoreCart is what drives this
|
||||
it( 'If discounted items are in the cart the discount subtotal is shown correctly', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...mockPreviewCart.totals,
|
||||
total_discount: 1000,
|
||||
total_price: 3800,
|
||||
},
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings( 'Discount -$10.00' )
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'If coupons are in the cart they are shown correctly', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...mockPreviewCart.totals,
|
||||
total_discount: 1000,
|
||||
total_price: 3800,
|
||||
},
|
||||
cartCoupons: [
|
||||
{
|
||||
code: '10off',
|
||||
discount_type: 'fixed_cart',
|
||||
totals: {
|
||||
total_discount: '1000',
|
||||
total_discount_tax: '0',
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
},
|
||||
label: '10off',
|
||||
},
|
||||
],
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText( container, 'Coupon: 10off' )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Shows fees if the cart_fees are set', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartFees: [
|
||||
{
|
||||
totals: {
|
||||
currency_code: 'USD',
|
||||
currency_decimal_separator: '.',
|
||||
currency_minor_unit: 2,
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
currency_symbol: '$',
|
||||
currency_thousand_separator: ',',
|
||||
total: 1000,
|
||||
total_tax: '0',
|
||||
},
|
||||
},
|
||||
],
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings( 'Fee $10.00' )
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Shows the coupon entry form when coupons are enabled', async () => {
|
||||
setUseStoreCartReturnValue();
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText( container, 'Add a coupon' )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Does not show the coupon entry if coupons are not enabled', () => {
|
||||
setUseStoreCartReturnValue();
|
||||
setGetSettingImplementation( ( setting, ...rest ) => {
|
||||
if ( setting === 'couponsEnabled' ) {
|
||||
return false;
|
||||
}
|
||||
const originalModule = jest.requireActual(
|
||||
'@woocommerce/settings'
|
||||
);
|
||||
return originalModule.getSetting( setting, ...rest );
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
queryByText( container, 'Coupon code' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Does not show the shipping section if needsShipping is false on the cart', () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartNeedsShipping: false,
|
||||
} );
|
||||
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect( queryByText( container, 'Shipping' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Does not show the taxes section if displayCartPricesIncludingTax is true', () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...mockPreviewCart.totals,
|
||||
total_tax: '1000',
|
||||
tax_lines: [ { name: 'Tax', price: '1000', rate: '5%' } ],
|
||||
},
|
||||
} );
|
||||
setGetSettingImplementation( ( setting, ...rest ) => {
|
||||
if ( setting === 'displayCartPricesIncludingTax' ) {
|
||||
return true;
|
||||
}
|
||||
if ( setting === 'taxesEnabled' ) {
|
||||
return true;
|
||||
}
|
||||
const originalModule = jest.requireActual(
|
||||
'@woocommerce/settings'
|
||||
);
|
||||
return originalModule.getSetting( setting, ...rest );
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
|
||||
expect(
|
||||
queryByText( container, 'Tax $10.00' )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Shows the taxes section if displayCartPricesIncludingTax is false and a tax total is set', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...mockPreviewCart.totals,
|
||||
total_tax: '1000',
|
||||
tax_lines: [ { name: 'Tax', price: '1000', rate: '5%' } ],
|
||||
},
|
||||
} );
|
||||
setUseShippingDataReturnValue( { needsShipping: false } );
|
||||
setGetSettingImplementation( ( setting, ...rest ) => {
|
||||
if ( setting === 'displayCartPricesIncludingTax' ) {
|
||||
return false;
|
||||
}
|
||||
if ( setting === 'taxesEnabled' ) {
|
||||
return true;
|
||||
}
|
||||
const originalModule = jest.requireActual(
|
||||
'@woocommerce/settings'
|
||||
);
|
||||
return originalModule.getSetting( setting, ...rest );
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings( 'Taxes $10.00' )
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Shows the grand total correctly', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...mockPreviewCart.totals,
|
||||
},
|
||||
cartNeedsShipping: false,
|
||||
} );
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings( 'Total $49.20' )
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'Correctly shows the shipping section if the cart requires shipping', async () => {
|
||||
setUseStoreCartReturnValue( {
|
||||
...defaultUseStoreCartValue,
|
||||
cartTotals: {
|
||||
...defaultUseStoreCartValue.cartTotals,
|
||||
total_shipping: '4000',
|
||||
},
|
||||
cartNeedsShipping: true,
|
||||
shippingRates: [
|
||||
{
|
||||
package_id: 0,
|
||||
name: 'Shipping method',
|
||||
destination: {
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: 'fb0c0a746719a7596f296344b80cb2b6',
|
||||
name: 'Hoodie - Blue, Yes',
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
key: '1f0e3dad99908345f7439f8ffabdffc4',
|
||||
name: 'Beanie',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_rates: [
|
||||
{
|
||||
rate_id: 'free_shipping:5',
|
||||
name: 'Free shipping',
|
||||
description: '',
|
||||
delivery_time: '',
|
||||
price: '4000',
|
||||
taxes: '0',
|
||||
instance_id: 5,
|
||||
method_id: 'free_shipping',
|
||||
meta_data: [
|
||||
{
|
||||
key: 'Items',
|
||||
value: 'Hoodie - Blue, Yes × 1, Beanie × 1',
|
||||
},
|
||||
],
|
||||
selected: true,
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} );
|
||||
|
||||
const { container } = render( <Block showRateAfterTaxName={ true } /> );
|
||||
expect(
|
||||
await findByText(
|
||||
container,
|
||||
textContentMatcherAcrossSiblings(
|
||||
'Shipping $40.00 Free shipping'
|
||||
)
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-cart-items-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Cart Items",
|
||||
"description": "Shows cart items.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { OrderSummary } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
|
||||
const Block = ( { className }: { className: string } ): JSX.Element => {
|
||||
const { cartItems } = useStoreCart();
|
||||
|
||||
return (
|
||||
<TotalsWrapper className={ className }>
|
||||
<OrderSummary cartItems={ cartItems } />
|
||||
</TotalsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Block className={ className } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { cart } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-cart-items-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ cart }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-coupon-form-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Coupon Form",
|
||||
"description": "Shows the apply coupon form.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": false,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsCoupon } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useStoreCartCoupons } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
|
||||
const Block = ( {
|
||||
className = '',
|
||||
}: {
|
||||
className?: string;
|
||||
} ): JSX.Element | null => {
|
||||
const couponsEnabled = getSetting( 'couponsEnabled', true );
|
||||
|
||||
const { applyCoupon, isApplyingCoupon } =
|
||||
useStoreCartCoupons( 'wc/checkout' );
|
||||
|
||||
if ( ! couponsEnabled ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TotalsWrapper className={ className }>
|
||||
<TotalsCoupon
|
||||
onSubmit={ applyCoupon }
|
||||
isLoading={ isApplyingCoupon }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Noninteractive>
|
||||
<Block className={ className } />
|
||||
</Noninteractive>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon, tag } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-coupon-form-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ tag }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-discount-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Discount",
|
||||
"description": "Shows the cart discount row.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsDiscount } from '@woocommerce/base-components/cart-checkout';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import {
|
||||
useStoreCartCoupons,
|
||||
useStoreCart,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { ExperimentalDiscountsMeta } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const DiscountSlotFill = (): JSX.Element => {
|
||||
// Prepare props to pass to the ExperimentalOrderMeta slot fill. We need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { extensions, receiveCart, ...cart } = useStoreCart();
|
||||
const discountsSlotFillProps = {
|
||||
extensions,
|
||||
cart,
|
||||
context: 'woocommerce/checkout',
|
||||
};
|
||||
|
||||
return <ExperimentalDiscountsMeta.Slot { ...discountsSlotFillProps } />;
|
||||
};
|
||||
|
||||
const Block = ( { className = '' }: { className?: string } ): JSX.Element => {
|
||||
const { cartTotals, cartCoupons } = useStoreCart();
|
||||
const { removeCoupon, isRemovingCoupon } =
|
||||
useStoreCartCoupons( 'wc/checkout' );
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
return (
|
||||
<>
|
||||
<TotalsWrapper className={ className }>
|
||||
<TotalsDiscount
|
||||
cartCoupons={ cartCoupons }
|
||||
currency={ totalsCurrency }
|
||||
isRemovingCoupon={ isRemovingCoupon }
|
||||
removeCoupon={ removeCoupon }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
<DiscountSlotFill />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Block className={ className } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { totals } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-discount-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ totals }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-fee-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Fees",
|
||||
"description": "Shows the cart fee row.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsFees, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
const Block = ( { className = '' }: { className?: string } ): JSX.Element => {
|
||||
const { cartFees, cartTotals } = useStoreCart();
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
return (
|
||||
<TotalsWrapper className={ className }>
|
||||
<TotalsFees currency={ totalsCurrency } cartFees={ cartFees } />
|
||||
</TotalsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Block className={ className } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { totals } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-fee-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ totals }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-shipping-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Shipping",
|
||||
"description": "Shows the cart shipping row.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TotalsShipping } from '@woocommerce/base-components/cart-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { TotalsWrapper } from '@woocommerce/blocks-checkout';
|
||||
|
||||
const Block = ( {
|
||||
className = '',
|
||||
}: {
|
||||
className?: string;
|
||||
} ): JSX.Element | null => {
|
||||
const { cartTotals, cartNeedsShipping } = useStoreCart();
|
||||
|
||||
if ( ! cartNeedsShipping ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
return (
|
||||
<TotalsWrapper className={ className }>
|
||||
<TotalsShipping
|
||||
showCalculator={ false }
|
||||
showRateSelector={ false }
|
||||
values={ cartTotals }
|
||||
currency={ totalsCurrency }
|
||||
isCheckout={ true }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Noninteractive>
|
||||
<Block className={ className } />
|
||||
</Noninteractive>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { totals } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-shipping-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ totals }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "woocommerce/checkout-order-summary-subtotal-block",
|
||||
"version": "1.0.0",
|
||||
"title": "Subtotal",
|
||||
"description": "Shows the cart subtotal row.",
|
||||
"category": "woocommerce",
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"attributes": {
|
||||
"className": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"lock": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"remove": true,
|
||||
"move": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"parent": [ "woocommerce/checkout-order-summary-block" ],
|
||||
"textdomain": "woocommerce",
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Subtotal, TotalsWrapper } from '@woocommerce/blocks-components';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
|
||||
const Block = ( { className = '' }: { className?: string } ): JSX.Element => {
|
||||
const { cartTotals } = useStoreCart();
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
return (
|
||||
<TotalsWrapper className={ className }>
|
||||
<Subtotal currency={ totalsCurrency } values={ cartTotals } />
|
||||
</TotalsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export const Edit = ( {
|
||||
attributes,
|
||||
}: {
|
||||
attributes: {
|
||||
className: string;
|
||||
};
|
||||
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||
} ): JSX.Element => {
|
||||
const { className } = attributes;
|
||||
const blockProps = useBlockProps();
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<Block className={ className } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Save = (): JSX.Element => {
|
||||
return <div { ...useBlockProps.save() } />;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export default Block;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { totals } from '@woocommerce/icons';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Edit, Save } from './edit';
|
||||
|
||||
registerBlockType( 'woocommerce/checkout-order-summary-subtotal-block', {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ totals }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
edit: Edit,
|
||||
save: Save,
|
||||
} );
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
export default {
|
||||
showRateAfterTaxName: {
|
||||
type: 'boolean',
|
||||
default: getSetting( 'displayCartPricesIncludingTax', false ),
|
||||
},
|
||||
lock: {
|
||||
type: 'object',
|
||||
default: {
|
||||
remove: true,
|
||||
move: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user