rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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,
},
};

View File

@@ -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"
}

View File

@@ -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 );

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 );
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 />
);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
export * from './attributes';
export * from './form-step-block';
export * from './form-step-heading';
export * from './additional-fields';

View File

@@ -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,
} );

View File

@@ -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 );

View File

@@ -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,
},
};

View File

@@ -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
}

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export const defaultPlaceOrderButtonLabel = __(
'Place Order',
'woo-gutenberg-products-block'
);

View File

@@ -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() } />;
};

View File

@@ -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 );

View File

@@ -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 );

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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'
);

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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 );

View File

@@ -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,
} );

View File

@@ -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;
};

View File

@@ -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,
},
},
};

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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 );

View File

@@ -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,
} );

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -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;
}
}

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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,
} );

View File

@@ -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%;
}
}
}
}

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -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;
}
}

View File

@@ -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,
} );

View File

@@ -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;
}

View File

@@ -0,0 +1,13 @@
export default {
className: {
type: 'string',
default: '',
},
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@@ -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
}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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,
} );

View File

@@ -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 } />;
};

View File

@@ -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 &times; 1, Beanie &times; 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 &times; 1, Beanie &times; 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 &times; 1, Beanie &times; 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 &times; 1, Beanie &times; 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();
} );
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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() } />;
};

View File

@@ -0,0 +1,6 @@
/**
* Internal dependencies
*/
import Block from './block';
export default Block;

View File

@@ -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,
} );

View File

@@ -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