diff --git a/assets/js/atomic/utils/index.js b/assets/js/atomic/utils/index.js
index 111fae8fd7d..8b3c23b7821 100644
--- a/assets/js/atomic/utils/index.js
+++ b/assets/js/atomic/utils/index.js
@@ -1,5 +1,5 @@
export * from './get-block-map';
export * from './create-blocks-from-template';
export * from './render-parent-block';
-export * from './block-styling.js';
+export * from './block-styling';
export * from './render-standalone-blocks';
diff --git a/assets/js/base/components/block-error-boundary/style.scss b/assets/js/base/components/block-error-boundary/style.scss
index 06b7b344483..0676d961a49 100644
--- a/assets/js/base/components/block-error-boundary/style.scss
+++ b/assets/js/base/components/block-error-boundary/style.scss
@@ -24,7 +24,7 @@
max-width: 60ch;
}
.wc-block-components-error__message {
- margin: 1em 0 0;
+ margin: 1em auto 0;
font-style: italic;
color: $studio-gray-30;
max-width: 60ch;
diff --git a/assets/js/base/components/cart-checkout/form-step/style.scss b/assets/js/base/components/cart-checkout/form-step/style.scss
index ddfea2b1540..cdfbf0206da 100644
--- a/assets/js/base/components/cart-checkout/form-step/style.scss
+++ b/assets/js/base/components/cart-checkout/form-step/style.scss
@@ -32,7 +32,11 @@
position: relative;
}
-.wc-block-components-checkout-step__content {
+.wc-block-components-checkout-step__content > * {
+ margin-bottom: em($gap);
+}
+.wc-block-components-checkout-step--with-step-number .wc-block-components-checkout-step__content > :last-child {
+ margin-bottom: 0;
padding-bottom: em($gap-large);
}
diff --git a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx
index e1913177849..9d3c27ab724 100644
--- a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx
+++ b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx
@@ -10,7 +10,7 @@ import {
getShippingRatesPackageCount,
getShippingRatesRateCount,
} from '@woocommerce/base-utils';
-import { useStoreCart } from '@woocommerce/base-context/hooks';
+import { useStoreCart, useEditorContext } from '@woocommerce/base-context';
import { CartResponseShippingRate } from '@woocommerce/type-defs/cart-response';
import { ReactElement } from 'react';
@@ -162,6 +162,7 @@ const ShippingRatesControl = ( {
ShippingRatesControlPackage,
},
};
+ const { isEditor } = useEditorContext();
return (
-
-
+ { isEditor ? (
-
+ ) : (
+ <>
+
+
+
+
+ >
+ ) }
);
};
diff --git a/assets/js/base/components/checkbox-control/index.js b/assets/js/base/components/checkbox-control/index.tsx
similarity index 60%
rename from assets/js/base/components/checkbox-control/index.js
rename to assets/js/base/components/checkbox-control/index.tsx
index 91fb25b9267..a28601685c3 100644
--- a/assets/js/base/components/checkbox-control/index.js
+++ b/assets/js/base/components/checkbox-control/index.tsx
@@ -2,7 +2,6 @@
* External dependencies
*/
import { withInstanceId } from '@woocommerce/base-hocs/with-instance-id';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
/**
@@ -10,16 +9,17 @@ import classNames from 'classnames';
*/
import './style.scss';
+type CheckboxControlProps = {
+ className?: string;
+ label?: string;
+ id?: string;
+ instanceId: string;
+ onChange: ( value: boolean ) => void;
+ children: React.ReactChildren;
+};
+
/**
* Component used to show a checkbox control with styles.
- *
- * @param {Object} props Incoming props for the component.
- * @param {string} props.className CSS class used.
- * @param {string} props.label Label for component.
- * @param {string} props.id Id for component.
- * @param {string} props.instanceId Unique id for instance of component.
- * @param {function():any} props.onChange Function called when input changes.
- * @param {Object} props.rest Rest of properties spread.
*/
const CheckboxControl = ( {
className,
@@ -27,8 +27,9 @@ const CheckboxControl = ( {
id,
instanceId,
onChange,
+ children,
...rest
-} ) => {
+}: CheckboxControlProps ): JSX.Element => {
const checkboxId = id || `checkbox-control-${ instanceId }`;
return (
@@ -54,19 +55,14 @@ const CheckboxControl = ( {
>
-
-
- { label }
-
+ { label && (
+
+ { label }
+
+ ) }
+ { children }
);
};
-CheckboxControl.propTypes = {
- className: PropTypes.string,
- label: PropTypes.string,
- id: PropTypes.string,
- onChange: PropTypes.func,
-};
-
export default withInstanceId( CheckboxControl );
diff --git a/assets/js/base/components/checkbox-control/style.scss b/assets/js/base/components/checkbox-control/style.scss
index 6cb72b0bcab..d0c8a9a3752 100644
--- a/assets/js/base/components/checkbox-control/style.scss
+++ b/assets/js/base/components/checkbox-control/style.scss
@@ -1,8 +1,7 @@
.wc-block-components-checkbox {
@include reset-typography();
- align-items: center;
+ align-items: flex-start;
display: flex;
- height: 1em;
position: relative;
.wc-block-components-checkbox__input[type="checkbox"] {
@@ -10,6 +9,7 @@
appearance: none;
border: 2px solid $input-border-gray;
border-radius: 2px;
+ box-sizing: border-box;
height: em(24px);
width: em(24px);
margin: 0;
@@ -48,8 +48,8 @@
.wc-block-components-checkbox__mark {
fill: #000;
position: absolute;
- left: em(3px);
- top: em(-2px);
+ margin-left: em(3px);
+ margin-top: em(1px);
width: em(18px);
height: em(18px);
@@ -58,9 +58,11 @@
}
}
+ > span,
.wc-block-components-checkbox__label {
padding-left: $gap;
vertical-align: middle;
+ line-height: em(24px);
}
}
diff --git a/assets/js/base/context/hooks/cart/test/use-store-cart.js b/assets/js/base/context/hooks/cart/test/use-store-cart.js
index 1a9ffcfed26..1adeb1861d1 100644
--- a/assets/js/base/context/hooks/cart/test/use-store-cart.js
+++ b/assets/js/base/context/hooks/cart/test/use-store-cart.js
@@ -61,6 +61,7 @@ describe( 'useStoreCart', () => {
state: '',
postcode: '',
country: '',
+ phone: '',
},
shippingRates: previewCart.shipping_rates,
extensions: {},
diff --git a/assets/js/base/context/hooks/cart/use-store-cart.ts b/assets/js/base/context/hooks/cart/use-store-cart.ts
index 2197748156d..406163033f5 100644
--- a/assets/js/base/context/hooks/cart/use-store-cart.ts
+++ b/assets/js/base/context/hooks/cart/use-store-cart.ts
@@ -41,12 +41,12 @@ const defaultShippingAddress: CartResponseShippingAddress = {
state: '',
postcode: '',
country: '',
+ phone: '',
};
const defaultBillingAddress: CartResponseBillingAddress = {
...defaultShippingAddress,
email: '',
- phone: '',
};
const defaultCartTotals: CartResponseTotals = {
diff --git a/assets/js/base/context/hooks/use-checkout-address.js b/assets/js/base/context/hooks/use-checkout-address.js
index 0ac8b09e573..a4e7cb3a61e 100644
--- a/assets/js/base/context/hooks/use-checkout-address.js
+++ b/assets/js/base/context/hooks/use-checkout-address.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import { defaultAddressFields } from '@woocommerce/settings';
-import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
+import { useEffect, useCallback, useRef } from '@wordpress/element';
/**
* Internal dependencies
@@ -10,43 +10,22 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import {
useShippingDataContext,
useCustomerDataContext,
- useCheckoutContext,
} from '../providers/cart-checkout';
-/**
- * Compare two addresses and see if they are the same.
- *
- * @param {Object} address1 First address.
- * @param {Object} address2 Second address.
- */
-const isSameAddress = ( address1, address2 ) => {
- return Object.keys( defaultAddressFields ).every(
- ( field ) => address1[ field ] === address2[ field ]
- );
-};
-
/**
* Custom hook for exposing address related functionality for the checkout address form.
*/
export const useCheckoutAddress = () => {
- const { customerId } = useCheckoutContext();
const { needsShipping } = useShippingDataContext();
const {
billingData,
setBillingData,
shippingAddress,
setShippingAddress,
+ shippingAsBilling,
+ setShippingAsBilling,
} = useCustomerDataContext();
- // This tracks the state of the "shipping as billing" address checkbox. It's
- // initial value is true (if shipping is needed), however, if the user is
- // logged in and they have a different billing address, we can toggle this off.
- const [ shippingAsBilling, setShippingAsBilling ] = useState(
- () =>
- needsShipping &&
- ( ! customerId || isSameAddress( shippingAddress, billingData ) )
- );
-
const currentShippingAsBilling = useRef( shippingAsBilling );
const previousBillingData = useRef( billingData );
@@ -78,9 +57,8 @@ export const useCheckoutAddress = () => {
[ needsShipping, setShippingAddress, setBillingData ]
);
- // When the "Use same address" checkbox is toggled we need to update the current billing address to reflect this;
- // that is either setting the billing address to the shipping address, or restoring the billing address to it's
- // previous state.
+ // When the "Use same address" checkbox is toggled we need to update the current billing address to reflect this.
+ // This either sets the billing address to the shipping address, or restores the billing address to it's previous state.
useEffect( () => {
if ( currentShippingAsBilling.current !== shippingAsBilling ) {
if ( shippingAsBilling ) {
@@ -88,13 +66,13 @@ export const useCheckoutAddress = () => {
setBillingData( shippingAddress );
} else {
const {
- // We need to pluck out email and phone from previous billing data because they can be empty, causing the current email and phone to get emptied. See issue #4155
+ // We need to pluck out email from previous billing data because they can be empty, causing the current email to get emptied. See issue #4155
/* eslint-disable no-unused-vars */
email,
- phone,
/* eslint-enable no-unused-vars */
...billingAddress
} = previousBillingData.current;
+
setBillingData( {
...billingAddress,
} );
@@ -103,14 +81,29 @@ export const useCheckoutAddress = () => {
}
}, [ shippingAsBilling, setBillingData, shippingAddress, billingData ] );
- const setEmail = ( value ) =>
- void setBillingData( {
- email: value,
- } );
- const setPhone = ( value ) =>
- void setBillingData( {
- phone: value,
- } );
+ const setEmail = useCallback(
+ ( value ) =>
+ void setBillingData( {
+ email: value,
+ } ),
+ [ setBillingData ]
+ );
+
+ const setPhone = useCallback(
+ ( value ) =>
+ void setBillingData( {
+ phone: value,
+ } ),
+ [ setBillingData ]
+ );
+
+ const setShippingPhone = useCallback(
+ ( value ) =>
+ void setShippingFields( {
+ phone: value,
+ } ),
+ [ setShippingFields ]
+ );
// Note that currentShippingAsBilling is returned rather than the current state of shippingAsBilling--this is so that
// the billing fields are not rendered before sync (billing field values are debounced and would be outdated)
@@ -122,8 +115,10 @@ export const useCheckoutAddress = () => {
setBillingFields,
setEmail,
setPhone,
+ setShippingPhone,
shippingAsBilling,
setShippingAsBilling,
+ showShippingFields: needsShipping,
showBillingFields:
! needsShipping || ! currentShippingAsBilling.current,
};
diff --git a/assets/js/base/context/hooks/use-customer-data.ts b/assets/js/base/context/hooks/use-customer-data.ts
index 165fda1cc52..98dcce60c2d 100644
--- a/assets/js/base/context/hooks/use-customer-data.ts
+++ b/assets/js/base/context/hooks/use-customer-data.ts
@@ -112,7 +112,7 @@ export const useCustomerData = (): {
/**
* Set billing data.
*
- * Contains special handling for email and phone so those fields are not overwritten if simply updating address.
+ * Contains special handling for email so those fields are not overwritten if simply updating address.
*/
const setBillingData = useCallback( ( newData ) => {
setCustomerData( ( prevState ) => {
@@ -130,10 +130,15 @@ export const useCustomerData = (): {
* Set shipping data.
*/
const setShippingAddress = useCallback( ( newData ) => {
- setCustomerData( ( prevState ) => ( {
- ...prevState,
- shippingAddress: newData,
- } ) );
+ setCustomerData( ( prevState ) => {
+ return {
+ ...prevState,
+ shippingAddress: {
+ ...prevState.shippingAddress,
+ ...newData,
+ },
+ };
+ } );
}, [] );
/**
diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx
index 83daeaea332..fde2834212b 100644
--- a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx
+++ b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx
@@ -55,8 +55,7 @@ export const useCheckoutContext = (): CheckoutStateContextType => {
/**
* Checkout state provider
- * This provides provides an api interface exposing checkout state for use with
- * cart or checkout blocks.
+ * This provides an API interface exposing checkout state for use with cart or checkout blocks.
*
* @param {Object} props Incoming props for the provider.
* @param {Object} props.children The children being wrapped.
diff --git a/assets/js/base/context/providers/cart-checkout/customer/index.js b/assets/js/base/context/providers/cart-checkout/customer/index.js
index 15c52c74ffc..abb6ff19682 100644
--- a/assets/js/base/context/providers/cart-checkout/customer/index.js
+++ b/assets/js/base/context/providers/cart-checkout/customer/index.js
@@ -1,12 +1,15 @@
/**
* External dependencies
*/
-import { createContext, useContext } from '@wordpress/element';
+import { createContext, useContext, useState } from '@wordpress/element';
+import { defaultAddressFields } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import { useCustomerData } from '../../../hooks/use-customer-data';
+import { useCheckoutContext } from '../checkout-state';
+import { useStoreCart } from '../../../hooks/cart/use-store-cart';
/**
* @typedef {import('@woocommerce/type-defs/contexts').CustomerDataContext} CustomerDataContext
@@ -44,6 +47,7 @@ export const defaultShippingAddress = {
state: '',
postcode: '',
country: '',
+ phone: '',
};
/**
@@ -54,6 +58,8 @@ const CustomerDataContext = createContext( {
shippingAddress: defaultShippingAddress,
setBillingData: () => null,
setShippingAddress: () => null,
+ shippingAsBilling: true,
+ setShippingAsBilling: () => null,
} );
/**
@@ -63,6 +69,18 @@ export const useCustomerDataContext = () => {
return useContext( CustomerDataContext );
};
+/**
+ * Compare two addresses and see if they are the same.
+ *
+ * @param {Object} address1 First address.
+ * @param {Object} address2 Second address.
+ */
+const isSameAddress = ( address1, address2 ) => {
+ return Object.keys( defaultAddressFields ).every(
+ ( field ) => address1[ field ] === address2[ field ]
+ );
+};
+
/**
* Customer Data context provider.
*
@@ -76,6 +94,13 @@ export const CustomerDataProvider = ( { children } ) => {
setBillingData,
setShippingAddress,
} = useCustomerData();
+ const { cartNeedsShipping: needsShipping } = useStoreCart();
+ const { customerId } = useCheckoutContext();
+ const [ shippingAsBilling, setShippingAsBilling ] = useState(
+ () =>
+ needsShipping &&
+ ( ! customerId || isSameAddress( shippingAddress, billingData ) )
+ );
/**
* @type {CustomerDataContext}
@@ -85,6 +110,8 @@ export const CustomerDataProvider = ( { children } ) => {
shippingAddress,
setBillingData,
setShippingAddress,
+ shippingAsBilling,
+ setShippingAsBilling,
};
return (
diff --git a/assets/js/base/context/providers/store-notices/context.js b/assets/js/base/context/providers/store-notices/context.js
index c488636ccfb..824d252242c 100644
--- a/assets/js/base/context/providers/store-notices/context.js
+++ b/assets/js/base/context/providers/store-notices/context.js
@@ -50,10 +50,10 @@ export const useStoreNoticesContext = () => {
* - Success
*
* @param {Object} props Incoming props for the component.
- * @param {React.ReactChildren} props.children The Elements wrapped by this component.
- * @param {string} props.className CSS class used.
- * @param {boolean} props.createNoticeContainer Whether to create a notice container or not.
- * @param {string} props.context The notice context for notices being rendered.
+ * @param {JSX.Element} props.children The Elements wrapped by this component.
+ * @param {string} [props.className] CSS class used.
+ * @param {boolean} [props.createNoticeContainer] Whether to create a notice container or not.
+ * @param {string} [props.context] The notice context for notices being rendered.
*/
export const StoreNoticesProvider = ( {
children,
diff --git a/assets/js/base/context/providers/validation/context.js b/assets/js/base/context/providers/validation/context.js
index 37b0dd7ac06..9ccfe77c308 100644
--- a/assets/js/base/context/providers/validation/context.js
+++ b/assets/js/base/context/providers/validation/context.js
@@ -41,7 +41,7 @@ export const useValidationContext = () => {
* for tracking validation.
*
* @param {Object} props Incoming props for the component.
- * @param {React.ReactChildren} props.children What react elements are wrapped by this component.
+ * @param {JSX.Element} props.children What react elements are wrapped by this component.
*/
export const ValidationContextProvider = ( { children } ) => {
const [ validationErrors, updateValidationErrors ] = useState( {} );
diff --git a/assets/js/base/hocs/with-scroll-to-top/index.js b/assets/js/base/hocs/with-scroll-to-top/index.js
deleted file mode 100644
index e57bc84b696..00000000000
--- a/assets/js/base/hocs/with-scroll-to-top/index.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * External dependencies
- */
-import { Component, createRef } from 'react';
-
-/**
- * Internal dependencies
- */
-import './style.scss';
-
-/**
- * HOC that provides a function to scroll to the top of the component.
- *
- * @param {Function} OriginalComponent Component being wrapped.
- */
-const withScrollToTop = ( OriginalComponent ) => {
- class WrappedComponent extends Component {
- constructor() {
- super();
-
- this.scrollPointRef = createRef();
- }
-
- scrollToTopIfNeeded = () => {
- const scrollPointRefYPosition = this.scrollPointRef.current.getBoundingClientRect()
- .bottom;
- const isScrollPointRefVisible =
- scrollPointRefYPosition >= 0 &&
- scrollPointRefYPosition <= window.innerHeight;
- if ( ! isScrollPointRefVisible ) {
- this.scrollPointRef.current.scrollIntoView();
- }
- };
-
- moveFocusToTop = ( focusableSelector ) => {
- const focusableElements = this.scrollPointRef.current.parentElement.querySelectorAll(
- focusableSelector
- );
- if ( focusableElements.length ) {
- focusableElements[ 0 ].focus();
- }
- };
-
- scrollToTop = ( args ) => {
- if ( ! window || ! Number.isFinite( window.innerHeight ) ) {
- return;
- }
-
- this.scrollToTopIfNeeded();
- if ( args && args.focusableSelector ) {
- this.moveFocusToTop( args.focusableSelector );
- }
- };
-
- render() {
- return (
- <>
-
-
- >
- );
- }
- }
-
- WrappedComponent.displayName = 'withScrollToTop';
-
- return WrappedComponent;
-};
-
-export default withScrollToTop;
diff --git a/assets/js/base/hocs/with-scroll-to-top/index.tsx b/assets/js/base/hocs/with-scroll-to-top/index.tsx
new file mode 100644
index 00000000000..c9b9f8c513e
--- /dev/null
+++ b/assets/js/base/hocs/with-scroll-to-top/index.tsx
@@ -0,0 +1,77 @@
+/**
+ * External dependencies
+ */
+import { useRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+interface ScrollToTopProps {
+ focusableSelector?: string;
+}
+
+const maybeScrollToTop = ( scrollPoint: HTMLElement ): void => {
+ const yPos = scrollPoint.getBoundingClientRect().bottom;
+ const isScrollPointVisible = yPos >= 0 && yPos <= window.innerHeight;
+
+ if ( ! isScrollPointVisible ) {
+ scrollPoint.scrollIntoView();
+ }
+};
+
+const moveFocusToTop = (
+ scrollPoint: HTMLElement,
+ focusableSelector: string
+): void => {
+ const focusableElements =
+ scrollPoint.parentElement?.querySelectorAll( focusableSelector ) || [];
+
+ if ( focusableElements.length ) {
+ ( focusableElements[ 0 ] as HTMLElement )?.focus();
+ }
+};
+
+const scrollToHTMLElement = (
+ scrollPoint: HTMLElement,
+ { focusableSelector }: ScrollToTopProps
+): void => {
+ if ( ! window || ! Number.isFinite( window.innerHeight ) ) {
+ return;
+ }
+
+ maybeScrollToTop( scrollPoint );
+
+ if ( focusableSelector ) {
+ moveFocusToTop( scrollPoint, focusableSelector );
+ }
+};
+
+/**
+ * HOC that provides a function to scroll to the top of the component.
+ */
+const withScrollToTop = (
+ OriginalComponent: React.FunctionComponent< Record< string, unknown > >
+) => {
+ return ( props: Record< string, unknown > ): JSX.Element => {
+ const scrollPointRef = useRef< HTMLDivElement >( null );
+ const scrollToTop = ( args: ScrollToTopProps ) => {
+ if ( scrollPointRef.current !== null ) {
+ scrollToHTMLElement( scrollPointRef.current, args );
+ }
+ };
+ return (
+ <>
+
+
+ >
+ );
+ };
+};
+
+export default withScrollToTop;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts b/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts
new file mode 100644
index 00000000000..d6caf35dc0b
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+
+export const blockName = 'woocommerce/checkout-i2';
+export const blockAttributes = {
+ isPreview: {
+ type: 'boolean',
+ default: false,
+ save: false,
+ },
+ hasDarkControls: {
+ type: 'boolean',
+ default: getSetting( 'hasDarkEditorStyleSupport', false ),
+ },
+ showCompanyField: {
+ type: 'boolean',
+ default: false,
+ },
+ requireCompanyField: {
+ type: 'boolean',
+ default: false,
+ },
+ allowCreateAccount: {
+ type: 'boolean',
+ default: false,
+ },
+ showApartmentField: {
+ type: 'boolean',
+ default: true,
+ },
+ showPhoneField: {
+ type: 'boolean',
+ default: true,
+ },
+ requirePhoneField: {
+ type: 'boolean',
+ default: false,
+ },
+ // Deprecated - here for v1 migration support
+ showOrderNotes: {
+ type: 'boolean',
+ default: true,
+ },
+ showPolicyLinks: {
+ type: 'boolean',
+ default: true,
+ },
+ showReturnToCart: {
+ type: 'boolean',
+ default: true,
+ },
+ cartPageId: {
+ type: 'number',
+ default: 0,
+ },
+ showRateAfterTaxName: {
+ type: 'boolean',
+ default: getSetting( 'displayCartPricesIncludingTax', false ),
+ },
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/block.tsx
new file mode 100644
index 00000000000..f1a9e992031
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/block.tsx
@@ -0,0 +1,181 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import classnames from 'classnames';
+import { createInterpolateElement, useEffect } from '@wordpress/element';
+import { useStoreCart, useStoreNotices } from '@woocommerce/base-context/hooks';
+import {
+ useCheckoutContext,
+ useValidationContext,
+ ValidationContextProvider,
+ StoreNoticesProvider,
+ CheckoutProvider,
+} from '@woocommerce/base-context';
+import { StoreSnackbarNoticesProvider } from '@woocommerce/base-context/providers';
+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 { SlotFillProvider } from '@woocommerce/blocks-checkout';
+import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
+
+/**
+ * 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 LoginPrompt = () => {
+ return (
+ <>
+ { __(
+ 'You must be logged in to checkout. ',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { __(
+ 'Click here to log in.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ >
+ );
+};
+
+const Checkout = ( {
+ attributes,
+ children,
+}: {
+ attributes: Attributes;
+ children: React.ReactChildren;
+} ): JSX.Element => {
+ const { hasOrder, customerId } = useCheckoutContext();
+ const { cartItems, cartIsLoading } = useStoreCart();
+
+ const {
+ allowCreateAccount,
+ showCompanyField,
+ requireCompanyField,
+ showApartmentField,
+ showPhoneField,
+ requirePhoneField,
+ } = attributes;
+
+ if ( ! cartIsLoading && cartItems.length === 0 ) {
+ return ;
+ }
+
+ if ( ! hasOrder ) {
+ return ;
+ }
+
+ if (
+ isLoginRequired( customerId ) &&
+ allowCreateAccount &&
+ getSetting( 'checkoutAllowsSignup', false )
+ ) {
+ ;
+ }
+
+ return (
+
+ { children }
+
+ );
+};
+
+const ScrollOnError = ( {
+ scrollToTop,
+}: {
+ scrollToTop: ( props: Record< string, unknown > ) => void;
+} ): null => {
+ const { hasNoticesOfType } = useStoreNotices();
+ const {
+ hasError: checkoutHasError,
+ isIdle: checkoutIsIdle,
+ } = useCheckoutContext();
+ const {
+ hasValidationErrors,
+ showAllValidationErrors,
+ } = useValidationContext();
+
+ const hasErrorsToDisplay =
+ checkoutIsIdle &&
+ checkoutHasError &&
+ ( hasValidationErrors || hasNoticesOfType( 'default' ) );
+
+ useEffect( () => {
+ if ( hasErrorsToDisplay ) {
+ showAllValidationErrors();
+ scrollToTop( { focusableSelector: 'input:invalid' } );
+ }
+ }, [ hasErrorsToDisplay, scrollToTop, showAllValidationErrors ] );
+
+ return null;
+};
+
+const Block = ( {
+ attributes,
+ children,
+ scrollToTop,
+}: {
+ attributes: Attributes;
+ children: React.ReactChildren;
+ scrollToTop: ( props: Record< string, unknown > ) => void;
+} ): JSX.Element => (
+ Try reloading the page. If the error persists, please get in touch with us so we can assist.',
+ 'woo-gutenberg-products-block'
+ ),
+ {
+ button: (
+
+ ),
+ }
+ ) }
+ showErrorMessage={ CURRENT_USER_IS_ADMIN }
+ >
+
+
+
+
+
+
+
+ { children }
+
+
+
+
+
+
+
+
+
+);
+
+export default withScrollToTop( Block );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/constants.js b/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/constants.js
new file mode 100644
index 00000000000..e564d2bdb5f
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/constants.js
@@ -0,0 +1,8 @@
+export const PRODUCT_OUT_OF_STOCK = 'woocommerce_product_out_of_stock';
+export const PRODUCT_NOT_PURCHASABLE =
+ 'woocommerce_rest_cart_product_is_not_purchasable';
+export const PRODUCT_NOT_ENOUGH_STOCK =
+ 'woocommerce_rest_cart_product_no_stock';
+export const PRODUCT_SOLD_INDIVIDUALLY =
+ 'woocommerce_rest_cart_product_sold_individually';
+export const GENERIC_CART_ITEM_ERROR = 'woocommerce_rest_cart_item_error';
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/index.js b/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/index.js
new file mode 100644
index 00000000000..3e283360add
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/checkout-order-error/index.js
@@ -0,0 +1,136 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { CART_URL } from '@woocommerce/block-settings';
+import { Icon, removeCart } from '@woocommerce/icons';
+import { getSetting } from '@woocommerce/settings';
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Internal dependencies
+ */
+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,
+];
+
+/**
+ * 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 preloadedApiRequests = getSetting( 'preloadedApiRequests', {} );
+ const checkoutData = {
+ code: '',
+ message: '',
+ ...( preloadedApiRequests[ '/wc/store/checkout' ]?.body || {} ),
+ };
+
+ 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.',
+ 'woo-gutenberg-products-block'
+ ),
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+/**
+ * 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', 'woo-gutenberg-products-block' );
+
+ if ( cartItemErrorCodes.includes( errorData.code ) ) {
+ heading = __(
+ 'There is a problem with your cart',
+ 'woo-gutenberg-products-block'
+ );
+ }
+
+ return (
+ { heading }
+ );
+};
+
+/**
+ * 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.',
+ 'woo-gutenberg-products-block'
+ );
+ }
+
+ return { message }
;
+};
+
+/**
+ * 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', 'woo-gutenberg-products-block' );
+ let buttonUrl = 'javascript:window.location.reload(true)';
+
+ if ( cartItemErrorCodes.includes( errorData.code ) ) {
+ buttonText = __( 'Edit your cart', 'woo-gutenberg-products-block' );
+ buttonUrl = CART_URL;
+ }
+
+ return (
+
+
+ { buttonText }
+
+
+ );
+};
+
+export default CheckoutOrderError;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/columns/columns-block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/columns/columns-block.tsx
new file mode 100644
index 00000000000..d0ea19ba6aa
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/columns/columns-block.tsx
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+export const Columns = ( {
+ children,
+ ...props
+}: {
+ children?: React.ReactNode;
+} ): JSX.Element => {
+ const blockProps = useBlockProps( props );
+
+ return { children }
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/columns/index.ts b/assets/js/blocks/cart-checkout/checkout-i2/columns/index.ts
new file mode 100644
index 00000000000..805a5f6d232
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/columns/index.ts
@@ -0,0 +1 @@
+export * from './columns-block';
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/context.ts b/assets/js/blocks/cart-checkout/checkout-i2/context.ts
new file mode 100644
index 00000000000..b539edbc7a3
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/context.ts
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+import { createContext, useContext } from '@wordpress/element';
+
+/**
+ * Context consumed by inner blocks.
+ */
+export type CheckoutBlockContextProps = {
+ allowCreateAccount: boolean;
+ showCompanyField: boolean;
+ showApartmentField: boolean;
+ showPhoneField: boolean;
+ requireCompanyField: boolean;
+ requirePhoneField: boolean;
+};
+
+export type CheckoutBlockControlsContextProps = {
+ addressFieldControls: () => JSX.Element | null;
+ accountControls: () => JSX.Element | null;
+};
+
+export const CheckoutBlockContext = createContext< CheckoutBlockContextProps >(
+ {
+ allowCreateAccount: false,
+ showCompanyField: false,
+ showApartmentField: false,
+ showPhoneField: false,
+ requireCompanyField: false,
+ requirePhoneField: false,
+ }
+);
+
+export const CheckoutBlockControlsContext = createContext<
+ CheckoutBlockControlsContextProps
+>( {
+ addressFieldControls: () => null,
+ accountControls: () => null,
+} );
+
+export const useCheckoutBlockContext = (): CheckoutBlockContextProps => {
+ return useContext( CheckoutBlockContext );
+};
+
+export const useCheckoutBlockControlsContext = (): CheckoutBlockControlsContextProps => {
+ return useContext( CheckoutBlockControlsContext );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/edit.tsx
new file mode 100644
index 00000000000..86032a65b28
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/edit.tsx
@@ -0,0 +1,318 @@
+/**
+ * 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,
+ useEditorContext,
+} from '@woocommerce/base-context';
+import {
+ previewCart,
+ previewSavedPaymentMethods,
+} from '@woocommerce/resource-previews';
+import {
+ PanelBody,
+ ToggleControl,
+ CheckboxControl,
+ Notice,
+} from '@wordpress/components';
+import { CartCheckoutFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
+import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
+import { createInterpolateElement } from '@wordpress/element';
+import { getAdminLink } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import './styles/editor.scss';
+import { Columns } from './columns';
+import { addClassToBody } from './hacks';
+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',
+];
+
+const BlockSettings = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: Attributes;
+ setAttributes: ( attributes: Record< string, unknown > ) => undefined;
+} ): JSX.Element => {
+ const { hasDarkControls } = attributes;
+ const { currentPostId } = useEditorContext();
+
+ return (
+
+ { currentPostId !== CHECKOUT_PAGE_ID && (
+
+ { createInterpolateElement(
+ __(
+ 'If you would like to use this block as your default checkout you must update your page settings in WooCommerce .',
+ 'woo-gutenberg-products-block'
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+
+ ),
+ }
+ ) }
+
+ ) }
+
+
+ setAttributes( {
+ hasDarkControls: ! hasDarkControls,
+ } )
+ }
+ />
+
+
+
+ );
+};
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: Attributes;
+ setAttributes: ( attributes: Record< string, unknown > ) => undefined;
+} ): JSX.Element => {
+ const {
+ allowCreateAccount,
+ showCompanyField,
+ requireCompanyField,
+ showApartmentField,
+ showPhoneField,
+ requirePhoneField,
+ showOrderNotes,
+ showPolicyLinks,
+ showReturnToCart,
+ showRateAfterTaxName,
+ cartPageId,
+ } = attributes;
+
+ const defaultInnerBlocksTemplate = [
+ [
+ 'woocommerce/checkout-fields-block',
+ {},
+ [
+ [ 'woocommerce/checkout-express-payment-block', {}, [] ],
+ [ 'woocommerce/checkout-contact-information-block', {}, [] ],
+ [ 'woocommerce/checkout-shipping-address-block', {}, [] ],
+ [ 'woocommerce/checkout-billing-address-block', {}, [] ],
+ [ 'woocommerce/checkout-shipping-methods-block', {}, [] ],
+ [ 'woocommerce/checkout-payment-block', {}, [] ],
+ showOrderNotes
+ ? [ 'woocommerce/checkout-order-note-block', {}, [] ]
+ : false,
+ showPolicyLinks
+ ? [ 'woocommerce/checkout-terms-block', {}, [] ]
+ : false,
+ [
+ 'woocommerce/checkout-actions-block',
+ {
+ showReturnToCart,
+ cartPageId,
+ },
+ [],
+ ],
+ ].filter( Boolean ),
+ ],
+ [
+ 'woocommerce/checkout-totals-block',
+ {},
+ [
+ [
+ 'woocommerce/checkout-order-summary-block',
+ {
+ showRateAfterTaxName,
+ },
+ [],
+ ],
+ ],
+ ],
+ ];
+
+ const toggleAttribute = ( key: keyof Attributes ): void => {
+ const newAttributes = {} as Partial< Attributes >;
+ newAttributes[ key ] = ! ( attributes[ key ] as boolean );
+ setAttributes( newAttributes );
+ };
+
+ const accountControls = (): JSX.Element => (
+
+
+
+ setAttributes( {
+ allowCreateAccount: ! allowCreateAccount,
+ } )
+ }
+ />
+
+
+ );
+
+ const addressFieldControls = (): JSX.Element => (
+
+
+
+ { __(
+ 'Show or hide fields in the checkout address forms.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ toggleAttribute( 'showCompanyField' ) }
+ />
+ { showCompanyField && (
+
+ toggleAttribute( 'requireCompanyField' )
+ }
+ className="components-base-control--nested"
+ />
+ ) }
+ toggleAttribute( 'showApartmentField' ) }
+ />
+ toggleAttribute( 'showPhoneField' ) }
+ />
+ { showPhoneField && (
+
+ toggleAttribute( 'requirePhoneField' )
+ }
+ className="components-base-control--nested"
+ />
+ ) }
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/empty-cart/index.js b/assets/js/blocks/cart-checkout/checkout-i2/empty-cart/index.js
new file mode 100644
index 00000000000..2cd8ba11d5b
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/empty-cart/index.js
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { SHOP_URL } from '@woocommerce/block-settings';
+import { Icon, cart } from '@woocommerce/icons';
+
+const EmptyCart = () => {
+ return (
+
+
+
+ { __( 'Your cart is empty!', 'woo-gutenberg-products-block' ) }
+
+
+ { __(
+ "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.",
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { SHOP_URL && (
+
+
+ { __( 'Browse store', 'woo-gutenberg-products-block' ) }
+
+
+ ) }
+
+ );
+};
+
+export default EmptyCart;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/additional-fields.tsx b/assets/js/blocks/cart-checkout/checkout-i2/form-step/additional-fields.tsx
new file mode 100644
index 00000000000..c5cd7737649
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/additional-fields.tsx
@@ -0,0 +1,29 @@
+/**
+ * External dependencies
+ */
+import { InnerBlocks } from '@wordpress/block-editor';
+import {
+ RegisteredBlocks,
+ getRegisteredBlocks,
+} from '@woocommerce/blocks-checkout';
+
+/**
+ * Internal dependencies
+ */
+import './editor.scss';
+
+export const AdditionalFields = ( {
+ area,
+}: {
+ area: keyof RegisteredBlocks;
+} ): JSX.Element => {
+ return (
+
+
+
+ );
+};
+
+export const AdditionalFieldsContent = (): JSX.Element => (
+
+);
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/attributes.ts b/assets/js/blocks/cart-checkout/checkout-i2/form-step/attributes.ts
new file mode 100644
index 00000000000..cf723d62893
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/attributes.ts
@@ -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;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/editor.scss b/assets/js/blocks/cart-checkout/checkout-i2/form-step/editor.scss
new file mode 100644
index 00000000000..ce41661640a
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/editor.scss
@@ -0,0 +1,4 @@
+
+.wc-block-checkout__additional_fields {
+ margin: 1.5em 0 -1.5em;
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-block.tsx
new file mode 100644
index 00000000000..32c6e0223e3
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-block.tsx
@@ -0,0 +1,82 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import classnames from 'classnames';
+import { PlainText, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, ToggleControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import FormStepHeading from './form-step-heading';
+import { useBlockPropsWithLocking } from '../hacks';
+export interface FormStepBlockProps {
+ attributes: { title: string; description: string; showStepNumber: boolean };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+ className?: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * Form Step Block for use in the editor.
+ */
+export const FormStepBlock = ( {
+ attributes: { title = '', description = '', showStepNumber = true },
+ setAttributes,
+ className = '',
+ children,
+}: FormStepBlockProps ): JSX.Element => {
+ const blockProps = useBlockPropsWithLocking( {
+ className: classnames( 'wc-block-components-checkout-step', className, {
+ 'wc-block-components-checkout-step--with-step-number': showStepNumber,
+ } ),
+ } );
+
+ return (
+
+
+
+
+ setAttributes( {
+ showStepNumber: ! showStepNumber,
+ } )
+ }
+ />
+
+
+
+ setAttributes( { title: value } ) }
+ />
+
+
+
+
+ setAttributes( { description: value } )
+ }
+ />
+
+
+ { children }
+
+
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-heading.tsx b/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-heading.tsx
new file mode 100644
index 00000000000..852980518d3
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/form-step-heading.tsx
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import Title from '@woocommerce/base-components/title';
+
+/**
+ * Step Heading Component
+ */
+const FormStepHeading = ( {
+ children,
+ stepHeadingContent,
+}: {
+ children: JSX.Element;
+ stepHeadingContent?: JSX.Element;
+} ): JSX.Element => (
+
+
+ { children }
+
+ { !! stepHeadingContent && (
+
+ { stepHeadingContent }
+
+ ) }
+
+);
+
+export default FormStepHeading;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/form-step/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/form-step/index.tsx
new file mode 100644
index 00000000000..90fa4d9719b
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/form-step/index.tsx
@@ -0,0 +1,4 @@
+export * from './attributes';
+export * from './form-step-block';
+export * from './form-step-heading';
+export * from './additional-fields';
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/frontend.tsx
new file mode 100644
index 00000000000..bf8c7478e39
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/frontend.tsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { Children, cloneElement, isValidElement } from '@wordpress/element';
+import { getValidBlockAttributes } from '@woocommerce/base-utils';
+import { useStoreCart } from '@woocommerce/base-context';
+import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
+import {
+ withStoreCartApiHydration,
+ withRestApiHydration,
+} from '@woocommerce/block-hocs';
+import { renderParentBlock } from '@woocommerce/atomic-utils';
+
+/**
+ * Internal dependencies
+ */
+import './inner-blocks/register-components';
+import Block from './block';
+import { blockName, blockAttributes } from './attributes';
+
+const getProps = ( el: Element ) => {
+ return {
+ attributes: getValidBlockAttributes(
+ 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();
+
+ return Children.map( children, ( child ) => {
+ if ( isValidElement( child ) ) {
+ const componentProps = {
+ extensions,
+ cart,
+ };
+ return cloneElement( child, componentProps );
+ }
+ return child;
+ } );
+};
+
+renderParentBlock( {
+ Block: withStoreCartApiHydration( withRestApiHydration( Block ) ),
+ blockName,
+ selector: '.wp-block-woocommerce-checkout-i2',
+ getProps,
+ blockMap: getRegisteredBlockComponents( blockName ) as Record<
+ string,
+ React.ReactNode
+ >,
+ blockWrapper: Wrapper,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/hacks.ts b/assets/js/blocks/cart-checkout/checkout-i2/hacks.ts
new file mode 100644
index 00000000000..5d222b58795
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/hacks.ts
@@ -0,0 +1,153 @@
+/**
+ * HACKS
+ *
+ * This file contains functionality to "lock" blocks i.e. to prevent blocks being moved or deleted. This needs to be
+ * kept in place until native support for locking is available in WordPress (estimated WordPress 5.9).
+ */
+
+/**
+ * @todo Remove custom block locking (requires native WordPress support)
+ */
+
+/**
+ * External dependencies
+ */
+import {
+ useBlockProps,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { isTextField } from '@wordpress/dom';
+import { store as blocksStore } from '@wordpress/blocks';
+import { useSelect, subscribe, select as _select } from '@wordpress/data';
+import { useEffect, useRef } from '@wordpress/element';
+import { MutableRefObject } from 'react';
+import { BACKSPACE, DELETE } from '@wordpress/keycodes';
+
+/**
+ * Toggle class on body.
+ *
+ * @param {string} className CSS Class name.
+ * @param {boolean} add True to add, false to remove.
+ */
+const toggleBodyClass = ( className: string, add = true ) => {
+ if ( add ) {
+ window.document.body.classList.add( className );
+ } else {
+ window.document.body.classList.remove( className );
+ }
+};
+
+/**
+ * addClassToBody
+ *
+ * This components watches the current selected block and adds a class name to the body if that block is locked. If the
+ * current block is not locked, it removes the class name. The appended body class is used to hide UI elements to prevent
+ * the block from being deleted.
+ *
+ * We use a component so we can react to changes in the store.
+ */
+export const addClassToBody = (): void => {
+ subscribe( () => {
+ const blockEditorSelect = _select( blockEditorStore );
+
+ if ( ! blockEditorSelect ) {
+ return;
+ }
+
+ const selectedBlock = blockEditorSelect.getSelectedBlock();
+
+ if ( ! selectedBlock ) {
+ return;
+ }
+
+ const blockSelect = _select( blocksStore );
+
+ const selectedBlockType = blockSelect.getBlockType(
+ selectedBlock.name
+ );
+
+ toggleBodyClass(
+ 'wc-lock-selected-block--remove',
+ !! selectedBlockType?.supports?.lock?.remove
+ );
+
+ toggleBodyClass(
+ 'wc-lock-selected-block--move',
+ !! selectedBlockType?.supports?.lock?.move
+ );
+ } );
+};
+
+/**
+ * This is a hook we use in conjunction with useBlockProps. Its goal is to check if a block is locked (move or remove)
+ * and will stop the keydown event from propagating to stop it from being deleted via the keyboard.
+ *
+ * @todo Disable custom locking support if native support is detected.
+ */
+const useLockBlock = ( {
+ clientId,
+ ref,
+ type,
+}: {
+ clientId: string;
+ ref: MutableRefObject< Element >;
+ type: string;
+} ): void => {
+ const { isSelected, blockType } = useSelect(
+ ( select ) => {
+ return {
+ isSelected: select( blockEditorStore ).isBlockSelected(
+ clientId
+ ),
+ blockType: select( blocksStore ).getBlockType( type ),
+ };
+ },
+ [ clientId ]
+ );
+
+ const node = ref.current;
+
+ return useEffect( () => {
+ if ( ! isSelected || ! node ) {
+ return;
+ }
+ function onKeyDown( event: KeyboardEvent ) {
+ const { keyCode, target } = event;
+ if ( keyCode !== BACKSPACE && keyCode !== DELETE ) {
+ return;
+ }
+
+ if ( target !== node || isTextField( target ) ) {
+ return;
+ }
+
+ // Prevent the keyboard event from propogating if it supports locking.
+ if ( blockType?.supports?.lock?.remove ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ node.addEventListener( 'keydown', onKeyDown, true );
+
+ return () => {
+ node.removeEventListener( 'keydown', onKeyDown, true );
+ };
+ }, [ node, isSelected, blockType ] );
+};
+
+/**
+ * This hook is a light wrapper to useBlockProps, it wraps that hook plus useLockBlock to pass data between them.
+ */
+export const useBlockPropsWithLocking = (
+ props?: Record< string, unknown > = {}
+): Record< string, unknown > => {
+ const ref = useRef< Element >();
+ const blockProps = useBlockProps( { ref, ...props } );
+ useLockBlock( {
+ ref,
+ type: blockProps[ 'data-type' ],
+ clientId: blockProps[ 'data-block' ],
+ } );
+ return blockProps;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/index.tsx
new file mode 100644
index 00000000000..3a1530a9e17
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/index.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, card } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+import { createBlock } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import { blockName, blockAttributes } from './attributes';
+import './inner-blocks';
+
+const settings = {
+ title: __( 'Checkout i2', 'woo-gutenberg-products-block' ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ category: 'woocommerce',
+ keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
+ description: __(
+ 'Display a checkout form so your customers can submit orders.',
+ 'woo-gutenberg-products-block'
+ ),
+ supports: {
+ align: [ 'wide', 'full' ],
+ html: false,
+ multiple: false,
+ },
+ attributes: blockAttributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+ transforms: {
+ to: [
+ {
+ type: 'block',
+ blocks: [ 'woocommerce/checkout' ],
+ transform: ( attributes ) => {
+ return createBlock( 'woocommerce/checkout', {
+ attributes,
+ } );
+ },
+ },
+ ],
+ from: [
+ {
+ type: 'block',
+ blocks: [ 'woocommerce/checkout-i2' ],
+ transform: ( attributes ) => {
+ return createBlock( 'woocommerce/checkout-i2', attributes );
+ },
+ },
+ ],
+ },
+};
+
+registerFeaturePluginBlockType( blockName, settings );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/attributes.tsx
new file mode 100644
index 00000000000..4ad24c7608e
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/attributes.tsx
@@ -0,0 +1,10 @@
+export default {
+ cartPageId: {
+ type: 'number',
+ default: 0,
+ },
+ showReturnToCart: {
+ type: 'boolean',
+ default: true,
+ },
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/block.tsx
new file mode 100644
index 00000000000..664dce5f1f9
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/block.tsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+import {
+ PlaceOrderButton,
+ ReturnToCartButton,
+} from '@woocommerce/base-components/cart-checkout';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+const Block = ( {
+ cartPageId,
+ showReturnToCart,
+}: {
+ cartPageId: number;
+ showReturnToCart: boolean;
+} ): JSX.Element => {
+ return (
+
+ { showReturnToCart && (
+
+ ) }
+
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/edit.tsx
new file mode 100644
index 00000000000..494d5cc866f
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/edit.tsx
@@ -0,0 +1,97 @@
+/**
+ * External dependencies
+ */
+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, Disabled } from '@wordpress/components';
+import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import { useBlockPropsWithLocking } from '../../hacks';
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ showReturnToCart: boolean;
+ cartPageId: number;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const blockProps = useBlockPropsWithLocking();
+ const { cartPageId = 0, showReturnToCart = true } = attributes;
+ const { current: savedCartPageId } = useRef( cartPageId );
+ const currentPostId = useSelect(
+ ( select ) => {
+ if ( ! savedCartPageId ) {
+ const store = select( 'core/editor' );
+ return store.getCurrentPostId();
+ }
+ return savedCartPageId;
+ },
+ [ savedCartPageId ]
+ );
+
+ return (
+
+
+
+
+ setAttributes( {
+ showReturnToCart: ! showReturnToCart,
+ } )
+ }
+ />
+
+ { showReturnToCart &&
+ ! (
+ currentPostId === CHECKOUT_PAGE_ID &&
+ savedCartPageId === 0
+ ) && (
+
+ setAttributes( { cartPageId: id } )
+ }
+ labels={ {
+ title: __(
+ 'Return to Cart button',
+ 'woo-gutenberg-products-block'
+ ),
+ default: __(
+ 'WooCommerce Cart Page',
+ 'woo-gutenberg-products-block'
+ ),
+ } }
+ />
+ ) }
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/frontend.tsx
new file mode 100644
index 00000000000..37602e61842
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/frontend.tsx
@@ -0,0 +1,12 @@
+/**
+ * External dependencies
+ */
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+
+export default withFilteredAttributes( attributes )( Block );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/index.tsx
new file mode 100644
index 00000000000..fe88b33abdb
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/index.tsx
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import attributes from './attributes';
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-actions-block', {
+ title: __( 'Actions', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Checkout actions buttons block.',
+ 'woo-gutenberg-products-block'
+ ),
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/style.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/style.scss
new file mode 100644
index 00000000000..6e6a5e5faf1
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-actions-block/style.scss
@@ -0,0 +1,41 @@
+.wc-block-checkout__actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-left: 9px;
+
+ .wc-block-components-checkout-place-order-button {
+ width: 50%;
+ padding: 1em;
+ height: auto;
+
+ .wc-block-components-button__text {
+ line-height: 24px;
+
+ > 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);
+ margin-right: $gap-large;
+ padding: em($gap-large) 0;
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/attributes.tsx
new file mode 100644
index 00000000000..22f7179a592
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/attributes.tsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import formStepAttributes from '../../form-step/attributes';
+
+export default {
+ ...formStepAttributes( {
+ defaultTitle: __( 'Billing address', 'woo-gutenberg-products-block' ),
+ defaultDescription: __(
+ 'Enter the address that matches your card or payment method.',
+ 'woo-gutenberg-products-block'
+ ),
+ } ),
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/block.tsx
new file mode 100644
index 00000000000..538645c95bc
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/block.tsx
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { useMemo, useEffect, Fragment } from '@wordpress/element';
+import { Disabled } from '@wordpress/components';
+import {
+ useCheckoutAddress,
+ useStoreEvents,
+ useEditorContext,
+} from '@woocommerce/base-context';
+import { AddressForm } from '@woocommerce/base-components/cart-checkout';
+
+/**
+ * Internal dependencies
+ */
+import PhoneNumber from '../../phone-number';
+
+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 {
+ defaultAddressFields,
+ billingFields,
+ setBillingFields,
+ setPhone,
+ } = useCheckoutAddress();
+ const { dispatchCheckoutEvent } = useStoreEvents();
+ const { isEditor } = useEditorContext();
+
+ // Clears data if fields are hidden.
+ useEffect( () => {
+ if ( ! showPhoneField ) {
+ setPhone( '' );
+ }
+ }, [ showPhoneField, setPhone ] );
+
+ const addressFieldsConfig = useMemo( () => {
+ return {
+ company: {
+ hidden: ! showCompanyField,
+ required: requireCompanyField,
+ },
+ address_2: {
+ hidden: ! showApartmentField,
+ },
+ };
+ }, [ showCompanyField, requireCompanyField, showApartmentField ] );
+
+ const AddressFormWrapperComponent = isEditor ? Disabled : Fragment;
+
+ return (
+
+ ) => {
+ setBillingFields( values );
+ dispatchCheckoutEvent( 'set-billing-address' );
+ } }
+ values={ billingFields }
+ fields={ Object.keys( defaultAddressFields ) }
+ fieldConfig={ addressFieldsConfig }
+ />
+ { showPhoneField && (
+ {
+ setPhone( value );
+ dispatchCheckoutEvent( 'set-phone-number', {
+ step: 'billing',
+ } );
+ } }
+ />
+ ) }
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/edit.tsx
new file mode 100644
index 00000000000..07cc5ddb931
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/edit.tsx
@@ -0,0 +1,73 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FormStepBlock,
+ AdditionalFields,
+ AdditionalFieldsContent,
+} from '../../form-step';
+import {
+ useCheckoutBlockContext,
+ useCheckoutBlockControlsContext,
+} from '../../context';
+import Block from './block';
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element | null => {
+ const {
+ showCompanyField,
+ showApartmentField,
+ requireCompanyField,
+ showPhoneField,
+ requirePhoneField,
+ } = useCheckoutBlockContext();
+ const {
+ addressFieldControls: Controls,
+ } = useCheckoutBlockControlsContext();
+ const { showBillingFields } = useCheckoutAddress();
+
+ if ( ! showBillingFields ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/frontend.tsx
new file mode 100644
index 00000000000..6d5232514a8
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/frontend.tsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import { useCheckoutContext } from '@woocommerce/base-context';
+import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+import { useCheckoutBlockContext } from '../../context';
+
+const FrontendBlock = ( {
+ title,
+ description,
+ showStepNumber,
+ children,
+}: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ children: JSX.Element;
+} ): JSX.Element | null => {
+ const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
+ const { showBillingFields } = useCheckoutAddress();
+ const {
+ requireCompanyField,
+ requirePhoneField,
+ showApartmentField,
+ showCompanyField,
+ showPhoneField,
+ } = useCheckoutBlockContext();
+
+ if ( ! showBillingFields ) {
+ return null;
+ }
+
+ return (
+
+
+ { children }
+
+ );
+};
+
+export default withFilteredAttributes( attributes )( FrontendBlock );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/index.tsx
new file mode 100644
index 00000000000..53e6fd0aea2
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-billing-address-block/index.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, address } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-billing-address-block', {
+ title: __( 'Billing Address', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Manage your address requirements.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/attributes.tsx
new file mode 100644
index 00000000000..6de11ac520f
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/attributes.tsx
@@ -0,0 +1,22 @@
+/**
+ * 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'
+ ),
+ } ),
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/block.tsx
new file mode 100644
index 00000000000..6006f38f8e1
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/block.tsx
@@ -0,0 +1,67 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
+import {
+ useCheckoutContext,
+ useCheckoutAddress,
+ useStoreEvents,
+} from '@woocommerce/base-context';
+import { getSetting } from '@woocommerce/settings';
+import CheckboxControl from '@woocommerce/base-components/checkbox-control';
+
+/**
+ * Internal dependencies
+ */
+
+const Block = ( {
+ allowCreateAccount,
+}: {
+ allowCreateAccount: boolean;
+} ): JSX.Element => {
+ const {
+ customerId,
+ shouldCreateAccount,
+ setShouldCreateAccount,
+ } = useCheckoutContext();
+ const { billingFields, setEmail } = useCheckoutAddress();
+ const { dispatchCheckoutEvent } = useStoreEvents();
+
+ const onChangeEmail = ( value ) => {
+ setEmail( value );
+ dispatchCheckoutEvent( 'set-email-address' );
+ };
+
+ const createAccountUI = ! customerId &&
+ allowCreateAccount &&
+ getSetting( 'checkoutAllowsGuest', false ) &&
+ getSetting( 'checkoutAllowsSignup', false ) && (
+ setShouldCreateAccount( value ) }
+ />
+ );
+
+ return (
+ <>
+
+ { createAccountUI }
+ >
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/edit.tsx
new file mode 100644
index 00000000000..bff3beed6ae
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/edit.tsx
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import { Disabled } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FormStepBlock,
+ AdditionalFields,
+ AdditionalFieldsContent,
+} from '../../form-step';
+import Block from './block';
+import {
+ useCheckoutBlockContext,
+ useCheckoutBlockControlsContext,
+} from '../../context';
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const { allowCreateAccount } = useCheckoutBlockContext();
+ const { accountControls: Controls } = useCheckoutBlockControlsContext();
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/frontend.tsx
new file mode 100644
index 00000000000..f9385217932
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/frontend.tsx
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import { useCheckoutContext } from '@woocommerce/base-context';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+import LoginPrompt from './login-prompt';
+import { useCheckoutBlockContext } from '../../context';
+
+const FrontendBlock = ( {
+ title,
+ description,
+ showStepNumber,
+ children,
+}: {
+ title: string;
+ description: string;
+ allowCreateAccount: boolean;
+ showStepNumber: boolean;
+ children: JSX.Element;
+} ) => {
+ const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
+ const { allowCreateAccount } = useCheckoutBlockContext();
+
+ return (
+ }
+ >
+
+ { children }
+
+ );
+};
+
+export default withFilteredAttributes( attributes )( FrontendBlock );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/index.tsx
new file mode 100644
index 00000000000..fb1cc395fc5
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/index.tsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, contact } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType(
+ 'woocommerce/checkout-contact-information-block',
+ {
+ title: __( 'Contact Information', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ "Get your customer's contact email.",
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+ }
+);
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/login-prompt.js b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/login-prompt.js
new file mode 100644
index 00000000000..747444f37f5
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-contact-information-block/login-prompt.js
@@ -0,0 +1,33 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { getSetting } from '@woocommerce/settings';
+import { useCheckoutContext } from '@woocommerce/base-context';
+import { LOGIN_URL } from '@woocommerce/block-settings';
+
+const LOGIN_TO_CHECKOUT_URL = `${ LOGIN_URL }?redirect_to=${ encodeURIComponent(
+ window.location.href
+) }`;
+
+const LoginPrompt = () => {
+ const { customerId } = useCheckoutContext();
+
+ if ( ! getSetting( 'checkoutShowLoginReminder', true ) || customerId ) {
+ return null;
+ }
+
+ return (
+ <>
+ { __(
+ 'Already have an account? ',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { __( 'Log in.', 'woo-gutenberg-products-block' ) }
+
+ >
+ );
+};
+
+export default LoginPrompt;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/block.tsx
new file mode 100644
index 00000000000..9e9fa39362c
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/block.tsx
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+import { useStoreCart } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import { CheckoutExpressPayment } from '../../../payment-methods';
+
+const Block = (): JSX.Element | null => {
+ const { cartNeedsPayment } = useStoreCart();
+
+ if ( ! cartNeedsPayment ) {
+ return null;
+ }
+
+ return ;
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/edit.tsx
new file mode 100644
index 00000000000..4112f617c8e
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/edit.tsx
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import './editor.scss';
+import { useBlockPropsWithLocking } from '../../hacks';
+
+export const Edit = (): JSX.Element => {
+ const blockProps = useBlockPropsWithLocking();
+
+ return (
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/editor.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/editor.scss
new file mode 100644
index 00000000000..25ef9d7c162
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/editor.scss
@@ -0,0 +1,11 @@
+// Adjust padding and margins in the editor to improve selected block outlines.
+.wp-block-woocommerce-checkout-express-payment-block {
+ margin-top: 14px;
+ margin-bottom: 14px;
+ padding-top: 14px;
+ padding-bottom: 14px;
+
+ .wc-block-components-express-payment-continue-rule--checkout {
+ margin-bottom: 0;
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/index.tsx
new file mode 100644
index 00000000000..66b9c2b2756
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-express-payment-block/index.tsx
@@ -0,0 +1,40 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, card } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-express-payment-block', {
+ title: __( 'Express Checkout', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Provide an express payment option for your customers.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes: {},
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/edit.tsx
new file mode 100644
index 00000000000..346eae568cd
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/edit.tsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+import { Main } from '@woocommerce/base-components/sidebar-layout';
+import { getRegisteredBlocks } from '@woocommerce/blocks-checkout';
+
+/**
+ * Internal dependencies
+ */
+import { useCheckoutBlockControlsContext } from '../../context';
+
+const ALLOWED_BLOCKS = [
+ 'woocommerce/checkout-express-payment-block',
+ 'woocommerce/checkout-shipping-address-block',
+ 'woocommerce/checkout-shipping-methods-block',
+ 'woocommerce/checkout-contact-information-block',
+ 'woocommerce/checkout-billing-address-block',
+ 'woocommerce/checkout-payment-block',
+ 'woocommerce/checkout-order-note-block',
+ 'woocommerce/checkout-actions-block',
+ 'woocommerce/checkout-terms-block',
+ 'core/paragraph',
+ 'core/heading',
+ 'core/separator',
+ ...getRegisteredBlocks( 'fields' ),
+];
+
+export const Edit = (): JSX.Element => {
+ const blockProps = useBlockProps();
+ const {
+ addressFieldControls: Controls,
+ } = useCheckoutBlockControlsContext();
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/frontend.tsx
new file mode 100644
index 00000000000..4e1d1455146
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/frontend.tsx
@@ -0,0 +1,20 @@
+/**
+ * External dependencies
+ */
+import { Main } from '@woocommerce/base-components/sidebar-layout';
+
+const FrontendBlock = ( {
+ children,
+}: {
+ children: JSX.Element;
+} ): JSX.Element => {
+ return (
+
+
+
+ );
+};
+
+export default FrontendBlock;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/index.tsx
new file mode 100644
index 00000000000..fa1a13f0208
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-fields-block/index.tsx
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-fields-block', {
+ title: __( 'Checkout Fields Block', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Wrapper block for checkout fields',
+ 'woo-gutenberg-products-block'
+ ),
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ },
+ parent: [ 'woocommerce/checkout-i2' ],
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/block.tsx
new file mode 100644
index 00000000000..06c31891aeb
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/block.tsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import {
+ useCheckoutContext,
+ useShippingDataContext,
+} from '@woocommerce/base-context';
+
+/**
+ * Internal dependencies
+ */
+import CheckoutOrderNotes from '../../../checkout/form/order-notes';
+
+const Block = (): JSX.Element => {
+ const { needsShipping } = useShippingDataContext();
+ const {
+ isProcessing: checkoutIsProcessing,
+ orderNotes,
+ dispatchActions,
+ } = useCheckoutContext();
+ const { setOrderNotes } = dispatchActions;
+
+ return (
+
+
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/edit.tsx
new file mode 100644
index 00000000000..3f9a6048feb
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/edit.tsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import { Disabled } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import './editor.scss';
+import { useBlockPropsWithLocking } from '../../hacks';
+
+export const Edit = (): JSX.Element => {
+ const blockProps = useBlockPropsWithLocking();
+ return (
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/editor.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/editor.scss
new file mode 100644
index 00000000000..edda61aaa3d
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/editor.scss
@@ -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;
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/index.tsx
new file mode 100644
index 00000000000..5350fe4024d
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-note-block/index.tsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, notes } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-order-note-block', {
+ title: __( 'Order Note', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Allow customers to add a note to their order.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes: {},
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/attributes.tsx
new file mode 100644
index 00000000000..d1cf9e82c57
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/attributes.tsx
@@ -0,0 +1,11 @@
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+
+export default {
+ showRateAfterTaxName: {
+ type: 'boolean',
+ default: getSetting( 'displayCartPricesIncludingTax', false ),
+ },
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/block.tsx
new file mode 100644
index 00000000000..b0b219cb5fd
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/block.tsx
@@ -0,0 +1,111 @@
+/**
+ * External dependencies
+ */
+import {
+ OrderSummary,
+ TotalsCoupon,
+ TotalsDiscount,
+ TotalsFooterItem,
+ TotalsShipping,
+} from '@woocommerce/base-components/cart-checkout';
+import {
+ Subtotal,
+ TotalsFees,
+ TotalsTaxes,
+ ExperimentalOrderMeta,
+ TotalsWrapper,
+} from '@woocommerce/blocks-checkout';
+
+import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
+import { useShippingDataContext } from '@woocommerce/base-context';
+import {
+ useStoreCartCoupons,
+ useStoreCart,
+} from '@woocommerce/base-context/hooks';
+import { getSetting } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+
+const Block = ( {
+ showRateAfterTaxName = false,
+}: {
+ showRateAfterTaxName: boolean;
+} ): JSX.Element => {
+ const { cartItems, cartTotals, cartCoupons, cartFees } = useStoreCart();
+ const {
+ applyCoupon,
+ removeCoupon,
+ isApplyingCoupon,
+ isRemovingCoupon,
+ } = useStoreCartCoupons();
+
+ const { needsShipping } = useShippingDataContext();
+ const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
+
+ // 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,
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ { getSetting( 'couponsEnabled', true ) && (
+
+
+
+ ) }
+ { needsShipping && (
+
+
+
+ ) }
+ { ! getSetting( 'displayCartPricesIncludingTax', false ) && (
+
+
+
+ ) }
+
+
+
+
+ >
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/edit.tsx
new file mode 100644
index 00000000000..1c6e06a8e2c
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/edit.tsx
@@ -0,0 +1,72 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, ToggleControl } from '@wordpress/components';
+import { getSetting } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import { useBlockPropsWithLocking } from '../../hacks';
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ showRateAfterTaxName: boolean;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const blockProps = useBlockPropsWithLocking();
+ const taxesEnabled = getSetting( 'taxesEnabled' ) as boolean;
+ const displayItemizedTaxes = getSetting(
+ 'displayItemizedTaxes',
+ false
+ ) as boolean;
+ const displayCartPricesIncludingTax = getSetting(
+ 'displayCartPricesIncludingTax',
+ false
+ ) as boolean;
+ return (
+
+
+ { taxesEnabled &&
+ displayItemizedTaxes &&
+ ! displayCartPricesIncludingTax && (
+
+
+ setAttributes( {
+ showRateAfterTaxName: ! attributes.showRateAfterTaxName,
+ } )
+ }
+ />
+
+ ) }
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/index.tsx
new file mode 100644
index 00000000000..700d92d4d7d
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-order-summary-block/index.tsx
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-order-summary-block', {
+ title: __( 'Order Summary', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Displays the order summary and totals.',
+ 'woo-gutenberg-products-block'
+ ),
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-totals-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/attributes.tsx
new file mode 100644
index 00000000000..6c2bffb9e09
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/attributes.tsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import formStepAttributes from '../../form-step/attributes';
+
+export default {
+ ...formStepAttributes( {
+ defaultTitle: __( 'Payment Method', 'woo-gutenberg-products-block' ),
+ defaultDescription: __(
+ 'Select a payment method below.',
+ 'woo-gutenberg-products-block'
+ ),
+ } ),
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/block.tsx
new file mode 100644
index 00000000000..5a654934704
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/block.tsx
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+import { StoreNoticesProvider } from '@woocommerce/base-context';
+import { useEmitResponse } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import { PaymentMethods } from '../../../payment-methods';
+
+const Block = (): JSX.Element | null => {
+ const { noticeContexts } = useEmitResponse();
+
+ return (
+
+
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/edit.tsx
new file mode 100644
index 00000000000..44fc74e1668
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/edit.tsx
@@ -0,0 +1,86 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
+import { PanelBody, Disabled, ExternalLink } from '@wordpress/components';
+import { ADMIN_URL, getSetting } from '@woocommerce/settings';
+import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FormStepBlock,
+ FormStepBlockProps,
+ AdditionalFields,
+ AdditionalFieldsContent,
+} from '../../form-step';
+import Block from './block';
+
+type paymentAdminLink = {
+ id: number;
+ title: string;
+ description: string;
+};
+
+export const Edit = ( props: FormStepBlockProps ): JSX.Element => {
+ const globalPaymentMethods = getSetting(
+ 'globalPaymentMethods'
+ ) as paymentAdminLink[];
+
+ return (
+
+
+ { globalPaymentMethods.length > 0 && (
+
+
+ { __(
+ 'You currently have the following payment integrations active.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { globalPaymentMethods.map( ( method ) => {
+ return (
+
+ );
+ } ) }
+
+ { __(
+ 'Manage payment methods',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ ) }
+
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/frontend.tsx
new file mode 100644
index 00000000000..9f9ebd47ff7
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/frontend.tsx
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { useStoreCart, useEmitResponse } from '@woocommerce/base-context/hooks';
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import {
+ useCheckoutContext,
+ StoreNoticesProvider,
+} from '@woocommerce/base-context';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+
+const FrontendBlock = ( {
+ title,
+ description,
+ showStepNumber,
+ children,
+}: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ children: JSX.Element;
+} ) => {
+ const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
+ const { cartNeedsPayment } = useStoreCart();
+ const { noticeContexts } = useEmitResponse();
+
+ if ( ! cartNeedsPayment ) {
+ return null;
+ }
+ return (
+
+
+
+
+ { children }
+
+ );
+};
+
+export default withFilteredAttributes( attributes )( FrontendBlock );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/index.tsx
new file mode 100644
index 00000000000..b74f3e830a8
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-payment-block/index.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, card } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-payment-block', {
+ title: __( 'Payment Options', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Manage your payment options.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/README.md b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/README.md
new file mode 100644
index 00000000000..3fc7264bc89
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/README.md
@@ -0,0 +1,5 @@
+# Checkout Sample Block
+
+This block is a demonstration of the extensibility system. It will be removed to a tutorial once finalized.
+
+For documentation relating to the block registration system on the checkout, see /packages/checkout/blocks-registry.
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/block.tsx
new file mode 100644
index 00000000000..bfc227ec7de
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/block.tsx
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+const Block = (): JSX.Element => {
+ return (
+
+
+ This sample block is a demonstration of the Checkout integration
+ interfaces.
+
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/edit.tsx
new file mode 100644
index 00000000000..8cbb9681286
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/edit.tsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, Disabled } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+
+export const Edit = (): JSX.Element => {
+ return (
+ <>
+
+
+ Options for the block go here.
+
+
+
+
+
+ >
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/frontend.tsx
new file mode 100644
index 00000000000..5222199a8c3
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/frontend.tsx
@@ -0,0 +1,10 @@
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+
+const FrontendBlock = (): JSX.Element => {
+ return ;
+};
+
+export default FrontendBlock;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/index.tsx
new file mode 100644
index 00000000000..45d6278f184
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-sample-block/index.tsx
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, asterisk } from '@woocommerce/icons';
+import { registerCheckoutBlock } from '@woocommerce/blocks-checkout';
+import { lazy } from '@wordpress/element';
+import { WC_BLOCKS_BUILD_URL } from '@woocommerce/block-settings';
+
+// Modify webpack publicPath at runtime based on location of WordPress Plugin.
+// eslint-disable-next-line no-undef,camelcase
+__webpack_public_path__ = WC_BLOCKS_BUILD_URL;
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+// @todo Sample block should only be visible in correct areas, not top level.
+registerCheckoutBlock( 'woocommerce/checkout-sample-block', {
+ component: lazy( () =>
+ import( /* webpackChunkName: "checkout-blocks/sample" */ './frontend' )
+ ),
+ areas: [ 'shippingAddress', 'billingAddress', 'contactInformation' ],
+ configuration: {
+ title: __( 'Sample Block', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'A sample block showing how to integrate with Checkout i2.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: true,
+ reusable: false,
+ },
+ attributes: {},
+ edit: Edit,
+ save: Save,
+ },
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/attributes.tsx
new file mode 100644
index 00000000000..4631c94517c
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/attributes.tsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import formStepAttributes from '../../form-step/attributes';
+
+export default {
+ ...formStepAttributes( {
+ defaultTitle: __( 'Shipping address', 'woo-gutenberg-products-block' ),
+ defaultDescription: __(
+ 'Enter the address where you want your order delivered.',
+ 'woo-gutenberg-products-block'
+ ),
+ } ),
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/block.tsx
new file mode 100644
index 00000000000..5c4620747bb
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/block.tsx
@@ -0,0 +1,108 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useMemo, useEffect, Fragment } from '@wordpress/element';
+import { Disabled } from '@wordpress/components';
+import { AddressForm } from '@woocommerce/base-components/cart-checkout';
+import {
+ useCheckoutAddress,
+ useStoreEvents,
+ useEditorContext,
+} from '@woocommerce/base-context';
+import CheckboxControl from '@woocommerce/base-components/checkbox-control';
+
+/**
+ * Internal dependencies
+ */
+import PhoneNumber from '../../phone-number';
+
+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 {
+ defaultAddressFields,
+ setShippingFields,
+ shippingFields,
+ setShippingAsBilling,
+ shippingAsBilling,
+ setShippingPhone,
+ } = useCheckoutAddress();
+ const { dispatchCheckoutEvent } = useStoreEvents();
+ const { isEditor } = useEditorContext();
+
+ // Clears data if fields are hidden.
+ useEffect( () => {
+ if ( ! showPhoneField ) {
+ setShippingPhone( '' );
+ }
+ }, [ showPhoneField, setShippingPhone ] );
+
+ const addressFieldsConfig = useMemo( () => {
+ return {
+ company: {
+ hidden: ! showCompanyField,
+ required: requireCompanyField,
+ },
+ address_2: {
+ hidden: ! showApartmentField,
+ },
+ };
+ }, [ showCompanyField, requireCompanyField, showApartmentField ] );
+
+ const AddressFormWrapperComponent = isEditor ? Disabled : Fragment;
+
+ return (
+ <>
+
+ ) => {
+ setShippingFields( values );
+ dispatchCheckoutEvent( 'set-shipping-address' );
+ } }
+ values={ shippingFields }
+ fields={ Object.keys( defaultAddressFields ) }
+ fieldConfig={ addressFieldsConfig }
+ />
+ { showPhoneField && (
+ {
+ setShippingPhone( value );
+ dispatchCheckoutEvent( 'set-phone-number', {
+ step: 'shipping',
+ } );
+ } }
+ />
+ ) }
+
+
+ setShippingAsBilling( checked )
+ }
+ />
+ >
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/edit.tsx
new file mode 100644
index 00000000000..6d7d3f3469c
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/edit.tsx
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FormStepBlock,
+ AdditionalFields,
+ AdditionalFieldsContent,
+} from '../../form-step';
+import {
+ useCheckoutBlockContext,
+ useCheckoutBlockControlsContext,
+} from '../../context';
+import Block from './block';
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const {
+ showCompanyField,
+ showApartmentField,
+ requireCompanyField,
+ showPhoneField,
+ requirePhoneField,
+ } = useCheckoutBlockContext();
+ const {
+ addressFieldControls: Controls,
+ } = useCheckoutBlockControlsContext();
+ return (
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/frontend.tsx
new file mode 100644
index 00000000000..42323e3dc41
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/frontend.tsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import { useCheckoutContext } from '@woocommerce/base-context';
+import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+import { useCheckoutBlockContext } from '../../context';
+
+const FrontendBlock = ( {
+ title,
+ description,
+ showStepNumber,
+ children,
+}: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ children: JSX.Element;
+} ) => {
+ const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
+ const { showShippingFields } = useCheckoutAddress();
+ const {
+ requireCompanyField,
+ requirePhoneField,
+ showApartmentField,
+ showCompanyField,
+ showPhoneField,
+ } = useCheckoutBlockContext();
+
+ if ( ! showShippingFields ) {
+ return null;
+ }
+
+ return (
+
+
+ { children }
+
+ );
+};
+
+export default withFilteredAttributes( attributes )( FrontendBlock );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/index.tsx
new file mode 100644
index 00000000000..af83a348909
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-address-block/index.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, address } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-shipping-address-block', {
+ title: __( 'Shipping Address', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Manage your address requirements.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/attributes.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/attributes.tsx
new file mode 100644
index 00000000000..046ac76a5be
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/attributes.tsx
@@ -0,0 +1,23 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import formStepAttributes from '../../form-step/attributes';
+
+export default {
+ ...formStepAttributes( {
+ defaultTitle: __( 'Shipping options', 'woo-gutenberg-products-block' ),
+ defaultDescription: __(
+ 'Select shipping options below.',
+ 'woo-gutenberg-products-block'
+ ),
+ } ),
+ allowCreateAccount: {
+ type: 'boolean',
+ default: false,
+ },
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/block.tsx
new file mode 100644
index 00000000000..23654167e57
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/block.tsx
@@ -0,0 +1,99 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
+import { getShippingRatesPackageCount } from '@woocommerce/base-utils';
+import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
+import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
+import {
+ useEditorContext,
+ useShippingDataContext,
+} from '@woocommerce/base-context';
+import { decodeEntities } from '@wordpress/html-entities';
+import { Notice } from 'wordpress-components';
+import classnames from 'classnames';
+import { getSetting } from '@woocommerce/settings';
+import type { PackageRateOption } from '@woocommerce/type-defs/shipping';
+import type { CartShippingPackageShippingRate } from '@woocommerce/type-defs/cart';
+
+/**
+ * Internal dependencies
+ */
+import NoShippingPlaceholder from './no-shipping-placeholder';
+
+/**
+ * Renders a shipping rate control option.
+ *
+ * @param {Object} option Shipping Rate.
+ */
+const renderShippingRatesControlOption = (
+ option: CartShippingPackageShippingRate
+): PackageRateOption => {
+ const priceWithTaxes = getSetting( 'displayCartPricesIncludingTax', false )
+ ? parseInt( option.price, 10 ) + parseInt( option.taxes, 10 )
+ : parseInt( option.price, 10 );
+ return {
+ label: decodeEntities( option.name ),
+ value: option.rate_id,
+ description: decodeEntities( option.description ),
+ secondaryLabel: (
+
+ ),
+ secondaryDescription: decodeEntities( option.delivery_time ),
+ };
+};
+
+const Block = (): JSX.Element | null => {
+ const { isEditor } = useEditorContext();
+ const {
+ shippingRates,
+ shippingRatesLoading,
+ needsShipping,
+ hasCalculatedShipping,
+ } = useShippingDataContext();
+
+ if ( ! needsShipping ) {
+ return null;
+ }
+
+ return (
+ <>
+ { isEditor && ! getShippingRatesPackageCount( shippingRates ) ? (
+
+ ) : (
+
+ { __(
+ 'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ ) : (
+ __(
+ 'Shipping options will appear here after entering your full shipping address.',
+ 'woo-gutenberg-products-block'
+ )
+ )
+ }
+ renderOption={ renderShippingRatesControlOption }
+ shippingRates={ shippingRates }
+ shippingRatesLoading={ shippingRatesLoading }
+ />
+ ) }
+ >
+ );
+};
+
+export default Block;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/edit.tsx
new file mode 100644
index 00000000000..782b72f0a10
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/edit.tsx
@@ -0,0 +1,129 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
+import { PanelBody, Disabled, ExternalLink } from '@wordpress/components';
+import { ADMIN_URL, getSetting } from '@woocommerce/settings';
+import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FormStepBlock,
+ AdditionalFields,
+ AdditionalFieldsContent,
+} from '../../form-step';
+import Block from './block';
+
+type shippingAdminLink = {
+ id: number;
+ title: string;
+ description: string;
+};
+
+export const Edit = ( {
+ attributes,
+ setAttributes,
+}: {
+ attributes: {
+ title: string;
+ description: string;
+ showStepNumber: boolean;
+ allowCreateAccount: boolean;
+ };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const globalShippingMethods = getSetting(
+ 'globalShippingMethods'
+ ) as shippingAdminLink[];
+ const activeShippingZones = getSetting(
+ 'activeShippingZones'
+ ) as shippingAdminLink[];
+
+ return (
+
+
+ { globalShippingMethods.length > 0 && (
+
+
+ { __(
+ 'You currently have the following shipping integrations active.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { globalShippingMethods.map( ( method ) => {
+ return (
+
+ );
+ } ) }
+
+ { __(
+ 'Manage shipping methods',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ ) }
+ { activeShippingZones.length && (
+
+
+ { __(
+ 'You currently have the following shipping zones active.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ { activeShippingZones.map( ( zone ) => {
+ return (
+
+ );
+ } ) }
+
+ { __(
+ 'Manage shipping zones',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ ) }
+
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/frontend.tsx
new file mode 100644
index 00000000000..ce564d60c6e
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/frontend.tsx
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
+import { FormStep } from '@woocommerce/base-components/cart-checkout';
+import { useCheckoutContext } from '@woocommerce/base-context';
+import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import attributes from './attributes';
+
+const FrontendBlock = ( {
+ title,
+ description,
+ showStepNumber,
+ children,
+}: {
+ title: string;
+ description: string;
+ requireCompanyField: boolean;
+ requirePhoneField: boolean;
+ showApartmentField: boolean;
+ showCompanyField: boolean;
+ showPhoneField: boolean;
+ showStepNumber: boolean;
+ children: JSX.Element;
+} ) => {
+ const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
+ const { showShippingFields } = useCheckoutAddress();
+
+ if ( ! showShippingFields ) {
+ return null;
+ }
+
+ return (
+
+
+ { children }
+
+ );
+};
+
+export default withFilteredAttributes( attributes )( FrontendBlock );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/index.tsx
new file mode 100644
index 00000000000..dbae2604244
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/index.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, truck } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+import attributes from './attributes';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-shipping-methods-block', {
+ title: __( 'Shipping Methods', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Shipping options for your store.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ lock: {
+ remove: true,
+ move: true,
+ },
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes,
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/index.js b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/index.js
new file mode 100644
index 00000000000..8eac3f05269
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/index.js
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Placeholder, Button } from 'wordpress-components';
+import { Icon, truck } from '@woocommerce/icons';
+import { ADMIN_URL } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+const NoShippingPlaceholder = () => {
+ return (
+ }
+ label={ __( 'Shipping options', 'woo-gutenberg-products-block' ) }
+ className="wc-block-checkout__no-shipping-placeholder"
+ >
+
+ { __(
+ 'Your store does not have any Shipping Options configured. Once you have added your Shipping Options they will appear here.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ { __(
+ 'Configure Shipping Options',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ );
+};
+
+export default NoShippingPlaceholder;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/style.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/style.scss
new file mode 100644
index 00000000000..a5e23fedd40
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-shipping-methods-block/no-shipping-placeholder/style.scss
@@ -0,0 +1,21 @@
+.components-placeholder.wc-block-checkout__no-shipping-placeholder {
+ margin-bottom: $gap;
+
+ * {
+ pointer-events: all; // Overrides parent disabled component in editor context
+ }
+
+ .components-placeholder__fieldset {
+ display: block;
+
+ .components-button {
+ background-color: $gray-900;
+ color: $white;
+ }
+
+ .wc-block-checkout__no-shipping-placeholder-description {
+ display: block;
+ margin: 0.25em 0 1em 0;
+ }
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/constants.js b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/constants.js
new file mode 100644
index 00000000000..733c01688ab
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/constants.js
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings';
+
+const termsPageLink = TERMS_URL
+ ? `${ __(
+ 'Terms and Conditions',
+ 'woo-gutenberg-product-blocks'
+ ) } `
+ : __( 'Terms and Conditions', 'woo-gutenberg-product-blocks' );
+
+const privacyPageLink = PRIVACY_URL
+ ? `${ __(
+ 'Privacy Policy',
+ 'woo-gutenberg-product-blocks'
+ ) } `
+ : __( 'Privacy Policy', 'woo-gutenberg-product-blocks' );
+
+export const termsConsentDefaultText = sprintf(
+ /* translators: %1$s terms page link, %2$s privacy page link. */
+ __(
+ 'By proceeding with your purchase you agree to our %1$s and %2$s',
+ 'woo-gutenberg-product-blocks'
+ ),
+ termsPageLink,
+ privacyPageLink
+);
+
+export const termsCheckboxDefaultText = sprintf(
+ /* translators: %1$s terms page link, %2$s privacy page link. */
+ __(
+ 'You must accept our %1$s and %2$s to continue with your purchase.',
+ 'woo-gutenberg-product-blocks'
+ ),
+ termsPageLink,
+ privacyPageLink
+);
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/edit.tsx
new file mode 100644
index 00000000000..1eb02cf0e41
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/edit.tsx
@@ -0,0 +1,109 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ useBlockProps,
+ RichText,
+ InspectorControls,
+} from '@wordpress/block-editor';
+import CheckboxControl from '@woocommerce/base-components/checkbox-control';
+import { PanelBody, ToggleControl, Notice } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './editor.scss';
+import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
+
+export const Edit = ( {
+ attributes: { checkbox, text },
+ setAttributes,
+}: {
+ attributes: { text: string; checkbox: boolean };
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+} ): JSX.Element => {
+ const currentText = text || termsCheckboxDefaultText;
+
+ return (
+ <>
+
+
+
+ setAttributes( {
+ checkbox: ! checkbox,
+ } )
+ }
+ />
+
+
+
+ { checkbox ? (
+ <>
+
+
+ setAttributes( { text: value } )
+ }
+ />
+ >
+ ) : (
+
+ setAttributes( { text: value } )
+ }
+ />
+ ) }
+
+ { ! currentText.includes( '
+ setAttributes( { text: '' } ),
+ },
+ ]
+ : []
+ }
+ >
+
+ { __(
+ 'Ensure you add links to your policy pages in this section.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ ) }
+ >
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return
;
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/editor.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/editor.scss
new file mode 100644
index 00000000000..bcf2e0342e0
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/editor.scss
@@ -0,0 +1,22 @@
+// Adjust padding and margins in the editor to improve selected block outlines.
+.wc-block-checkout__terms {
+ margin: 20px 0 20px 9px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ display: flex;
+ align-items: flex-start;
+
+ .block-editor-rich-text__editable {
+ padding-left: $gap;
+ vertical-align: middle;
+ line-height: em(24px);
+ }
+}
+
+.wc-block-checkout__terms_notice {
+ margin-left: 9px;
+
+ .components-notice__action {
+ margin-left: 0;
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/frontend.tsx
new file mode 100644
index 00000000000..d66c058637a
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/frontend.tsx
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+import CheckboxControl from '@woocommerce/base-components/checkbox-control';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
+import './style.scss';
+
+const FrontendBlock = ( {
+ text,
+ checkbox,
+}: {
+ text: string;
+ checkbox: boolean;
+} ): JSX.Element => {
+ const [ checked, setChecked ] = useState( false );
+ return (
+
+ { checkbox ? (
+ <>
+ setChecked( ( value ) => ! value ) }
+ >
+
+
+ >
+ ) : (
+
+ ) }
+
+ );
+};
+
+export default FrontendBlock;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/index.tsx
new file mode 100644
index 00000000000..4e106d77315
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/index.tsx
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, asterisk } from '@woocommerce/icons';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-terms-block', {
+ title: __( 'Terms and Conditions', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Ensure customers agree to your terms and conditions and privacy policy.',
+ 'woo-gutenberg-products-block'
+ ),
+ icon: {
+ src: ,
+ foreground: '#874FB9',
+ },
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ },
+ parent: [ 'woocommerce/checkout-fields-block' ],
+ attributes: {
+ checkbox: {
+ type: 'boolean',
+ default: false,
+ },
+ text: {
+ type: 'string',
+ required: false,
+ },
+ },
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/style.scss b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/style.scss
new file mode 100644
index 00000000000..d2e8f0df83a
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-terms-block/style.scss
@@ -0,0 +1,8 @@
+.wc-block-checkout__terms {
+ margin: 1.5em 0 1.5em 9px;
+
+ textarea {
+ top: -5px;
+ position: relative;
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/edit.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/edit.tsx
new file mode 100644
index 00000000000..3e4c13635ae
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/edit.tsx
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
+import { getRegisteredBlocks } from '@woocommerce/blocks-checkout';
+
+/**
+ * Internal dependencies
+ */
+import type { InnerBlockTemplate } from '../../types';
+
+const ALLOWED_BLOCKS: string[] = [
+ 'woocommerce/checkout-order-summary-block',
+ ...getRegisteredBlocks( 'totals' ),
+];
+const TEMPLATE: InnerBlockTemplate[] = [
+ [ 'woocommerce/checkout-order-summary-block', {}, [] ],
+];
+
+export const Edit = (): JSX.Element => {
+ const blockProps = useBlockProps();
+ return (
+
+
+
+
+
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/frontend.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/frontend.tsx
new file mode 100644
index 00000000000..60c166b5cc7
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/frontend.tsx
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
+
+const FrontendBlock = ( {
+ children,
+}: {
+ children: JSX.Element;
+} ): JSX.Element => {
+ return (
+ { children }
+ );
+};
+
+export default FrontendBlock;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/index.tsx
new file mode 100644
index 00000000000..444264d6d37
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/checkout-totals-block/index.tsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit, Save } from './edit';
+
+registerFeaturePluginBlockType( 'woocommerce/checkout-totals-block', {
+ title: __( 'Checkout Totals Block', 'woo-gutenberg-products-block' ),
+ category: 'woocommerce',
+ description: __(
+ 'Wrapper block for checkout totals',
+ 'woo-gutenberg-products-block'
+ ),
+ supports: {
+ align: false,
+ html: false,
+ multiple: false,
+ reusable: false,
+ inserter: false,
+ },
+ parent: [ 'woocommerce/checkout-i2' ],
+ attributes: {},
+ apiVersion: 2,
+ edit: Edit,
+ save: Save,
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/index.tsx
new file mode 100644
index 00000000000..f434d995d8e
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/index.tsx
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import './checkout-fields-block';
+import './checkout-totals-block';
+import './checkout-shipping-address-block';
+import './checkout-terms-block';
+import './checkout-contact-information-block';
+import './checkout-billing-address-block';
+import './checkout-actions-block';
+import './checkout-order-note-block';
+import './checkout-order-summary-block';
+import './checkout-payment-block';
+import './checkout-express-payment-block';
+import './checkout-shipping-methods-block';
+import './checkout-sample-block';
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/register-components.ts b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/register-components.ts
new file mode 100644
index 00000000000..07efa628692
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/register-components.ts
@@ -0,0 +1,118 @@
+/**
+ * External dependencies
+ */
+import { lazy } from '@wordpress/element';
+import { registerBlockComponent } from '@woocommerce/blocks-registry';
+import { WC_BLOCKS_BUILD_URL } from '@woocommerce/block-settings';
+
+// Modify webpack publicPath at runtime based on location of WordPress Plugin.
+// eslint-disable-next-line no-undef,camelcase
+__webpack_public_path__ = WC_BLOCKS_BUILD_URL;
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-fields-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/fields" */ './checkout-fields-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-terms-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/terms" */ './checkout-terms-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-totals-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/totals" */ './checkout-totals-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-billing-address-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/billing-address" */ './checkout-billing-address-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-actions-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/actions" */ './checkout-actions-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-contact-information-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/contact-information" */ './checkout-contact-information-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-order-note-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/order-note" */ './checkout-order-note-block/block'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-order-summary-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/order-summary" */ './checkout-order-summary-block/block'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-payment-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/payment" */ './checkout-payment-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-shipping-address-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/shipping-address" */ './checkout-shipping-address-block/frontend'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-express-payment-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/block'
+ )
+ ),
+} );
+
+registerBlockComponent( {
+ blockName: 'woocommerce/checkout-shipping-methods-block',
+ component: lazy( () =>
+ import(
+ /* webpackChunkName: "checkout-blocks/shipping-methods" */ './checkout-shipping-methods-block/frontend'
+ )
+ ),
+} );
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/phone-number/index.tsx b/assets/js/blocks/cart-checkout/checkout-i2/phone-number/index.tsx
new file mode 100644
index 00000000000..2334eacb414
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/phone-number/index.tsx
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
+
+/**
+ * Renders a phone number input.
+ */
+const PhoneNumber = ( {
+ id = 'phone',
+ isRequired = false,
+ value = '',
+ onChange,
+}: {
+ id?: string;
+ isRequired: boolean;
+ value: string;
+ onChange: ( value: string ) => void;
+} ): JSX.Element => {
+ return (
+
+ );
+};
+
+export default PhoneNumber;
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/styles/editor.scss b/assets/js/blocks/cart-checkout/checkout-i2/styles/editor.scss
new file mode 100644
index 00000000000..2fe5db93fde
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/styles/editor.scss
@@ -0,0 +1,45 @@
+[data-type="woocommerce/checkout-i2"] {
+ .wc-block-components-sidebar-layout {
+ display: block;
+ }
+ .block-editor-block-list__layout {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: baseline;
+ }
+}
+
+[data-type="woocommerce/checkout-fields-block"] {
+ .block-editor-block-list__layout {
+ display: block;
+ }
+}
+
+[data-type="woocommerce/checkout-totals-block"] {
+ .block-editor-block-list__layout {
+ display: block;
+ }
+}
+
+.wc-block-checkout__address-fields-notice {
+ margin: $gap 0 0;
+}
+
+body.wc-lock-selected-block--move {
+ .block-editor-block-mover__move-button-container,
+ .block-editor-block-mover,
+ .block-editor-block-settings-menu {
+ display: none;
+ }
+}
+
+body.wc-lock-selected-block--remove {
+ .block-editor-block-settings-menu__popover {
+ .components-menu-group:last-child {
+ display: none;
+ }
+ .components-menu-group:nth-last-child(2) {
+ margin-bottom: -12px;
+ }
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/styles/style.scss b/assets/js/blocks/cart-checkout/checkout-i2/styles/style.scss
new file mode 100644
index 00000000000..34d46d51886
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/styles/style.scss
@@ -0,0 +1,97 @@
+// Loading skeleton.
+.is-loading.wp-block-woocommerce-checkout-i2 {
+ .wp-block-woocommerce-checkout-totals-block,
+ .wp-block-woocommerce-checkout-fields-block {
+ > div {
+ @include placeholder();
+ margin: 0 0 1.5em 0;
+ display: none;
+ }
+
+ .wp-block-woocommerce-checkout-contact-information-block,
+ .wp-block-woocommerce-checkout-payment-block {
+ min-height: 10em;
+ display: block;
+ }
+ .wp-block-woocommerce-checkout-shipping-address-block {
+ min-height: 24em;
+ display: block;
+ }
+ .wp-block-woocommerce-checkout-actions-block {
+ width: 50%;
+ min-height: 4em;
+ margin-left: 50%;
+ display: block;
+ }
+ .wp-block-woocommerce-checkout-order-summary-block {
+ min-height: 47em;
+ display: block;
+ }
+ }
+
+ // @todo these styles replace the need for SidebarLayout styles. We define styles here so placeholder elements (loading state) for the checkout has the same sidebar type layout before JS loads.
+ &.wp-block-woocommerce-checkout-i2 {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 auto $gap;
+ position: relative;
+
+ .wp-block-woocommerce-checkout-fields-block {
+ box-sizing: border-box;
+ margin: 0;
+ padding-right: percentage($gap-largest / 1060px); // ~1060px is the default width of the content area in Storefront.
+ width: 65%;
+ }
+ .wp-block-woocommerce-checkout-totals-block {
+ box-sizing: border-box;
+ margin: 0;
+ padding-left: percentage($gap-large / 1060px);
+ width: 35%;
+
+ .wc-block-components-panel > h2 {
+ @include font-size(regular);
+ @include reset-box();
+ @include reset-typography();
+ .wc-block-components-panel__button {
+ font-weight: 400;
+ }
+ }
+ }
+ }
+
+ .is-medium,
+ .is-small,
+ .is-mobile {
+ &.wp-block-woocommerce-checkout-i2 {
+ flex-direction: column;
+ margin: 0 auto $gap;
+
+ .wp-block-woocommerce-checkout-fields-block {
+ padding: 0;
+ width: 100%;
+ }
+ .wp-block-woocommerce-checkout-totals-block {
+ padding: 0;
+ width: 100%;
+ }
+ }
+ }
+
+ .is-large {
+ .wp-block-woocommerce-checkout-totals-block {
+ .wc-block-components-totals-item,
+ .wc-block-components-panel {
+ padding-left: $gap;
+ padding-right: $gap;
+ }
+ }
+ }
+
+ // For Twenty Twenty we need to increase specificity a bit more.
+ .theme-twentytwenty {
+ .wp-block-woocommerce-checkout-totals-block .wc-block-components-panel > h2 {
+ @include font-size(large);
+ @include reset-box();
+ }
+ }
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/types.ts b/assets/js/blocks/cart-checkout/checkout-i2/types.ts
new file mode 100644
index 00000000000..eef9841b7db
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/types.ts
@@ -0,0 +1,21 @@
+export type InnerBlockTemplate = [
+ string,
+ Record< string, unknown >,
+ InnerBlockTemplate[] | undefined
+];
+
+export interface Attributes {
+ allowCreateAccount: boolean;
+ hasDarkControls: boolean;
+ showCompanyField: boolean;
+ showApartmentField: boolean;
+ showPhoneField: boolean;
+ requireCompanyField: boolean;
+ requirePhoneField: boolean;
+ // Deprecated.
+ showOrderNotes: boolean;
+ showPolicyLinks: boolean;
+ showReturnToCart: boolean;
+ showRateAfterTaxName: boolean;
+ cartPageId: number;
+}
diff --git a/assets/js/blocks/cart-checkout/checkout-i2/utils.ts b/assets/js/blocks/cart-checkout/checkout-i2/utils.ts
new file mode 100644
index 00000000000..71eb49c53bb
--- /dev/null
+++ b/assets/js/blocks/cart-checkout/checkout-i2/utils.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { LOGIN_URL } from '@woocommerce/block-settings';
+import { getSetting } from '@woocommerce/settings';
+
+export const LOGIN_TO_CHECKOUT_URL = `${ LOGIN_URL }?redirect_to=${ encodeURIComponent(
+ window.location.href
+) }`;
+
+export const isLoginRequired = ( customerId: number ): boolean => {
+ return ! customerId && ! getSetting( 'checkoutAllowsGuest', false );
+};
+
+export const reloadPage = (): void => void window.location.reload( true );
diff --git a/assets/js/blocks/cart-checkout/checkout/index.js b/assets/js/blocks/cart-checkout/checkout/index.js
index a397b8b8220..44ad55c357e 100644
--- a/assets/js/blocks/cart-checkout/checkout/index.js
+++ b/assets/js/blocks/cart-checkout/checkout/index.js
@@ -4,7 +4,11 @@
import { __ } from '@wordpress/i18n';
import { Icon, card } from '@woocommerce/icons';
import classnames from 'classnames';
-import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
+import {
+ registerFeaturePluginBlockType,
+ isExperimentalBuild,
+} from '@woocommerce/block-settings';
+import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -13,6 +17,36 @@ import edit from './edit';
import blockAttributes from './attributes';
import './editor.scss';
+const transforms = isExperimentalBuild()
+ ? {
+ transforms: {
+ from: [
+ {
+ type: 'block',
+ blocks: [ 'woocommerce/checkout' ],
+ transform: ( attributes ) => {
+ return createBlock( 'woocommerce/checkout', {
+ attributes,
+ } );
+ },
+ },
+ ],
+ to: [
+ {
+ type: 'block',
+ blocks: [ 'woocommerce/checkout-i2' ],
+ transform: ( attributes ) => {
+ return createBlock(
+ 'woocommerce/checkout-i2',
+ attributes
+ );
+ },
+ },
+ ],
+ },
+ }
+ : {};
+
const settings = {
title: __( 'Checkout', 'woo-gutenberg-products-block' ),
icon: {
@@ -46,6 +80,7 @@ const settings = {
/>
);
},
+ ...transforms,
};
registerFeaturePluginBlockType( 'woocommerce/checkout', settings );
diff --git a/assets/js/data/default-states.ts b/assets/js/data/default-states.ts
index f0e58da9383..901742110b1 100644
--- a/assets/js/data/default-states.ts
+++ b/assets/js/data/default-states.ts
@@ -32,6 +32,7 @@ export const defaultCartState: CartState = {
state: '',
postcode: '',
country: '',
+ phone: '',
},
billingAddress: {
first_name: '',
diff --git a/assets/js/editor-components/external-link-card/editor.scss b/assets/js/editor-components/external-link-card/editor.scss
new file mode 100644
index 00000000000..fac6bb73047
--- /dev/null
+++ b/assets/js/editor-components/external-link-card/editor.scss
@@ -0,0 +1,34 @@
+.wc-block-editor-components-external-link-card {
+ display: block;
+ display: flex;
+ flex-direction: row;
+ text-decoration: none;
+ margin: $gap-large 0;
+ color: inherit;
+ align-items: flex-start;
+
+ & + .wc-block-editor-components-external-link-card {
+ margin-top: -($gap-large - $gap);
+ }
+ .wc-block-editor-components-external-link-card__content {
+ flex: 1 1 0;
+ padding-right: $gap;
+ }
+ .wc-block-editor-components-external-link-card__title {
+ font-weight: 500;
+ display: block;
+ }
+ .wc-block-editor-components-external-link-card__description {
+ color: $gray-700;
+ display: block;
+ @include font-size(small);
+ margin-top: 0.5em;
+ }
+ .wc-block-editor-components-external-link-card__icon {
+ flex: 0 0 24px;
+ margin: 0;
+ text-align: right;
+ color: inherit;
+ vertical-align: top;
+ }
+}
diff --git a/assets/js/editor-components/external-link-card/index.tsx b/assets/js/editor-components/external-link-card/index.tsx
new file mode 100644
index 00000000000..326a0b57ecf
--- /dev/null
+++ b/assets/js/editor-components/external-link-card/index.tsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, chevronRight } from '@woocommerce/icons';
+import { VisuallyHidden } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './editor.scss';
+
+/**
+ * Show a link that displays a title, description, and optional icon. Links are opened in a new tab.
+ */
+const ExternalLinkCard = ( {
+ href,
+ title,
+ description,
+}: {
+ href: string;
+ title: string;
+ description?: string;
+} ): JSX.Element => {
+ return (
+
+
+
+ { title }
+
+ { description && (
+
+ { description }
+
+ ) }
+
+
+ {
+ /* translators: accessibility text */
+ __( '(opens in a new tab)', 'woo-gutenberg-products-block' )
+ }
+
+
+
+ );
+};
+
+export default ExternalLinkCard;
diff --git a/assets/js/editor-components/external-link-card/stories/index.js b/assets/js/editor-components/external-link-card/stories/index.js
new file mode 100644
index 00000000000..eb1d5cea7cc
--- /dev/null
+++ b/assets/js/editor-components/external-link-card/stories/index.js
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import ExternalLinkCard from '../';
+
+export default {
+ title: 'WooCommerce Blocks/editor-components/ExternalLinkCard',
+ component: ExternalLinkCard,
+};
+
+export const Default = () => (
+
+);
diff --git a/assets/js/global.d.ts b/assets/js/global.d.ts
new file mode 100644
index 00000000000..12cbebc2079
--- /dev/null
+++ b/assets/js/global.d.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line camelcase
+declare let __webpack_public_path__: string;
diff --git a/assets/js/icons/index.js b/assets/js/icons/index.js
index b575c55d7b8..ece39c5bc8a 100644
--- a/assets/js/icons/index.js
+++ b/assets/js/icons/index.js
@@ -1,7 +1,9 @@
export { default as Icon } from './icon';
+export { default as address } from './library/address';
export { default as arrowBack } from './library/arrow-back';
export { default as arrowDownAlt2 } from './library/arrow-down-alt2';
+export { default as asterisk } from './library/asterisk';
export { default as atom } from './library/atom';
export { default as bank } from './library/bank';
export { default as barcode } from './library/barcode';
@@ -13,13 +15,16 @@ export { default as cart } from './library/cart';
export { default as checkPayment } from './library/check-payment';
export { default as chevronDown } from './library/chevron-down';
export { default as chevronUp } from './library/chevron-up';
+export { default as chevronRight } from './library/chevron-right';
export { default as comment } from './library/comment';
+export { default as contact } from './library/contact';
export { default as done } from './library/done';
export { default as discussion } from './library/discussion';
export { default as exclamation } from './library/exclamation';
export { default as external } from './library/external';
export { default as folderStarred } from './library/folder-starred';
export { default as folder } from './library/folder';
+export { default as formStep } from './library/form-step';
export { default as grid } from './library/grid';
export { default as heading } from './library/heading';
export { default as image } from './library/image';
diff --git a/assets/js/icons/library/address.js b/assets/js/icons/library/address.js
new file mode 100644
index 00000000000..3ff8d94582a
--- /dev/null
+++ b/assets/js/icons/library/address.js
@@ -0,0 +1,16 @@
+/**
+ * External dependencies
+ */
+import { SVG } from 'wordpress-components';
+
+const address = (
+
+
+
+);
+
+export default address;
diff --git a/assets/js/icons/library/asterisk.js b/assets/js/icons/library/asterisk.js
new file mode 100644
index 00000000000..5cfb9663b03
--- /dev/null
+++ b/assets/js/icons/library/asterisk.js
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { SVG } from 'wordpress-components';
+
+const asterisk = (
+
+
+
+
+
+
+);
+
+export default asterisk;
diff --git a/assets/js/icons/library/card.js b/assets/js/icons/library/card.js
index 4a6c24f4e2d..c720e3fcf70 100644
--- a/assets/js/icons/library/card.js
+++ b/assets/js/icons/library/card.js
@@ -6,7 +6,10 @@ import { SVG } from 'wordpress-components';
const card = (
-
+
);
diff --git a/assets/js/icons/library/chevron-right.js b/assets/js/icons/library/chevron-right.js
new file mode 100644
index 00000000000..5fe7b80d2ac
--- /dev/null
+++ b/assets/js/icons/library/chevron-right.js
@@ -0,0 +1,12 @@
+/**
+ * External dependencies
+ */
+import { SVG } from 'wordpress-components';
+
+const chevronRight = (
+
+
+
+);
+
+export default chevronRight;
diff --git a/assets/js/icons/library/contact.js b/assets/js/icons/library/contact.js
new file mode 100644
index 00000000000..0ea866fe6eb
--- /dev/null
+++ b/assets/js/icons/library/contact.js
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+import { SVG } from 'wordpress-components';
+
+const contact = (
+
+
+
+
+
+
+);
+
+export default contact;
diff --git a/assets/js/icons/library/form-step.js b/assets/js/icons/library/form-step.js
new file mode 100644
index 00000000000..3ee48eca537
--- /dev/null
+++ b/assets/js/icons/library/form-step.js
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { SVG } from 'wordpress-components';
+
+const FormStep = (
+
+
+
+
+
+
+);
+
+export default FormStep;
diff --git a/assets/js/icons/library/notes.js b/assets/js/icons/library/notes.js
index 2a6dc1f0a9a..8f802cea57f 100644
--- a/assets/js/icons/library/notes.js
+++ b/assets/js/icons/library/notes.js
@@ -4,9 +4,18 @@
import { SVG } from 'wordpress-components';
const notes = (
-
-
-
+
+
+
+
+
);
diff --git a/assets/js/icons/library/truck.js b/assets/js/icons/library/truck.js
index 6d245585da2..eaee116770a 100644
--- a/assets/js/icons/library/truck.js
+++ b/assets/js/icons/library/truck.js
@@ -4,9 +4,12 @@
import { SVG } from 'wordpress-components';
const truck = (
-
-
-
+
+
);
diff --git a/assets/js/previews/cart.ts b/assets/js/previews/cart.ts
index d037e180c5c..f3f120bd7a6 100644
--- a/assets/js/previews/cart.ts
+++ b/assets/js/previews/cart.ts
@@ -176,6 +176,7 @@ export const previewCart: CartResponse = {
state: '',
postcode: '',
country: '',
+ phone: '',
},
billing_address: {
first_name: '',
diff --git a/assets/js/types/type-defs/cart-response.ts b/assets/js/types/type-defs/cart-response.ts
index 5e3581f4397..69d3007cf29 100644
--- a/assets/js/types/type-defs/cart-response.ts
+++ b/assets/js/types/type-defs/cart-response.ts
@@ -82,11 +82,11 @@ export interface CartResponseShippingAddress
extends ResponseBaseAddress,
ResponseFirstNameLastName {
company: string;
+ phone: string;
}
export interface CartResponseBillingAddress
extends CartResponseShippingAddress {
- phone: string;
email: string;
}
diff --git a/assets/js/types/type-defs/cart.ts b/assets/js/types/type-defs/cart.ts
index 8edaf010222..cfbadb5c045 100644
--- a/assets/js/types/type-defs/cart.ts
+++ b/assets/js/types/type-defs/cart.ts
@@ -65,10 +65,10 @@ export interface CartShippingRate {
export interface CartShippingAddress extends BaseAddress, FirstNameLastName {
company: string;
+ phone: string;
}
export interface CartBillingAddress extends CartShippingAddress {
- phone: string;
email: string;
}
diff --git a/assets/js/types/type-defs/contexts.js b/assets/js/types/type-defs/contexts.js
index 260a761dd51..46e357b53ae 100644
--- a/assets/js/types/type-defs/contexts.js
+++ b/assets/js/types/type-defs/contexts.js
@@ -11,10 +11,12 @@
/**
* @typedef {Object} CustomerDataContext
*
- * @property {BillingData} billingData The current billing data, including address and email.
- * @property {CartShippingAddress} shippingAddress The current set address for shipping.
- * @property {function(Object)} setBillingData A function for setting billing data.
- * @property {function(Object)} setShippingAddress A function for setting shipping address.
+ * @property {BillingData} billingData The current billing data, including address and email.
+ * @property {CartShippingAddress} shippingAddress The current set address for shipping.
+ * @property {Function} setBillingData A function for setting billing data.
+ * @property {Function} setShippingAddress A function for setting shipping address.
+ * @property {boolean} shippingAsBilling A boolean which tracks if the customer is using the same billing and shipping address.
+ * @property {Function} setShippingAsBilling A function for toggling shipping as billing.
*/
/**
diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js
index abfed5e87bd..2f94a879db1 100644
--- a/bin/webpack-entries.js
+++ b/bin/webpack-entries.js
@@ -45,6 +45,10 @@ const blocks = {
checkout: {
customDir: 'cart-checkout/checkout',
},
+ 'checkout-i2': {
+ customDir: 'cart-checkout/checkout-i2',
+ isExperimental: true,
+ },
'single-product': {
isExperimental: true,
},
diff --git a/package-lock.json b/package-lock.json
index 11833691a2f..6b559ecc845 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4825,6 +4825,46 @@
}
}
},
+ "@types/wordpress__block-editor": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@types/wordpress__block-editor/-/wordpress__block-editor-2.2.9.tgz",
+ "integrity": "sha512-TrmruuJFb0jplVvWki+ThQKmNGU0fkr+DXHIiwp8YN6atRNn9AJRL0FF7B4QXeoE3lHIpdZ41pFuuTgs3aqmiw==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*",
+ "@types/wordpress__blocks": "*",
+ "@types/wordpress__components": "*",
+ "@types/wordpress__data": "*",
+ "@types/wordpress__keycodes": "*",
+ "@wordpress/element": "^2.14.0",
+ "react-autosize-textarea": "^7.0.0"
+ },
+ "dependencies": {
+ "react-autosize-textarea": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz",
+ "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==",
+ "dev": true,
+ "requires": {
+ "autosize": "^4.0.2",
+ "line-height": "^0.3.1",
+ "prop-types": "^15.5.6"
+ }
+ }
+ }
+ },
+ "@types/wordpress__blocks": {
+ "version": "6.4.12",
+ "resolved": "https://registry.npmjs.org/@types/wordpress__blocks/-/wordpress__blocks-6.4.12.tgz",
+ "integrity": "sha512-ezQoo4NTfVY/KiCA8kwUXBJhgyoPq1HtfpRk6NXxQOwcDyFK0zN0knCvLn5YqVx1u8j3fSblbkW4RcFOK1xGIQ==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*",
+ "@types/wordpress__components": "*",
+ "@types/wordpress__data": "*",
+ "@wordpress/element": "^2.14.0"
+ }
+ },
"@types/wordpress__components": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/@types/wordpress__components/-/wordpress__components-14.0.2.tgz",
@@ -5456,7 +5496,6 @@
"@wordpress/components": "10.2.0",
"@wordpress/compose": "3.23.1",
"@wordpress/date": "3.13.0",
- "@wordpress/dom": "2.16.0",
"@wordpress/element": "2.19.0",
"@wordpress/html-entities": "2.10.0",
"@wordpress/i18n": "3.17.0",
@@ -5510,7 +5549,6 @@
"@wordpress/compose": "^3.20.1",
"@wordpress/date": "^3.11.1",
"@wordpress/deprecated": "^2.9.0",
- "@wordpress/dom": "^2.14.0",
"@wordpress/element": "^2.17.1",
"@wordpress/hooks": "^2.9.0",
"@wordpress/i18n": "^3.15.0",
@@ -5830,7 +5868,6 @@
"@wordpress/compose": "3.23.1",
"@wordpress/date": "3.13.0",
"@wordpress/deprecated": "^3.1.1",
- "@wordpress/dom": "2.16.0",
"@wordpress/element": "2.19.0",
"@wordpress/html-entities": "2.10.0",
"@wordpress/i18n": "3.17.0",
@@ -5874,7 +5911,6 @@
"@wordpress/compose": "^3.20.1",
"@wordpress/date": "^3.11.1",
"@wordpress/deprecated": "^2.9.0",
- "@wordpress/dom": "^2.14.0",
"@wordpress/element": "^2.17.1",
"@wordpress/hooks": "^2.9.0",
"@wordpress/i18n": "^3.15.0",
@@ -6066,7 +6102,6 @@
"@wordpress/compose": "^3.20.1",
"@wordpress/date": "^3.11.1",
"@wordpress/deprecated": "^2.9.0",
- "@wordpress/dom": "^2.14.0",
"@wordpress/element": "^2.17.1",
"@wordpress/hooks": "^2.9.0",
"@wordpress/i18n": "^3.15.0",
@@ -6402,6 +6437,16 @@
"uuid": "^7.0.2"
}
},
+ "@wordpress/dom": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-2.18.0.tgz",
+ "integrity": "sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "lodash": "^4.17.19"
+ }
+ },
"@wordpress/is-shallow-equal": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-2.3.0.tgz",
@@ -6570,6 +6615,16 @@
"@babel/runtime": "^7.13.10"
}
},
+ "@wordpress/dom": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-2.18.0.tgz",
+ "integrity": "sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "lodash": "^4.17.19"
+ }
+ },
"@wordpress/is-shallow-equal": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-2.3.0.tgz",
@@ -6652,6 +6707,16 @@
"@wordpress/hooks": "^2.12.3"
}
},
+ "@wordpress/dom": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-2.18.0.tgz",
+ "integrity": "sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "lodash": "^4.17.19"
+ }
+ },
"@wordpress/element": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/@wordpress/element/-/element-2.20.3.tgz",
@@ -6730,7 +6795,6 @@
"requires": {
"@babel/runtime": "^7.12.5",
"@wordpress/deprecated": "^2.11.0",
- "@wordpress/dom": "^2.16.0",
"@wordpress/element": "^2.19.0",
"@wordpress/is-shallow-equal": "^3.0.0",
"@wordpress/keycodes": "^2.18.0",
@@ -7231,12 +7295,13 @@
}
},
"@wordpress/dom": {
- "version": "2.16.0",
- "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-2.16.0.tgz",
- "integrity": "sha512-IjXPfv9SuEkVbmxD4eaxn01zZmYUxp/4wrMcsHAHGym59k/bN6uJOQprrU/tTiSR4Zlf8Jmo22HWmuL654k8zg==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-3.1.2.tgz",
+ "integrity": "sha512-ahY2nFqX7dktTHbuSyxnx3uz3LC5Y3g5Ji4mkoJZsA2BVAJFc8Vj7dGWnSstcPnuECGlkcEXF5FvMpIgsJB20Q==",
+ "dev": true,
"requires": {
- "@babel/runtime": "^7.12.5",
- "lodash": "^4.17.19"
+ "@babel/runtime": "^7.13.10",
+ "lodash": "^4.17.21"
}
},
"@wordpress/dom-ready": {
@@ -7408,6 +7473,18 @@
"rememo": "^3.0.0",
"tinycolor2": "^1.4.1",
"uuid": "^7.0.2"
+ },
+ "dependencies": {
+ "@wordpress/dom": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-2.18.0.tgz",
+ "integrity": "sha512-tM2WeQuSObl3nzWjUTF0/dyLnA7sdl/MXaSe32D64OF89bjSyJvjUipI7gjKzI3kJ7ddGhwcTggGvSB06MOoCQ==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "lodash": "^4.17.19"
+ }
+ }
}
},
"@wordpress/core-data": {
@@ -9067,7 +9144,6 @@
"@wordpress/compose": "^3.22.0",
"@wordpress/date": "^3.12.0",
"@wordpress/deprecated": "^2.10.0",
- "@wordpress/dom": "^2.15.0",
"@wordpress/element": "^2.18.0",
"@wordpress/hooks": "^2.10.0",
"@wordpress/i18n": "^3.16.0",
@@ -10814,6 +10890,16 @@
"dev": true,
"optional": true
},
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -16372,6 +16458,13 @@
}
}
},
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true,
+ "optional": true
+ },
"filelist": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@@ -23150,6 +23243,13 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
+ "nan": {
+ "version": "2.14.2",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
+ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
+ "dev": true,
+ "optional": true
+ },
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@@ -33321,7 +33421,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
- "optional": true
+ "optional": true,
+ "requires": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ }
},
"glob-parent": {
"version": "3.1.0",
@@ -34225,7 +34329,6 @@
"@wordpress/compose": "^3.22.0",
"@wordpress/date": "^3.12.0",
"@wordpress/deprecated": "^2.10.0",
- "@wordpress/dom": "^2.15.0",
"@wordpress/element": "^2.18.0",
"@wordpress/hooks": "^2.10.0",
"@wordpress/i18n": "^3.16.0",
@@ -34345,7 +34448,6 @@
"requires": {
"@babel/runtime": "^7.12.5",
"@wordpress/deprecated": "^2.11.0",
- "@wordpress/dom": "^2.16.0",
"@wordpress/element": "^2.19.0",
"@wordpress/is-shallow-equal": "^3.0.0",
"@wordpress/keycodes": "^2.18.0",
diff --git a/package.json b/package.json
index d4ef2def7f2..d7a60001467 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"./assets/js/atomic/blocks/**",
"./assets/js/filters/**",
"./assets/js/settings/blocks/**",
- "./assets/js/middleware/**"
+ "./assets/js/middleware/**",
+ "./assets/js/blocks/cart-checkout/checkout-i2/inner-blocks/**/index.tsx"
],
"repository": {
"type": "git",
@@ -97,6 +98,7 @@
"@types/lodash": "4.14.171",
"@types/react": "16.14.10",
"@types/wordpress__data": "4.6.10",
+ "@types/wordpress__block-editor": "^2.2.9",
"@types/wordpress__data-controls": "1.0.5",
"@types/wordpress__deprecated": "2.4.2",
"@types/wordpress__element": "2.4.1",
@@ -115,6 +117,7 @@
"@wordpress/components": "11.1.1",
"@wordpress/data-controls": "1.20.8",
"@wordpress/dependency-extraction-webpack-plugin": "2.8.0",
+ "@wordpress/dom": "^3.1.2",
"@wordpress/e2e-test-utils": "5.1.2",
"@wordpress/editor": "9.22.0",
"@wordpress/element": "2.17.1",
diff --git a/packages/checkout/blocks-registry/README.md b/packages/checkout/blocks-registry/README.md
new file mode 100644
index 00000000000..01f1b29e0b6
--- /dev/null
+++ b/packages/checkout/blocks-registry/README.md
@@ -0,0 +1,74 @@
+# WooCommerce Blocks - Blocks Registry
+
+This directory contains the Checkout Blocks Registry - functions to register custom blocks that can be inserted into various areas within the Checkout.
+
+**-- These docs will be moved to the main docs directory once finalized --**
+
+The Checkout Block has a function based interface for registering custom Blocks so that merchants can insert them into specific Inner Block areas within the Checkout page layout. Custom Blocks registered in this way can also define a component to render on the frontend in place of the Block.
+
+## Table of Contents
+
+- [Registering a block - `registerCheckoutBlock( block, options )`](#registering-a-block---registercheckoutblock-block-options-)
+ - [`component` (required)](#component-required)
+ - [`areas` (required)](#areas-required)
+ - [`configuration`](#configuration)
+
+## Registering a block - `registerCheckoutBlock( block, options )`
+
+To register a checkout block, you use the registerCheckoutBlock function from the checkout blocks registry. An example of importing this for use in your JavaScript file is:
+
+_Aliased import_
+
+```js
+import { registerCheckoutBlock } from '@woocommerce/blocks-checkout';
+```
+
+_wc global_
+
+```js
+const { registerCheckoutBlock } = wc.blocksCheckout;
+```
+
+The register function expects a block name string, and a JavaScript object with options specific to the block you are registering:
+
+```js
+registerCheckoutBlock( blockName, options );
+```
+
+The options you feed the configuration instance should be an object in this shape (see `CheckoutBlockOptions` typedef):
+
+```js
+const options = {
+ component: () => A Function Component
,
+ areas: [ 'areaName' ],
+ configuration: {},
+};
+```
+
+Here's some more details on the _configuration_ options:
+
+### `component` (required)
+
+This is a React component that should replace the Block on the frontend. It will be fed any attributes from the Block and have access to any public context providers in the Checkout context.
+
+You should provide either a _React Component_ or a `React.lazy()` component if you wish to lazy load for performance reasons.
+
+### `areas` (required)
+
+This is an array of string based area names which define when the custom block can be inserted within the Checkout.
+
+See the `RegisteredBlocks` typedef for available areas. Valid values at time of writing include:
+
+- `totals` - The right side of the checkout containing order totals.
+- `fields` - The left side of the checkout containing checkout form steps.
+- `contactInformation` - Within the contact information form step.
+- `shippingAddress` - Within the shipping address form step.
+- `billingAddress` - Within the billing address form step.
+- `shippingMethods` - Within the shipping methods form step.
+- `paymentMethods` - Within the payment methods form step.
+
+### `configuration`
+
+This is where a standard `BlockConfiguration` should be provided. This matches the [Block Registration Configuration in Gutenberg](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/).
+
+If a `BlockConfiguration` is not provided, your Block Type will not be registered in WordPress unless you manually register the block using `registerBlockType` (this is what `registerCheckoutBlock` does for you behind the scenes).
diff --git a/packages/checkout/blocks-registry/index.ts b/packages/checkout/blocks-registry/index.ts
new file mode 100644
index 00000000000..30d5dc71e0d
--- /dev/null
+++ b/packages/checkout/blocks-registry/index.ts
@@ -0,0 +1,194 @@
+/**
+ * External dependencies
+ */
+import { BlockConfiguration } from '@wordpress/blocks';
+import { registerBlockComponent } from '@woocommerce/blocks-registry';
+import { registerExperimentalBlockType } from '@woocommerce/block-settings';
+import type { LazyExoticComponent } from 'react';
+import { isObject } from '@woocommerce/types';
+
+/**
+ * List of block areas where blocks can be registered for use. Keyed by area name.
+ */
+export type RegisteredBlocks = {
+ fields: Array< string >;
+ totals: Array< string >;
+ contactInformation: Array< string >;
+ shippingAddress: Array< string >;
+ billingAddress: Array< string >;
+ shippingMethods: Array< string >;
+ paymentMethods: Array< string >;
+};
+
+let registeredBlocks: RegisteredBlocks = {
+ fields: [],
+ totals: [],
+ contactInformation: [ 'core/paragraph' ],
+ shippingAddress: [ 'core/paragraph' ],
+ billingAddress: [ 'core/paragraph' ],
+ shippingMethods: [ 'core/paragraph' ],
+ paymentMethods: [ 'core/paragraph' ],
+};
+
+/**
+ * Asserts that an option is of the given type. Otherwise, throws an error.
+ *
+ * @throws Will throw an error if the type of the option doesn't match the expected type.
+ */
+const assertType = (
+ optionName: string,
+ option: unknown,
+ expectedType: unknown
+): void => {
+ const actualType = typeof option;
+ if ( actualType !== expectedType ) {
+ throw new Error(
+ `Incorrect value for the ${ optionName } argument when registering a checkout block. It was a ${ actualType }, but must be a ${ expectedType }.`
+ );
+ }
+};
+
+/**
+ * Validation to ensure an area exists.
+ */
+const assertValidArea = ( area: string ): void => {
+ if ( ! registeredBlocks.hasOwnProperty( area ) ) {
+ throw new Error(
+ `Incorrect value for the "area" argument. It was a ${ area }, but must be one of ${ Object.keys(
+ registeredBlocks
+ ).join( ', ' ) }.`
+ );
+ }
+};
+
+/**
+ * Validate the block name.
+ *
+ * @throws Will throw an error if the blockname is invalid.
+ */
+const assertBlockName = ( blockName: string ): void => {
+ assertType( 'blockName', blockName, 'string' );
+
+ if ( ! blockName ) {
+ throw new Error(
+ `Value for the blockName argument must not be empty.`
+ );
+ }
+};
+
+/**
+ * Asserts that an option is of the given type. Otherwise, throws an error.
+ *
+ * @throws Will throw an error if the type of the option doesn't match the expected type.
+ */
+const assertOption = (
+ options: Record< string, unknown >,
+ optionName: string,
+ expectedType: 'array' | 'object' | 'string' | 'boolean' | 'number'
+): void => {
+ const actualType = typeof options[ optionName ];
+
+ if ( expectedType === 'array' ) {
+ if ( ! Array.isArray( options[ optionName ] ) ) {
+ throw new Error(
+ `Incorrect value for the ${ optionName } argument when registering a checkout block component. It was a ${ actualType }, but must be an array.`
+ );
+ }
+ } else if ( actualType !== expectedType ) {
+ throw new Error(
+ `Incorrect value for the ${ optionName } argument when registering a checkout block component. It was a ${ actualType }, but must be a ${ expectedType }.`
+ );
+ }
+};
+
+/**
+ * Asserts that an option is a valid react element or lazy callback. Otherwise, throws an error.
+ *
+ * @throws Will throw an error if the type of the option doesn't match the expected type.
+ */
+const assertBlockComponent = (
+ options: Record< string, unknown >,
+ optionName: string
+) => {
+ const optionValue = options[ optionName ];
+
+ if ( optionValue ) {
+ if ( typeof optionValue === 'function' ) {
+ return;
+ }
+ if (
+ isObject( optionValue ) &&
+ optionValue.$$typeof &&
+ optionValue.$$typeof === Symbol.for( 'react.lazy' )
+ ) {
+ return;
+ }
+ }
+ throw new Error(
+ `Incorrect value for the ${ optionName } argument when registering a block component. Component must be a valid React Element or Lazy callback.`
+ );
+};
+
+/**
+ * Adds a block (block name) to an area, if the area exists. If the area does not exist, an error is thrown.
+ */
+const registerBlockForArea = (
+ area: keyof RegisteredBlocks,
+ blockName: string
+): void | Error => {
+ assertValidArea( area );
+ registeredBlocks = {
+ ...registeredBlocks,
+ [ area ]: [ ...registeredBlocks[ area ], blockName ],
+ };
+};
+
+/**
+ * Get a list of blocks available within a specific area.
+ */
+export const getRegisteredBlocks = (
+ area: keyof RegisteredBlocks
+): Array< string > => {
+ assertValidArea( area );
+ return [ ...registeredBlocks[ area ] ];
+};
+
+export type CheckoutBlockOptions = {
+ // This is a component to render on the frontend in place of this block, when used.
+ component:
+ | LazyExoticComponent< React.ComponentType< unknown > >
+ | JSX.Element;
+ // Area(s) to add the block to. This can be a single area (string) or an array of areas.
+ areas: Array< keyof RegisteredBlocks >;
+ // Standard block configuration object. If not passed, the block will not be registered with WordPress and must be done manually.
+ configuration?: BlockConfiguration;
+};
+
+/**
+ * Main API for registering a new checkout block within areas.
+ */
+export const registerCheckoutBlock = (
+ blockName: string,
+ options: CheckoutBlockOptions
+): void => {
+ assertBlockName( blockName );
+ assertOption( options, 'areas', 'array' );
+ assertBlockComponent( options, 'component' );
+
+ if ( options?.configuration ) {
+ assertOption( options, 'configuration', 'object' );
+ registerExperimentalBlockType( blockName, {
+ ...options.configuration,
+ category: 'woocommerce',
+ } );
+ }
+
+ options.areas.forEach( ( area ) =>
+ registerBlockForArea( area, blockName )
+ );
+
+ registerBlockComponent( {
+ blockName,
+ component: options.component,
+ } );
+};
diff --git a/packages/checkout/blocks-registry/test/index.js b/packages/checkout/blocks-registry/test/index.js
new file mode 100644
index 00000000000..6b25c8a969c
--- /dev/null
+++ b/packages/checkout/blocks-registry/test/index.js
@@ -0,0 +1,61 @@
+/**
+ * Internal dependencies
+ */
+import { getRegisteredBlocks, registerCheckoutBlock } from '../index';
+
+describe( 'checkout blocks registry', () => {
+ const component = () => {
+ return null;
+ };
+
+ describe( 'registerCheckoutBlock', () => {
+ const invokeTest = ( blockName, options ) => () => {
+ return registerCheckoutBlock( blockName, options );
+ };
+ it( 'throws an error when registered block is missing `blockName`', () => {
+ expect(
+ invokeTest( null, { areas: [ 'fields' ], component } )
+ ).toThrowError( /blockName/ );
+ expect(
+ invokeTest( '', { areas: [ 'fields' ], component } )
+ ).toThrowError( /blockName/ );
+ } );
+ it( 'throws an error when area is invalid', () => {
+ expect(
+ invokeTest( 'test/block-name', {
+ areas: [ 'invalid-area' ],
+ component,
+ } )
+ ).toThrowError( /area/ );
+ } );
+ it( 'throws an error when registered block is missing `component`', () => {
+ expect(
+ invokeTest( 'test/block-name', {
+ areas: [ 'fields' ],
+ component: null,
+ } )
+ ).toThrowError( /component/ );
+ } );
+ } );
+
+ describe( 'getRegisteredBlocks', () => {
+ const invokeTest = ( areas ) => () => {
+ return getRegisteredBlocks( areas );
+ };
+ it( 'gets an empty array when checkout area has no registered blocks', () => {
+ expect( getRegisteredBlocks( 'fields' ) ).toEqual( [] );
+ } );
+ it( 'throws an error if the area is not defined', () => {
+ expect( invokeTest( 'non-existent-area' ) ).toThrowError( /area/ );
+ } );
+ it( 'gets a block that was successfully registered', () => {
+ registerCheckoutBlock( 'test/block-name', {
+ areas: [ 'fields' ],
+ component,
+ } );
+ expect( getRegisteredBlocks( 'fields' ) ).toEqual( [
+ 'test/block-name',
+ ] );
+ } );
+ } );
+} );
diff --git a/packages/checkout/index.js b/packages/checkout/index.js
index 2830f67d790..c3f187e7713 100644
--- a/packages/checkout/index.js
+++ b/packages/checkout/index.js
@@ -3,6 +3,7 @@ export * from './utils';
export * from './slot';
export * from './registry';
export { default as TotalsWrapper } from './wrapper';
+export * from './blocks-registry';
export { default as ExperimentalOrderMeta } from './order-meta';
export { default as ExperimentalDiscountsMeta } from './discounts-meta';
export { default as ExperimentalOrderShippingPackages } from './order-shipping-packages';
diff --git a/packages/tsconfig.json b/packages/tsconfig.json
index 8fb8bfbe2e4..a36f99a8656 100644
--- a/packages/tsconfig.json
+++ b/packages/tsconfig.json
@@ -11,7 +11,8 @@
"../settings/shared/index.ts",
"../settings/blocks/index.ts",
"../type-defs",
- "../assets/js/data"
+ "../assets/js/data",
+ "../assets/js/blocks-registry"
],
"exclude": [ "**/test/**" ]
}
diff --git a/src/BlockTypes/CheckoutI2.php b/src/BlockTypes/CheckoutI2.php
new file mode 100644
index 00000000000..fedc20171bb
--- /dev/null
+++ b/src/BlockTypes/CheckoutI2.php
@@ -0,0 +1,329 @@
+ 'wc-' . $this->block_name . '-block',
+ 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
+ 'dependencies' => [ 'wc-blocks' ],
+ ];
+ return $key ? $script[ $key ] : $script;
+ }
+
+ /**
+ * Get the frontend script handle for this block type.
+ *
+ * @see $this->register_block_type()
+ * @param string $key Data to get, or default to everything.
+ * @return array|string
+ */
+ protected function get_block_type_script( $key = null ) {
+ $script = [
+ 'handle' => 'wc-' . $this->block_name . '-block-frontend',
+ 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
+ 'dependencies' => [],
+ ];
+ return $key ? $script[ $key ] : $script;
+ }
+
+ /**
+ * Enqueue frontend assets for this block, just in time for rendering.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ */
+ protected function enqueue_assets( array $attributes ) {
+ do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' );
+ parent::enqueue_assets( $attributes );
+ do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' );
+ }
+
+ /**
+ * Append frontend scripts when rendering the block.
+ *
+ * @param array $attributes Block attributes.
+ * @param string $content Block content.
+ * @return string Rendered block type output.
+ */
+ protected function render( $attributes, $content ) {
+ if ( $this->is_checkout_endpoint() ) {
+ // Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the
+ // legacy shortcode instead and do not render block.
+ return '[woocommerce_checkout]';
+ }
+
+ // Deregister core checkout scripts and styles.
+ wp_dequeue_script( 'wc-checkout' );
+ wp_dequeue_script( 'wc-password-strength-meter' );
+ wp_dequeue_script( 'selectWoo' );
+ wp_dequeue_style( 'select2' );
+
+ return $this->inject_html_data_attributes( $content, $attributes );
+ }
+
+ /**
+ * Check if we're viewing a checkout page endpoint, rather than the main checkout page itself.
+ *
+ * @return boolean
+ */
+ protected function is_checkout_endpoint() {
+ return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' );
+ }
+
+ /**
+ * Extra data passed through from server to client for block.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ * Note, this will be empty in the editor context when the block is
+ * not in the post content on editor load.
+ */
+ protected function enqueue_data( array $attributes = [] ) {
+ parent::enqueue_data( $attributes );
+
+ $this->asset_data_registry->add(
+ 'allowedCountries',
+ function() {
+ return $this->deep_sort_with_accents( WC()->countries->get_allowed_countries() );
+ },
+ true
+ );
+ $this->asset_data_registry->add(
+ 'allowedStates',
+ function() {
+ return $this->deep_sort_with_accents( WC()->countries->get_allowed_country_states() );
+ },
+ true
+ );
+ $this->asset_data_registry->add(
+ 'shippingCountries',
+ function() {
+ return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() );
+ },
+ true
+ );
+ $this->asset_data_registry->add(
+ 'shippingStates',
+ function() {
+ return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() );
+ },
+ true
+ );
+ $this->asset_data_registry->add(
+ 'countryLocale',
+ function() {
+ // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
+ $country_locale = wc()->countries->get_country_locale();
+ $states = wc()->countries->get_states();
+
+ foreach ( $states as $country => $states ) {
+ if ( empty( $states ) ) {
+ $country_locale[ $country ]['state']['required'] = false;
+ $country_locale[ $country ]['state']['hidden'] = true;
+ }
+ }
+ return $country_locale;
+ },
+ true
+ );
+ $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true );
+ $this->asset_data_registry->add(
+ 'checkoutAllowsGuest',
+ false === filter_var(
+ WC()->checkout()->is_registration_required(),
+ FILTER_VALIDATE_BOOLEAN
+ ),
+ true
+ );
+ $this->asset_data_registry->add(
+ 'checkoutAllowsSignup',
+ filter_var(
+ WC()->checkout()->is_registration_enabled(),
+ FILTER_VALIDATE_BOOLEAN
+ ),
+ true
+ );
+ $this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ), true );
+ $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true );
+ $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true );
+ $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true );
+ $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true );
+ $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true );
+ $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
+ $this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 );
+
+ $is_block_editor = $this->is_block_editor();
+
+ // Hydrate the following data depending on admin or frontend context.
+ if ( $is_block_editor && ! $this->asset_data_registry->exists( 'shippingMethodsExist' ) ) {
+ $methods_exist = wc_get_shipping_method_count( false, true ) > 0;
+ $this->asset_data_registry->add( 'shippingMethodsExist', $methods_exist );
+ }
+
+ if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) {
+ $shipping_methods = WC()->shipping()->get_shipping_methods();
+ $formatted_shipping_methods = array_reduce(
+ $shipping_methods,
+ function( $acc, $method ) {
+ if ( $method->supports( 'settings' ) ) {
+ $acc[] = [
+ 'id' => $method->id,
+ 'title' => $method->method_title,
+ 'description' => $method->method_description,
+ ];
+ }
+ return $acc;
+ },
+ []
+ );
+ $this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods );
+ }
+
+ if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) {
+ $shipping_zones = \WC_Shipping_Zones::get_zones();
+ $formatted_shipping_zones = array_reduce(
+ $shipping_zones,
+ function( $acc, $zone ) {
+ $acc[] = [
+ 'id' => $zone['id'],
+ 'title' => $zone['zone_name'],
+ 'description' => $zone['formatted_zone_location'],
+ ];
+ return $acc;
+ },
+ []
+ );
+ $formatted_shipping_zones[] = [
+ 'id' => 0,
+ 'title' => __( 'International', 'woo-gutenberg-products-block' ),
+ 'description' => __( 'Locations outside all other zones', 'woo-gutenberg-products-block' ),
+ ];
+ $this->asset_data_registry->add( 'activeShippingZones', $formatted_shipping_zones );
+ }
+
+ if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
+ $payment_methods = WC()->payment_gateways->payment_gateways();
+ $formatted_payment_methods = array_reduce(
+ $payment_methods,
+ function( $acc, $method ) {
+ if ( 'yes' === $method->enabled ) {
+ $acc[] = [
+ 'id' => $method->id,
+ 'title' => $method->method_title,
+ 'description' => $method->method_description,
+ ];
+ }
+ return $acc;
+ },
+ []
+ );
+ $this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods );
+ }
+
+ if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
+ $this->hydrate_from_api();
+ $this->hydrate_customer_payment_methods();
+ }
+
+ do_action( 'woocommerce_blocks_checkout_enqueue_data' );
+ }
+
+ /**
+ * Are we currently on the admin block editor screen?
+ */
+ protected function is_block_editor() {
+ if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
+ return false;
+ }
+ $screen = get_current_screen();
+
+ return $screen && $screen->is_block_editor();
+ }
+
+ /**
+ * Removes accents from an array of values, sorts by the values, then returns the original array values sorted.
+ *
+ * @param array $array Array of values to sort.
+ * @return array Sorted array.
+ */
+ protected function deep_sort_with_accents( $array ) {
+ if ( ! is_array( $array ) || empty( $array ) ) {
+ return $array;
+ }
+
+ if ( is_array( reset( $array ) ) ) {
+ return array_map( [ $this, 'deep_sort_with_accents' ], $array );
+ }
+
+ $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) );
+ asort( $array_without_accents );
+ return array_replace( $array_without_accents, $array );
+ }
+
+ /**
+ * Get customer payment methods for use in checkout.
+ */
+ protected function hydrate_customer_payment_methods() {
+ if ( ! is_user_logged_in() || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) {
+ return;
+ }
+ add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
+ $this->asset_data_registry->add(
+ 'customerPaymentMethods',
+ wc_get_customer_saved_methods_list( get_current_user_id() )
+ );
+ remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
+ }
+
+ /**
+ * Hydrate the checkout block with data from the API.
+ */
+ protected function hydrate_from_api() {
+ // Print existing notices now, otherwise they are caught by the Cart
+ // Controller and converted to exceptions.
+ wc_print_notices();
+
+ add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
+ $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' );
+ $this->asset_data_registry->hydrate_api_request( '/wc/store/checkout' );
+ remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
+ }
+
+ /**
+ * Callback for woocommerce_payment_methods_list_item filter to add token id
+ * to the generated list.
+ *
+ * @param array $list_item The current list item for the saved payment method.
+ * @param \WC_Token $token The token for the current list item.
+ *
+ * @return array The list item with the token id added.
+ */
+ public static function include_token_id_with_payment_methods( $list_item, $token ) {
+ $list_item['tokenId'] = $token->get_id();
+ $brand = ! empty( $list_item['method']['brand'] ) ?
+ strtolower( $list_item['method']['brand'] ) :
+ '';
+ // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core.
+ if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) {
+ $list_item['method']['brand'] = wc_get_credit_card_type_label( $brand );
+ }
+ return $list_item;
+ }
+}
diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php
index 62e7a57e517..d18ad7f40b3 100644
--- a/src/BlockTypesController.php
+++ b/src/BlockTypesController.php
@@ -126,6 +126,7 @@ protected function get_block_types() {
if ( Package::feature()->is_experimental_build() ) {
$block_types[] = 'SingleProduct';
+ $block_types[] = 'CheckoutI2';
}
/**
@@ -167,6 +168,18 @@ protected function get_atomic_blocks() {
'product-tag-list',
'product-stock-indicator',
'product-add-to-cart',
+ 'checkout-fields-block',
+ 'checkout-totals-block',
+ 'checkout-billing-address-block',
+ 'checkout-actions-block',
+ 'checkout-contact-information-block',
+ 'checkout-order-note-block',
+ 'checkout-order-summary-block',
+ 'checkout-payment-block',
+ 'checkout-shipping-address-block',
+ 'checkout-shipping-methods-block',
+ 'checkout-express-payment-block',
+ 'checkout-terms-block',
];
}
}
diff --git a/src/Payments/Api.php b/src/Payments/Api.php
index 709fe2e3803..e3bb0de5866 100644
--- a/src/Payments/Api.php
+++ b/src/Payments/Api.php
@@ -63,7 +63,7 @@ protected function init() {
* @return array
*/
public function add_payment_method_script_dependencies( $dependencies, $handle ) {
- if ( ! in_array( $handle, [ 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-block', 'wc-cart-block-frontend' ], true ) ) {
+ if ( ! in_array( $handle, [ 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-checkout-i2-block', 'wc-checkout-i2-block-frontend', 'wc-cart-block', 'wc-cart-block-frontend' ], true ) ) {
return $dependencies;
}
return array_merge( $dependencies, $this->payment_method_registry->get_all_active_payment_method_script_dependencies() );
@@ -214,7 +214,7 @@ public function verify_payment_methods_dependencies() {
sprintf( 'console.error( "%s" );', $error_message )
);
- $cart_checkout_scripts = [ 'wc-cart-block', 'wc-cart-block-frontend', 'wc-checkout-block', 'wc-checkout-block-frontend' ];
+ $cart_checkout_scripts = [ 'wc-cart-block', 'wc-cart-block-frontend', 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-checkout-i2-block', 'wc-checkout-i2-block-frontend' ];
foreach ( $cart_checkout_scripts as $script_handle ) {
if (
! array_key_exists( $script_handle, $wp_scripts->registered ) ||
diff --git a/src/StoreApi/Routes/CartUpdateCustomer.php b/src/StoreApi/Routes/CartUpdateCustomer.php
index 16331567e9f..55251da4c29 100644
--- a/src/StoreApi/Routes/CartUpdateCustomer.php
+++ b/src/StoreApi/Routes/CartUpdateCustomer.php
@@ -90,9 +90,9 @@ protected function get_route_post_response( \WP_REST_Request $request ) {
'state' => wc()->customer->get_billing_state(),
'postcode' => wc()->customer->get_billing_postcode(),
'country' => wc()->customer->get_billing_country(),
+ 'phone' => wc()->customer->get_billing_phone(),
];
}
-
wc()->customer->set_props(
array(
'billing_first_name' => isset( $billing['first_name'] ) ? $billing['first_name'] : null,
@@ -104,8 +104,8 @@ protected function get_route_post_response( \WP_REST_Request $request ) {
'billing_state' => isset( $billing['state'] ) ? $billing['state'] : null,
'billing_postcode' => isset( $billing['postcode'] ) ? $billing['postcode'] : null,
'billing_country' => isset( $billing['country'] ) ? $billing['country'] : null,
+ 'billing_phone' => isset( $billing['phone'] ) ? $billing['phone'] : null,
'billing_email' => isset( $request['billing_address'], $request['billing_address']['email'] ) ? $request['billing_address']['email'] : null,
- 'billing_phone' => isset( $request['billing_address'], $request['billing_address']['phone'] ) ? $request['billing_address']['phone'] : null,
'shipping_first_name' => isset( $shipping['first_name'] ) ? $shipping['first_name'] : null,
'shipping_last_name' => isset( $shipping['last_name'] ) ? $shipping['last_name'] : null,
'shipping_company' => isset( $shipping['company'] ) ? $shipping['company'] : null,
@@ -117,6 +117,16 @@ protected function get_route_post_response( \WP_REST_Request $request ) {
'shipping_country' => isset( $shipping['country'] ) ? $shipping['country'] : null,
)
);
+
+ $shipping_phone_value = isset( $shipping['phone'] ) ? $shipping['phone'] : null;
+
+ // @todo Remove custom shipping_phone handling (requires WC 5.6+)
+ if ( is_callable( [ wc()->customer, 'set_shipping_phone' ] ) ) {
+ wc()->customer->set_shipping_phone( $shipping_phone_value );
+ } else {
+ wc()->customer->update_meta_data( 'shipping_phone', $shipping_phone_value );
+ }
+
wc()->customer->save();
$this->calculate_totals();
@@ -163,6 +173,14 @@ protected function maybe_update_order() {
]
);
+ $shipping_phone_value = is_callable( [ wc()->customer, 'get_shipping_phone' ] ) ? wc()->customer->get_shipping_phone() : wc()->customer->get_meta( 'shipping_phone', true );
+
+ if ( is_callable( [ $draft_order, 'set_shipping_phone' ] ) ) {
+ $draft_order->set_shipping_phone( $shipping_phone_value );
+ } else {
+ $draft_order->update_meta_data( '_shipping_phone', $shipping_phone_value );
+ }
+
$draft_order->save();
}
}
diff --git a/src/StoreApi/Routes/Checkout.php b/src/StoreApi/Routes/Checkout.php
index e0faa213543..72e52c037c2 100644
--- a/src/StoreApi/Routes/Checkout.php
+++ b/src/StoreApi/Routes/Checkout.php
@@ -460,6 +460,8 @@ private function update_customer_from_request( WP_REST_Request $request ) {
foreach ( $request['shipping_address'] as $key => $value ) {
if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) {
$customer->{"set_shipping_$key"}( $value );
+ } elseif ( 'phone' === $key ) {
+ $customer->update_meta_data( 'shipping_phone', $value );
}
}
}
diff --git a/src/StoreApi/Schemas/AbstractAddressSchema.php b/src/StoreApi/Schemas/AbstractAddressSchema.php
index eeb8c2a538d..5b70759902e 100644
--- a/src/StoreApi/Schemas/AbstractAddressSchema.php
+++ b/src/StoreApi/Schemas/AbstractAddressSchema.php
@@ -74,6 +74,12 @@ public function get_properties() {
'context' => [ 'view', 'edit' ],
'required' => true,
],
+ 'phone' => [
+ 'description' => __( 'Phone', 'woo-gutenberg-products-block' ),
+ 'type' => 'string',
+ 'context' => [ 'view', 'edit' ],
+ 'required' => true,
+ ],
];
}
@@ -96,6 +102,7 @@ public function sanitize_callback( $address, $request, $param ) {
$address['city'] = wc_clean( wp_unslash( $address['city'] ) );
$address['state'] = $this->format_state( wc_clean( wp_unslash( $address['state'] ) ), $address['country'] );
$address['postcode'] = $address['postcode'] ? wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
+ $address['phone'] = wc_clean( wp_unslash( $address['phone'] ) );
return $address;
}
@@ -176,6 +183,13 @@ public function validate_callback( $address, $request, $param ) {
);
}
+ if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) {
+ $errors->add(
+ 'invalid_phone',
+ __( 'The provided phone number is not valid', 'woo-gutenberg-products-block' )
+ );
+ }
+
return $errors->has_errors( $errors ) ? $errors : true;
}
}
diff --git a/src/StoreApi/Schemas/BillingAddressSchema.php b/src/StoreApi/Schemas/BillingAddressSchema.php
index fcd5a5f4b63..bdff2d209d4 100644
--- a/src/StoreApi/Schemas/BillingAddressSchema.php
+++ b/src/StoreApi/Schemas/BillingAddressSchema.php
@@ -42,12 +42,6 @@ public function get_properties() {
'context' => [ 'view', 'edit' ],
'required' => true,
],
- 'phone' => [
- 'description' => __( 'Phone', 'woo-gutenberg-products-block' ),
- 'type' => 'string',
- 'context' => [ 'view', 'edit' ],
- 'required' => true,
- ],
]
);
}
@@ -63,7 +57,6 @@ public function get_properties() {
public function sanitize_callback( $address, $request, $param ) {
$address = parent::sanitize_callback( $address, $request, $param );
$address['email'] = wc_clean( wp_unslash( $address['email'] ) );
- $address['phone'] = wc_clean( wp_unslash( $address['phone'] ) );
return $address;
}
@@ -87,13 +80,6 @@ public function validate_callback( $address, $request, $param ) {
);
}
- if ( ! empty( $address['phone'] ) && ! \WC_Validation::is_phone( $address['phone'] ) ) {
- $errors->add(
- 'invalid_phone',
- __( 'The provided phone number is not valid', 'woo-gutenberg-products-block' )
- );
- }
-
return $errors->has_errors( $errors ) ? $errors : true;
}
diff --git a/src/StoreApi/Schemas/ShippingAddressSchema.php b/src/StoreApi/Schemas/ShippingAddressSchema.php
index 880f3eac529..922c714a11b 100644
--- a/src/StoreApi/Schemas/ShippingAddressSchema.php
+++ b/src/StoreApi/Schemas/ShippingAddressSchema.php
@@ -36,6 +36,12 @@ class ShippingAddressSchema extends AbstractAddressSchema {
*/
public function get_item_response( $address ) {
if ( ( $address instanceof \WC_Customer || $address instanceof \WC_Order ) ) {
+ if ( is_callable( [ $address, 'get_shipping_phone' ] ) ) {
+ $shipping_phone = $address->get_shipping_phone();
+ } else {
+ $shipping_phone = $address->get_meta( $address instanceof \WC_Customer ? 'shipping_phone' : '_shipping_phone', true );
+ }
+
return (object) $this->prepare_html_response(
[
'first_name' => $address->get_shipping_first_name(),
@@ -47,6 +53,7 @@ public function get_item_response( $address ) {
'state' => $address->get_shipping_state(),
'postcode' => $address->get_shipping_postcode(),
'country' => $address->get_shipping_country(),
+ 'phone' => $shipping_phone,
]
);
}
diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php
index ff68f4cad05..9ed78a807cc 100644
--- a/src/StoreApi/Utilities/OrderController.php
+++ b/src/StoreApi/Utilities/OrderController.php
@@ -94,6 +94,15 @@ public function sync_customer_data_with_order( \WC_Order $order ) {
'shipping_country' => $order->get_shipping_country(),
]
);
+
+ $shipping_phone_value = is_callable( [ $order, 'get_shipping_phone' ] ) ? $order->get_shipping_phone() : $order->get_meta( '_shipping_phone', true );
+
+ if ( is_callable( [ $customer, 'set_shipping_phone' ] ) ) {
+ $customer->set_shipping_phone( $shipping_phone_value );
+ } else {
+ $customer->update_meta_data( 'shipping_phone', $shipping_phone_value );
+ }
+
$customer->save();
};
}
@@ -477,5 +486,13 @@ protected function update_addresses_from_cart( \WC_Order $order ) {
'shipping_country' => wc()->customer->get_shipping_country(),
]
);
+
+ $shipping_phone_value = is_callable( [ wc()->customer, 'get_shipping_phone' ] ) ? wc()->customer->get_shipping_phone() : wc()->customer->get_meta( 'shipping_phone', true );
+
+ if ( is_callable( [ $order, 'set_shipping_phone' ] ) ) {
+ $order->set_shipping_phone( $shipping_phone_value );
+ } else {
+ $order->update_meta_data( '_shipping_phone', $shipping_phone_value );
+ }
}
}