initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
export const blockName = 'woocommerce/checkout';
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 ),
},
};

View File

@ -0,0 +1,191 @@
/**
* 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'
) }
<a href={ LOGIN_TO_CHECKOUT_URL }>
{ __(
'Click here to log in.',
'woo-gutenberg-products-block'
) }
</a>
</>
);
};
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 <EmptyCart />;
}
if ( ! hasOrder ) {
return <CheckoutOrderError />;
}
if (
isLoginRequired( customerId ) &&
allowCreateAccount &&
getSetting( 'checkoutAllowsSignup', false )
) {
<LoginPrompt />;
}
return (
<CheckoutBlockContext.Provider
value={ {
allowCreateAccount,
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
} }
>
{ children }
</CheckoutBlockContext.Provider>
);
};
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( () => {
let scrollToTopTimeout: number;
if ( hasErrorsToDisplay ) {
showAllValidationErrors();
// Scroll after a short timeout to allow a re-render. This will allow focusableSelector to match updated components.
scrollToTopTimeout = window.setTimeout( () => {
scrollToTop( {
focusableSelector: 'input:invalid, .has-error input',
} );
}, 50 );
}
return () => {
clearTimeout( scrollToTopTimeout );
};
}, [ hasErrorsToDisplay, scrollToTop, showAllValidationErrors ] );
return null;
};
const Block = ( {
attributes,
children,
scrollToTop,
}: {
attributes: Attributes;
children: React.ReactChildren;
scrollToTop: ( props: Record< string, unknown > ) => void;
} ): JSX.Element => (
<BlockErrorBoundary
header={ __( 'Something went wrong…', 'woo-gutenberg-products-block' ) }
text={ createInterpolateElement(
__(
'The checkout has encountered an unexpected error. <button>Try reloading the page</button>. If the error persists, please get in touch with us so we can assist.',
'woo-gutenberg-products-block'
),
{
button: (
<button
className="wc-block-link-button"
onClick={ reloadPage }
/>
),
}
) }
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreSnackbarNoticesProvider context="wc/checkout">
<StoreNoticesProvider context="wc/checkout">
<ValidationContextProvider>
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider>
<CheckoutProvider>
<SidebarLayout
className={ classnames( 'wc-block-checkout', {
'has-dark-controls':
attributes.hasDarkControls,
} ) }
>
<Checkout attributes={ attributes }>
{ children }
</Checkout>
<ScrollOnError scrollToTop={ scrollToTop } />
</SidebarLayout>
</CheckoutProvider>
</SlotFillProvider>
</ValidationContextProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
</BlockErrorBoundary>
);
export default withScrollToTop( Block );

View File

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

View File

@ -0,0 +1,137 @@
/**
* 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 './style.scss';
import {
PRODUCT_OUT_OF_STOCK,
PRODUCT_NOT_PURCHASABLE,
PRODUCT_NOT_ENOUGH_STOCK,
PRODUCT_SOLD_INDIVIDUALLY,
GENERIC_CART_ITEM_ERROR,
} from './constants';
const cartItemErrorCodes = [
PRODUCT_OUT_OF_STOCK,
PRODUCT_NOT_PURCHASABLE,
PRODUCT_NOT_ENOUGH_STOCK,
PRODUCT_SOLD_INDIVIDUALLY,
GENERIC_CART_ITEM_ERROR,
];
/**
* 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.',
'woocommerce'
),
};
return (
<div className="wc-block-checkout-error">
<Icon
className="wc-block-checkout-error__image"
alt=""
srcElement={ removeCart }
size={ 100 }
/>
<ErrorTitle errorData={ errorData } />
<ErrorMessage errorData={ errorData } />
<ErrorButton errorData={ errorData } />
</div>
);
};
/**
* Get the error message to display.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.errorData Object containing code and message.
*/
const ErrorTitle = ( { errorData } ) => {
let heading = __( 'Checkout error', 'woocommerce' );
if ( cartItemErrorCodes.includes( errorData.code ) ) {
heading = __(
'There is a problem with your cart',
'woocommerce'
);
}
return (
<strong className="wc-block-checkout-error_title">{ heading }</strong>
);
};
/**
* Get the error message to display.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.errorData Object containing code and message.
*/
const ErrorMessage = ( { errorData } ) => {
let message = errorData.message;
if ( cartItemErrorCodes.includes( errorData.code ) ) {
message =
message +
' ' +
__(
'Please edit your cart and try again.',
'woocommerce'
);
}
return <p className="wc-block-checkout-error__description">{ message }</p>;
};
/**
* Get the CTA button to display.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.errorData Object containing code and message.
*/
const ErrorButton = ( { errorData } ) => {
let buttonText = __( 'Retry', 'woocommerce' );
let buttonUrl = 'javascript:window.location.reload(true)';
if ( cartItemErrorCodes.includes( errorData.code ) ) {
buttonText = __( 'Edit your cart', 'woocommerce' );
buttonUrl = CART_URL;
}
return (
<span className="wp-block-button">
<a href={ buttonUrl } className="wp-block-button__link">
{ buttonText }
</a>
</span>
);
};
export default CheckoutOrderError;

View File

@ -0,0 +1,21 @@
.wc-block-checkout-error {
padding: $gap-largest;
text-align: center;
width: 100%;
.wc-block-checkout-error__image {
max-width: 150px;
margin: 0 auto 1em;
display: block;
color: inherit;
}
.wc-block-checkout-error__title {
display: block;
margin: 0;
font-weight: bold;
}
.wc-block-checkout-error__description {
display: block;
margin: 0.25em 0 1em 0;
}
}

View File

@ -0,0 +1,15 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
export const Columns = ( {
children,
...props
}: {
children?: React.ReactNode;
} ): JSX.Element => {
const blockProps = useBlockProps( props );
return <div { ...blockProps }>{ children }</div>;
};

View File

@ -0,0 +1 @@
export * from './columns-block';

View File

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

View File

@ -0,0 +1,322 @@
/**
* 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';
import { CartCheckoutCompatibilityNotice } from '@woocommerce/editor-components/compatibility-notices';
/**
* Internal dependencies
*/
import './styles/editor.scss';
import { Columns } from './columns';
import { addClassToBody, useBlockPropsWithLocking } 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 (
<InspectorControls>
{ currentPostId !== CHECKOUT_PAGE_ID && (
<Notice
className="wc-block-checkout__page-notice"
isDismissible={ false }
status="warning"
>
{ createInterpolateElement(
__(
'If you would like to use this block as your default checkout you must update your <a>page settings in WooCommerce</a>.',
'woo-gutenberg-products-block'
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={ getAdminLink(
'admin.php?page=wc-settings&tab=advanced'
) }
target="_blank"
rel="noopener noreferrer"
/>
),
}
) }
</Notice>
) }
<PanelBody title={ __( 'Style', 'woo-gutenberg-products-block' ) }>
<ToggleControl
label={ __(
'Dark mode inputs',
'woo-gutenberg-products-block'
) }
help={ __(
'Inputs styled specifically for use on dark background colors.',
'woo-gutenberg-products-block'
) }
checked={ hasDarkControls }
onChange={ () =>
setAttributes( {
hasDarkControls: ! hasDarkControls,
} )
}
/>
</PanelBody>
<CartCheckoutFeedbackPrompt />
</InspectorControls>
);
};
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 => (
<InspectorControls>
<PanelBody
title={ __(
'Account options',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Allow shoppers to sign up for a user account during checkout',
'woo-gutenberg-products-block'
) }
checked={ allowCreateAccount }
onChange={ () =>
setAttributes( {
allowCreateAccount: ! allowCreateAccount,
} )
}
/>
</PanelBody>
</InspectorControls>
);
const addressFieldControls = (): JSX.Element => (
<InspectorControls>
<PanelBody
title={ __( 'Address Fields', 'woo-gutenberg-products-block' ) }
>
<p className="wc-block-checkout__controls-text">
{ __(
'Show or hide fields in the checkout address forms.',
'woo-gutenberg-products-block'
) }
</p>
<ToggleControl
label={ __( 'Company', 'woo-gutenberg-products-block' ) }
checked={ showCompanyField }
onChange={ () => toggleAttribute( 'showCompanyField' ) }
/>
{ showCompanyField && (
<CheckboxControl
label={ __(
'Require company name?',
'woo-gutenberg-products-block'
) }
checked={ requireCompanyField }
onChange={ () =>
toggleAttribute( 'requireCompanyField' )
}
className="components-base-control--nested"
/>
) }
<ToggleControl
label={ __(
'Apartment, suite, etc.',
'woo-gutenberg-products-block'
) }
checked={ showApartmentField }
onChange={ () => toggleAttribute( 'showApartmentField' ) }
/>
<ToggleControl
label={ __( 'Phone', 'woo-gutenberg-products-block' ) }
checked={ showPhoneField }
onChange={ () => toggleAttribute( 'showPhoneField' ) }
/>
{ showPhoneField && (
<CheckboxControl
label={ __(
'Require phone number?',
'woo-gutenberg-products-block'
) }
checked={ requirePhoneField }
onChange={ () =>
toggleAttribute( 'requirePhoneField' )
}
className="components-base-control--nested"
/>
) }
</PanelBody>
</InspectorControls>
);
const blockProps = useBlockPropsWithLocking();
return (
<div { ...blockProps }>
<EditorProvider
previewData={ { previewCart, previewSavedPaymentMethods } }
>
<BlockSettings
attributes={ attributes }
setAttributes={ setAttributes }
/>
<CheckoutProvider>
<Columns>
<SidebarLayout
className={ classnames( 'wc-block-checkout', {
'has-dark-controls': attributes.hasDarkControls,
} ) }
>
<CheckoutBlockControlsContext.Provider
value={ {
addressFieldControls,
accountControls,
} }
>
<CheckoutBlockContext.Provider
value={ {
allowCreateAccount,
showCompanyField,
requireCompanyField,
showApartmentField,
showPhoneField,
requirePhoneField,
} }
>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={ defaultInnerBlocksTemplate }
templateLock="insert"
/>
</CheckoutBlockContext.Provider>
</CheckoutBlockControlsContext.Provider>
</SidebarLayout>
</Columns>
</CheckoutProvider>
</EditorProvider>
<CartCheckoutCompatibilityNotice blockName="checkout" />
</div>
);
};
export const Save = (): JSX.Element => {
return (
<div
{ ...useBlockProps.save( {
className: 'wc-block-checkout is-loading',
} ) }
>
<InnerBlocks.Content />
</div>
);
};

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { getBlockTypes } from '@wordpress/blocks';
// List of core block types to allow in inner block areas.
const coreBlockTypes = [ 'core/paragraph', 'core/image', 'core/separator' ];
/**
* Gets a list of allowed blocks types under a specific parent block type.
*/
export const getAllowedBlocks = ( block: string ): string[] => [
...getBlockTypes()
.filter( ( blockType ) =>
( blockType?.parent || [] ).includes( block )
)
.map( ( { name } ) => name ),
...coreBlockTypes,
];

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { SHOP_URL } from '@woocommerce/block-settings';
import { Icon, cart } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import './style.scss';
const EmptyCart = () => {
return (
<div className="wc-block-checkout-empty">
<Icon
className="wc-block-checkout-empty__image"
alt=""
srcElement={ cart }
size={ 100 }
/>
<strong className="wc-block-checkout-empty__title">
{ __( 'Your cart is empty!', 'woocommerce' ) }
</strong>
<p className="wc-block-checkout-empty__description">
{ __(
"Checkout is not available whilst your cart is empty—please take a look through our store and come back when you're ready to place an order.",
'woocommerce'
) }
</p>
{ SHOP_URL && (
<span className="wp-block-button">
<a href={ SHOP_URL } className="wp-block-button__link">
{ __( 'Browse store', 'woocommerce' ) }
</a>
</span>
) }
</div>
);
};
export default EmptyCart;

View File

@ -0,0 +1,21 @@
.wc-block-checkout-empty {
padding: $gap-largest;
text-align: center;
width: 100%;
.wc-block-checkout-empty__image {
max-width: 150px;
margin: 0 auto 1em;
display: block;
color: inherit;
}
.wc-block-checkout-empty__title {
display: block;
margin: 0;
font-weight: bold;
}
.wc-block-checkout-empty__description {
display: block;
margin: 0.25em 0 1em 0;
}
}

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import './editor.scss';
import { useForcedLayout } from '../use-forced-layout';
import { getAllowedBlocks } from '../editor-utils';
export const AdditionalFields = ( {
block,
}: {
// Name of the parent block.
block: string;
} ): JSX.Element => {
const { 'data-block': clientId } = useBlockProps();
const allowedBlocks = getAllowedBlocks( block );
useForcedLayout( {
clientId,
template: allowedBlocks,
} );
return (
<div className="wc-block-checkout__additional_fields">
<InnerBlocks allowedBlocks={ allowedBlocks } />
</div>
);
};
export const AdditionalFieldsContent = (): JSX.Element => (
<InnerBlocks.Content />
);

View File

@ -0,0 +1,32 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
const attributes = ( {
defaultTitle = __( 'Step', 'woo-gutenberg-products-block' ),
defaultDescription = __(
'Step description text.',
'woo-gutenberg-products-block'
),
defaultShowStepNumber = true,
}: {
defaultTitle: string;
defaultDescription: string;
defaultShowStepNumber?: boolean;
} ): Record< string, Record< string, unknown > > => ( {
title: {
type: 'string',
default: defaultTitle,
},
description: {
type: 'string',
default: defaultDescription,
},
showStepNumber: {
type: 'boolean',
default: defaultShowStepNumber,
},
} );
export default attributes;

View File

@ -0,0 +1,12 @@
.wc-block-checkout__additional_fields {
margin: 1.5em 0 -1.5em;
}
.wc-block-components-checkout-step__description-placeholder {
opacity: 0.5;
}
.wc-block-components-checkout-step__title {
display: flex;
width: 100%;
}

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import {
PlainText,
InspectorControls,
useBlockProps,
} from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
/**
* Internal dependencies
*/
import FormStepHeading from './form-step-heading';
export interface FormStepBlockProps {
attributes: { title: string; description: string; showStepNumber: boolean };
setAttributes: ( attributes: Record< string, unknown > ) => void;
className?: string;
children?: React.ReactNode;
lock?: { move: boolean; remove: boolean };
}
/**
* Form Step Block for use in the editor.
*/
export const FormStepBlock = ( {
attributes,
setAttributes,
className = '',
children,
}: FormStepBlockProps ): JSX.Element => {
const { title = '', description = '', showStepNumber = true } = attributes;
const blockProps = useBlockProps( {
className: classnames( 'wc-block-components-checkout-step', className, {
'wc-block-components-checkout-step--with-step-number': showStepNumber,
} ),
} );
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody
title={ __(
'Form Step Options',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Show step number',
'woo-gutenberg-products-block'
) }
checked={ showStepNumber }
onChange={ () =>
setAttributes( {
showStepNumber: ! showStepNumber,
} )
}
/>
</PanelBody>
</InspectorControls>
<FormStepHeading>
<PlainText
className={ '' }
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
/>
</FormStepHeading>
<div className="wc-block-components-checkout-step__container">
<p className="wc-block-components-checkout-step__description">
<PlainText
className={
! description
? 'wc-block-components-checkout-step__description-placeholder'
: ''
}
value={ description }
placeholder={ __(
'Optional text for this form step.',
'woo-gutenberg-products-block'
) }
onChange={ ( value ) =>
setAttributes( {
description: value,
} )
}
/>
</p>
<div className="wc-block-components-checkout-step__content">
{ children }
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,32 @@
/**
* External dependencies
*/
import Title from '@woocommerce/base-components/title';
/**
* Step Heading Component
*/
const FormStepHeading = ( {
children,
stepHeadingContent,
}: {
children: JSX.Element;
stepHeadingContent?: JSX.Element;
} ): JSX.Element => (
<div className="wc-block-components-checkout-step__heading">
<Title
aria-hidden="true"
className="wc-block-components-checkout-step__title"
headingLevel="2"
>
{ children }
</Title>
{ !! stepHeadingContent && (
<span className="wc-block-components-checkout-step__heading-content">
{ stepHeadingContent }
</span>
) }
</div>
);
export default FormStepHeading;

View File

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

View File

@ -0,0 +1,69 @@
/**
* External dependencies
*/
import { Children, cloneElement, isValidElement } from '@wordpress/element';
import { getValidBlockAttributes } from '@woocommerce/base-utils';
import { useStoreCart } from '@woocommerce/base-context';
import {
useCheckoutExtensionData,
useValidation,
} from '@woocommerce/base-context/hooks';
import { getRegisteredBlockComponents } from '@woocommerce/blocks-registry';
import {
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();
const checkoutExtensionData = useCheckoutExtensionData();
const validation = useValidation();
return Children.map( children, ( child ) => {
if ( isValidElement( child ) ) {
const componentProps = {
extensions,
cart,
checkoutExtensionData,
validation,
};
return cloneElement( child, componentProps );
}
return child;
} );
};
renderParentBlock( {
Block: withStoreCartApiHydration( withRestApiHydration( Block ) ),
blockName,
selector: '.wp-block-woocommerce-checkout',
getProps,
blockMap: getRegisteredBlockComponents( blockName ) as Record<
string,
React.ReactNode
>,
blockWrapper: Wrapper,
} );

View File

@ -0,0 +1,176 @@
/**
* 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 { subscribe, select as _select } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { MutableRefObject } from 'react';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
import { hasFilter } from '@wordpress/hooks';
import { getBlockType } from '@wordpress/blocks';
/**
* 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 => {
if ( ! hasFilter( 'blocks.registerBlockType', 'core/lock/addAttribute' ) ) {
subscribe( () => {
const blockEditorSelect = _select( blockEditorStore );
if ( ! blockEditorSelect ) {
return;
}
const selectedBlock = blockEditorSelect.getSelectedBlock();
if ( ! selectedBlock ) {
return;
}
toggleBodyClass(
'wc-lock-selected-block--remove',
!! selectedBlock?.attributes?.lock?.remove
);
toggleBodyClass(
'wc-lock-selected-block--move',
!! selectedBlock?.attributes?.lock?.move
);
} );
}
};
const isBlockLocked = ( clientId: string ): boolean => {
if ( ! clientId ) {
return false;
}
const { getBlock } = _select( blockEditorStore );
const block = getBlock( clientId );
// If lock.remove is defined at the block instance (not using the default value)
// Then we use it.
if ( typeof block?.attributes?.lock?.remove === 'boolean' ) {
return block.attributes.lock.remove;
}
// If we don't have lock on the block instance, we check the type
const blockType = getBlockType( block.name );
if ( typeof blockType?.attributes?.lock?.default?.remove === 'boolean' ) {
return blockType?.attributes?.lock?.default?.remove;
}
// If nothing is defined, return false
return false;
};
/**
* This is a hook we use in conjunction with useBlockProps. Its goal is to check if of the block's children is locked and being deleted.
* It will stop the keydown event from propagating to stop it from being deleted via the keyboard.
*
*/
const useLockedChildren = ( {
ref,
}: {
ref: MutableRefObject< HTMLElement | undefined >;
} ): void => {
const lockInCore = hasFilter(
'blocks.registerBlockType',
'core/lock/addAttribute'
);
const node = ref.current;
return useEffect( () => {
if ( ! node || lockInCore ) {
return;
}
function onKeyDown( event: KeyboardEvent ) {
const { keyCode, target } = event;
if ( ! ( target instanceof HTMLElement ) ) {
return;
}
// We're not trying to delete something here.
if ( keyCode !== BACKSPACE && keyCode !== DELETE ) {
return;
}
// We're in a field, so we should let text be deleted.
if ( isTextField( target ) ) {
return;
}
// Typecast to fix issue with isTextField.
const targetNode = target as HTMLElement;
// Our target isn't a block.
if ( targetNode.dataset.block === undefined ) {
return;
}
const clientId = targetNode.dataset.block;
const isLocked = isBlockLocked( clientId );
// Prevent the keyboard event from propogating if it supports locking.
if ( isLocked ) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
node.addEventListener( 'keydown', onKeyDown, {
capture: true,
passive: false,
} );
return () => {
node.removeEventListener( 'keydown', onKeyDown, {
capture: true,
} );
};
}, [ node, lockInCore ] );
};
/**
* 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< HTMLElement >();
const blockProps = useBlockProps( { ref, ...props } );
useLockedChildren( {
ref,
} );
return blockProps;
};

View File

@ -0,0 +1,55 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { Icon, fields } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import { blockName, blockAttributes } from './attributes';
import './inner-blocks';
const settings = {
title: __( 'Checkout', 'woo-gutenberg-products-block' ),
icon: {
src: <Icon srcElement={ fields } />,
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,
// Migrates v1 to v2 checkout.
deprecated: [
{
attributes: blockAttributes,
save( { attributes }: { attributes: { className: string } } ) {
return (
<div
className={ classnames(
'is-loading',
attributes.className
) }
/>
);
},
},
],
};
registerFeaturePluginBlockType( blockName, settings );

View File

@ -0,0 +1,17 @@
export default {
cartPageId: {
type: 'number',
default: 0,
},
showReturnToCart: {
type: 'boolean',
default: true,
},
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-actions-block",
"version": "1.0.0",
"title": "Actions",
"description": "Allow customers to place their order.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 (
<div className="wc-block-checkout__actions">
{ showReturnToCart && (
<ReturnToCartButton
link={ getSetting( 'page-' + cartPageId, false ) }
/>
) }
<PlaceOrderButton />
</div>
);
};
export default Block;

View File

@ -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';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
showReturnToCart: boolean;
cartPageId: number;
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const blockProps = useBlockProps();
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 (
<div { ...blockProps }>
<InspectorControls>
<PanelBody
title={ __(
'Account options',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Show a "Return to Cart" link',
'woo-gutenberg-products-block'
) }
checked={ showReturnToCart }
onChange={ () =>
setAttributes( {
showReturnToCart: ! showReturnToCart,
} )
}
/>
</PanelBody>
{ showReturnToCart &&
! (
currentPostId === CHECKOUT_PAGE_ID &&
savedCartPageId === 0
) && (
<PageSelector
pageId={ cartPageId }
setPageId={ ( id: number ) =>
setAttributes( { cartPageId: id } )
}
labels={ {
title: __(
'Return to Cart button',
'woo-gutenberg-products-block'
),
default: __(
'WooCommerce Cart Page',
'woo-gutenberg-products-block'
),
} }
/>
) }
</InspectorControls>
<Disabled>
<Block
showReturnToCart={ showReturnToCart }
cartPageId={ cartPageId }
/>
</Disabled>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() } />;
};

View File

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

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, button } from '@wordpress/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import attributes from './attributes';
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon icon={ button } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,39 @@
.wc-block-checkout__actions {
display: flex;
justify-content: space-between;
align-items: center;
.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);
padding: em($gap-large) 0;
}
}

View File

@ -0,0 +1,26 @@
/**
* 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'
),
} ),
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-billing-address-block",
"version": "1.0.0",
"title": "Billing Address",
"description": "Collect your customer's billing address.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 (
<AddressFormWrapperComponent>
<AddressForm
id="billing"
type="billing"
onChange={ ( values: Record< string, unknown > ) => {
setBillingFields( values );
dispatchCheckoutEvent( 'set-billing-address' );
} }
values={ billingFields }
fields={ Object.keys( defaultAddressFields ) }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
isRequired={ requirePhoneField }
value={ billingFields.phone }
onChange={ ( value ) => {
setPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'billing',
} );
} }
/>
) }
</AddressFormWrapperComponent>
);
};
export default Block;

View File

@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { useCheckoutAddress } from '@woocommerce/base-context/hooks';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* 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 (
<FormStepBlock
setAttributes={ setAttributes }
attributes={ attributes }
className="wc-block-checkout__billing-fields"
>
<Controls />
<Block
showCompanyField={ showCompanyField }
showApartmentField={ showApartmentField }
requireCompanyField={ requireCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
/>
<AdditionalFields block={ innerBlockAreas.BILLING_ADDRESS } />
</FormStepBlock>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<AdditionalFieldsContent />
</div>
);
};

View File

@ -0,0 +1,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 (
<FormStep
id="billing-fields"
disabled={ checkoutIsProcessing }
className="wc-block-checkout__billing-fields"
title={ title }
description={ description }
showStepNumber={ showStepNumber }
>
<Block
requireCompanyField={ requireCompanyField }
showApartmentField={ showApartmentField }
showCompanyField={ showCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
/>
{ children }
</FormStep>
);
};
export default withFilteredAttributes( attributes )( FrontendBlock );

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, address } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ address } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,29 @@
/**
* 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'
),
} ),
lock: {
type: 'object',
default: {
remove: true,
move: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-contact-information-block",
"version": "1.0.0",
"title": "Contact Information",
"description": "Collect your customer's contact information.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 ) && (
<CheckboxControl
className="wc-block-checkout__create-account"
label={ __(
'Create an account?',
'woo-gutenberg-products-block'
) }
checked={ shouldCreateAccount }
onChange={ ( value ) => setShouldCreateAccount( value ) }
/>
);
return (
<>
<ValidatedTextInput
id="email"
type="email"
label={ __( 'Email address', 'woo-gutenberg-products-block' ) }
value={ billingFields.email }
autoComplete="email"
onChange={ onChangeEmail }
required={ true }
/>
{ createAccountUI }
</>
);
};
export default Block;

View File

@ -0,0 +1,55 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* 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 (
<FormStepBlock
attributes={ attributes }
setAttributes={ setAttributes }
>
<Controls />
<Disabled>
<Block allowCreateAccount={ allowCreateAccount } />
</Disabled>
<AdditionalFields block={ innerBlockAreas.CONTACT_INFORMATION } />
</FormStepBlock>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<AdditionalFieldsContent />
</div>
);
};

View File

@ -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 (
<FormStep
id="contact-fields"
disabled={ checkoutIsProcessing }
className="wc-block-checkout__contact-fields"
title={ title }
description={ description }
showStepNumber={ showStepNumber }
stepHeadingContent={ () => <LoginPrompt /> }
>
<Block allowCreateAccount={ allowCreateAccount } />
{ children }
</FormStep>
);
};
export default withFilteredAttributes( attributes )( FrontendBlock );

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, contact } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ contact } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -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? ',
'woocommerce'
) }
<a href={ LOGIN_TO_CHECKOUT_URL }>
{ __( 'Log in.', 'woocommerce' ) }
</a>
</>
);
};
export default LoginPrompt;

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-express-payment-block",
"version": "1.0.0",
"title": "Express Checkout",
"description": "Provide an express payment option for your customers.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 <CheckoutExpressPayment />;
};
export default Block;

View File

@ -0,0 +1,86 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { Placeholder, Button } from 'wordpress-components';
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
import { Icon, card } from '@woocommerce/icons';
import { ADMIN_URL } from '@woocommerce/settings';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import Block from './block';
import './editor.scss';
/**
* Renders a placeholder in the editor.
*/
const NoExpressPaymentMethodsPlaceholder = () => {
return (
<Placeholder
icon={ <Icon srcElement={ card } /> }
label={ __( 'Express Checkout', 'woo-gutenberg-products-block' ) }
className="wp-block-woocommerce-checkout-express-payment-block-placeholder"
>
<span className="wp-block-woocommerce-checkout-express-payment-block-placeholder__description">
{ __(
"Your store doesn't have any Payment Methods that support the Express Checkout Block. If they are added, they will be shown here.",
'woo-gutenberg-products-block'
) }
</span>
<Button
isPrimary
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout` }
target="_blank"
rel="noopener noreferrer"
className="wp-block-woocommerce-checkout-express-payment-block-placeholder__button"
>
{ __(
'Configure Payment Methods',
'woo-gutenberg-products-block'
) }
</Button>
</Placeholder>
);
};
export const Edit = ( {
attributes,
}: {
attributes: {
lock: {
move: boolean;
remove: boolean;
};
};
} ): JSX.Element | null => {
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
const blockProps = useBlockProps( {
className: classnames( {
'wp-block-woocommerce-checkout-express-payment-block--has-express-payment-methods': hasExpressPaymentMethods,
} ),
attributes,
} );
if ( ! isInitialized ) {
return null;
}
return (
<div { ...blockProps }>
{ hasExpressPaymentMethods ? (
<Block />
) : (
<NoExpressPaymentMethodsPlaceholder />
) }
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() } />;
};

View File

@ -0,0 +1,29 @@
// Adjust padding and margins in the editor to improve selected block outlines.
.wp-block-woocommerce-checkout-express-payment-block {
margin: 14px 0 28px;
.components-placeholder__label svg {
font-size: 1em;
}
.wc-block-components-express-payment-continue-rule--checkout {
margin-bottom: 0;
}
&.wp-block-woocommerce-checkout-express-payment-block--has-express-payment-methods {
padding: 14px 0;
margin: -14px 0 14px 0 !important;
position: relative;
}
}
.wp-block-woocommerce-checkout-express-payment-block-placeholder {
* {
pointer-events: all; // Overrides parent disabled component in editor context
}
.wp-block-woocommerce-checkout-express-payment-block-placeholder__description {
display: block;
margin: 0 0 1em;
}
}

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, card } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ card } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-fields-block",
"version": "1.0.0",
"title": "Checkout Fields",
"description": "Column containing checkout address fields.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { Main } from '@woocommerce/base-components/sidebar-layout';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* Internal dependencies
*/
import { useCheckoutBlockControlsContext } from '../../context';
import { useForcedLayout } from '../../use-forced-layout';
import { getAllowedBlocks } from '../../editor-utils';
import './style.scss';
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
const blockProps = useBlockProps();
const allowedBlocks = getAllowedBlocks( innerBlockAreas.CHECKOUT_FIELDS );
const {
addressFieldControls: Controls,
} = useCheckoutBlockControlsContext();
useForcedLayout( {
clientId,
template: allowedBlocks,
} );
return (
<Main className="wc-block-checkout__main">
<div { ...blockProps }>
<Controls />
<form className="wc-block-components-form wc-block-checkout__form">
<InnerBlocks
allowedBlocks={ allowedBlocks }
templateLock={ false }
renderAppender={ InnerBlocks.ButtonBlockAppender }
/>
</form>
</div>
</Main>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { Main } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import './style.scss';
const FrontendBlock = ( {
children,
}: {
children: JSX.Element;
} ): JSX.Element => {
return (
<Main className="wc-block-checkout__main">
<form className="wc-block-components-form wc-block-checkout__form">
{ children }
</form>
</Main>
);
};
export default FrontendBlock;

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, column } from '@wordpress/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon icon={ column } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,53 @@
.wc-block-checkout__form {
margin: 0;
max-width: 100%;
}
.is-mobile,
.is-small,
.is-medium {
.wc-block-checkout__main {
order: 1;
}
}
.is-small,
.is-medium,
.is-large {
.wc-block-checkout__shipping-fields,
.wc-block-checkout__billing-fields {
.wc-block-components-address-form {
margin-left: #{-$gap-small * 0.5};
margin-right: #{-$gap-small * 0.5};
&::after {
content: "";
clear: both;
display: block;
}
.wc-block-components-text-input,
.wc-block-components-country-input,
.wc-block-components-state-input {
float: left;
margin-left: #{$gap-small * 0.5};
margin-right: #{$gap-small * 0.5};
position: relative;
width: calc(50% - #{$gap-small});
&:nth-of-type(2),
&:first-of-type {
margin-top: 0;
}
}
.wc-block-components-address-form__company,
.wc-block-components-address-form__address_1,
.wc-block-components-address-form__address_2 {
width: calc(100% - #{$gap-small});
}
.wc-block-components-checkbox {
clear: both;
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-order-note-block",
"version": "1.0.0",
"title": "Order Note",
"description": "Allow customers to add a note to their order.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 '../../order-notes';
const Block = (): JSX.Element => {
const { needsShipping } = useShippingDataContext();
const {
isProcessing: checkoutIsProcessing,
orderNotes,
dispatchActions,
} = useCheckoutContext();
const { setOrderNotes } = dispatchActions;
return (
<FormStep
id="order-notes"
showStepNumber={ false }
className="wc-block-checkout__order-notes"
disabled={ checkoutIsProcessing }
>
<CheckoutOrderNotes
disabled={ checkoutIsProcessing }
onChange={ setOrderNotes }
placeholder={
needsShipping
? __(
'Notes about your order, e.g. special notes for delivery.',
'woo-gutenberg-products-block'
)
: __(
'Notes about your order.',
'woo-gutenberg-products-block'
)
}
value={ orderNotes }
/>
</FormStep>
);
};
export default Block;

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
/**
* Internal dependencies
*/
import Block from './block';
import './editor.scss';
export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Disabled>
<Block />
</Disabled>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() } />;
};

View File

@ -0,0 +1,12 @@
// Adjust padding and margins in the editor to improve selected block outlines.
.wp-block-woocommerce-checkout-order-note-block {
margin-top: 20px;
margin-bottom: 20px;
padding-top: 4px;
padding-bottom: 4px;
.wc-block-checkout__add-note {
margin-top: 0;
margin-bottom: 0;
}
}

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, notes } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ notes } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
export default {
showRateAfterTaxName: {
type: 'boolean',
default: getSetting( 'displayCartPricesIncludingTax', false ),
},
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,25 @@
{
"name": "woocommerce/checkout-order-summary-block",
"version": "1.0.0",
"title": "Order Summary",
"description": "Show customers a summary of their order.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true
}
}
},
"parent": [ "woocommerce/checkout-totals-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,112 @@
/**
* 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 (
<>
<TotalsWrapper>
<OrderSummary cartItems={ cartItems } />
</TotalsWrapper>
<TotalsWrapper>
<Subtotal currency={ totalsCurrency } values={ cartTotals } />
<TotalsFees currency={ totalsCurrency } cartFees={ cartFees } />
<TotalsDiscount
cartCoupons={ cartCoupons }
currency={ totalsCurrency }
isRemovingCoupon={ isRemovingCoupon }
removeCoupon={ removeCoupon }
values={ cartTotals }
/>
</TotalsWrapper>
{ getSetting( 'couponsEnabled', true ) && (
<TotalsWrapper>
<TotalsCoupon
onSubmit={ applyCoupon }
initialOpen={ false }
isLoading={ isApplyingCoupon }
/>
</TotalsWrapper>
) }
{ needsShipping && (
<TotalsWrapper>
<TotalsShipping
showCalculator={ false }
showRateSelector={ false }
values={ cartTotals }
currency={ totalsCurrency }
/>
</TotalsWrapper>
) }
{ ! getSetting( 'displayCartPricesIncludingTax', false ) &&
parseInt( cartTotals.total_tax, 10 ) > 0 && (
<TotalsWrapper>
<TotalsTaxes
currency={ totalsCurrency }
showRateAfterTaxName={ showRateAfterTaxName }
values={ cartTotals }
/>
</TotalsWrapper>
) }
<TotalsWrapper>
<TotalsFooterItem
currency={ totalsCurrency }
values={ cartTotals }
/>
</TotalsWrapper>
<ExperimentalOrderMeta.Slot { ...slotFillProps } />
</>
);
};
export default Block;

View File

@ -0,0 +1,79 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import Block from './block';
export const Edit = ( {
attributes,
setAttributes,
}: {
attributes: {
showRateAfterTaxName: boolean;
lock: {
move: boolean;
remove: boolean;
};
};
setAttributes: ( attributes: Record< string, unknown > ) => void;
} ): JSX.Element => {
const blockProps = useBlockProps();
const taxesEnabled = getSetting( 'taxesEnabled' ) as boolean;
const displayItemizedTaxes = getSetting(
'displayItemizedTaxes',
false
) as boolean;
const displayCartPricesIncludingTax = getSetting(
'displayCartPricesIncludingTax',
false
) as boolean;
return (
<div { ...blockProps }>
<InspectorControls>
{ taxesEnabled &&
displayItemizedTaxes &&
! displayCartPricesIncludingTax && (
<PanelBody
title={ __(
'Taxes',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Show rate after tax name',
'woo-gutenberg-products-block'
) }
help={ __(
'Show the percentage rate alongside each tax line in the summary.',
'woo-gutenberg-products-block'
) }
checked={ attributes.showRateAfterTaxName }
onChange={ () =>
setAttributes( {
showRateAfterTaxName: ! attributes.showRateAfterTaxName,
} )
}
/>
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Block
showRateAfterTaxName={ attributes.showRateAfterTaxName }
/>
</Disabled>
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() } />;
};

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, totals } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ totals } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import formStepAttributes from '../../form-step/attributes';
export default {
...formStepAttributes( {
defaultTitle: __( 'Payment options', 'woo-gutenberg-products-block' ),
defaultDescription: '',
} ),
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-payment-block",
"version": "1.0.0",
"title": "Payment Options",
"description": "Payment options for your store.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 (
<StoreNoticesProvider context={ noticeContexts.PAYMENTS }>
<PaymentMethods />
</StoreNoticesProvider>
);
};
export default Block;

View File

@ -0,0 +1,87 @@
/**
* 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';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* 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 (
<FormStepBlock
{ ...props }
className="wc-block-checkout__payment-method"
>
<InspectorControls>
{ globalPaymentMethods.length > 0 && (
<PanelBody
title={ __(
'Methods',
'woo-gutenberg-products-block'
) }
>
<p className="wc-block-checkout__controls-text">
{ __(
'You currently have the following payment integrations active.',
'woo-gutenberg-products-block'
) }
</p>
{ globalPaymentMethods.map( ( method ) => {
return (
<ExternalLinkCard
key={ method.id }
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout&section=${ method.id }` }
title={ method.title }
description={ method.description }
/>
);
} ) }
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout` }
>
{ __(
'Manage payment methods',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Block />
</Disabled>
<AdditionalFields block={ innerBlockAreas.PAYMENT_METHODS } />
</FormStepBlock>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<AdditionalFieldsContent />
</div>
);
};

View File

@ -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 (
<FormStep
id="payment-method"
disabled={ checkoutIsProcessing }
className="wc-block-checkout__payment-method"
title={ title }
description={ description }
showStepNumber={ showStepNumber }
>
<StoreNoticesProvider context={ noticeContexts.PAYMENTS }>
<Block />
</StoreNoticesProvider>
{ children }
</FormStep>
);
};
export default withFilteredAttributes( attributes )( FrontendBlock );

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, card } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ card } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,26 @@
/**
* 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'
),
} ),
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-shipping-address-block",
"version": "1.0.0",
"title": "Shipping Address",
"description": "Collect your customer's shipping address.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -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 (
<>
<AddressFormWrapperComponent>
<AddressForm
id="shipping"
type="shipping"
onChange={ ( values: Record< string, unknown > ) => {
setShippingFields( values );
dispatchCheckoutEvent( 'set-shipping-address' );
} }
values={ shippingFields }
fields={ Object.keys( defaultAddressFields ) }
fieldConfig={ addressFieldsConfig }
/>
{ showPhoneField && (
<PhoneNumber
id="shipping-phone"
isRequired={ requirePhoneField }
value={ shippingFields.phone }
onChange={ ( value ) => {
setShippingPhone( value );
dispatchCheckoutEvent( 'set-phone-number', {
step: 'shipping',
} );
} }
/>
) }
</AddressFormWrapperComponent>
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
'woo-gutenberg-products-block'
) }
checked={ shippingAsBilling }
onChange={ ( checked: boolean ) =>
setShippingAsBilling( checked )
}
/>
</>
);
};
export default Block;

View File

@ -0,0 +1,67 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* 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 (
<FormStepBlock
setAttributes={ setAttributes }
attributes={ attributes }
className="wc-block-checkout__shipping-fields"
>
<Controls />
<Block
showCompanyField={ showCompanyField }
showApartmentField={ showApartmentField }
requireCompanyField={ requireCompanyField }
showPhoneField={ showPhoneField }
requirePhoneField={ requirePhoneField }
/>
<AdditionalFields block={ innerBlockAreas.SHIPPING_ADDRESS } />
</FormStepBlock>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<AdditionalFieldsContent />
</div>
);
};

View File

@ -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 (
<FormStep
id="shipping-fields"
disabled={ checkoutIsProcessing }
className="wc-block-checkout__shipping-fields"
title={ title }
description={ description }
showStepNumber={ showStepNumber }
>
<Block
requireCompanyField={ requireCompanyField }
requirePhoneField={ requirePhoneField }
showApartmentField={ showApartmentField }
showCompanyField={ showCompanyField }
showPhoneField={ showPhoneField }
/>
{ children }
</FormStep>
);
};
export default withFilteredAttributes( attributes )( FrontendBlock );

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, address } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ address } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,27 @@
/**
* 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: '',
} ),
allowCreateAccount: {
type: 'boolean',
default: false,
},
lock: {
type: 'object',
default: {
move: true,
remove: true,
},
},
};

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-shipping-methods-block",
"version": "1.0.0",
"title": "Shipping Options",
"description": "Shipping options for your store.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"lock": {
"type": "object",
"default": {
"remove": true,
"move": true
}
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,112 @@
/**
* 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';
import './style.scss';
/**
* 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: (
<FormattedMonetaryAmount
currency={ getCurrencyFromPriceResponse( option ) }
value={ priceWithTaxes }
/>
),
secondaryDescription: decodeEntities( option.delivery_time ),
};
};
const Block = (): JSX.Element | null => {
const { isEditor } = useEditorContext();
const {
shippingRates,
shippingRatesLoading,
needsShipping,
hasCalculatedShipping,
} = useShippingDataContext();
if ( ! needsShipping ) {
return null;
}
const shippingRatesPackageCount = getShippingRatesPackageCount(
shippingRates
);
if (
! isEditor &&
! hasCalculatedShipping &&
! shippingRatesPackageCount
) {
return (
<p>
{ __(
'Shipping options will be displayed here after entering your full shipping address.',
'woo-gutenberg-products-block'
) }
</p>
);
}
return (
<>
{ isEditor && ! shippingRatesPackageCount ? (
<NoShippingPlaceholder />
) : (
<ShippingRatesControl
noResultsMessage={
<Notice
isDismissible={ false }
className={ classnames(
'wc-block-components-shipping-rates-control__no-results-notice',
'woocommerce-error'
) }
>
{ __(
'There are no shipping options available. Please check your shipping address.',
'woo-gutenberg-products-block'
) }
</Notice>
}
renderOption={ renderShippingRatesControlOption }
shippingRates={ shippingRates }
shippingRatesLoading={ shippingRatesLoading }
/>
) }
</>
);
};
export default Block;

View File

@ -0,0 +1,130 @@
/**
* 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';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* 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 (
<FormStepBlock
attributes={ attributes }
setAttributes={ setAttributes }
>
<InspectorControls>
{ globalShippingMethods.length > 0 && (
<PanelBody
title={ __(
'Methods',
'woo-gutenberg-products-block'
) }
>
<p className="wc-block-checkout__controls-text">
{ __(
'You currently have the following shipping integrations active.',
'woo-gutenberg-products-block'
) }
</p>
{ globalShippingMethods.map( ( method ) => {
return (
<ExternalLinkCard
key={ method.id }
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping&section=${ method.id }` }
title={ method.title }
description={ method.description }
/>
);
} ) }
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
>
{ __(
'Manage shipping methods',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</PanelBody>
) }
{ activeShippingZones.length && (
<PanelBody
title={ __( 'Zones', 'woo-gutenberg-products-block' ) }
>
<p className="wc-block-checkout__controls-text">
{ __(
'You currently have the following shipping zones active.',
'woo-gutenberg-products-block'
) }
</p>
{ activeShippingZones.map( ( zone ) => {
return (
<ExternalLinkCard
key={ zone.id }
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping&zone_id=${ zone.id }` }
title={ zone.title }
description={ zone.description }
/>
);
} ) }
<ExternalLink
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
>
{ __(
'Manage shipping zones',
'woo-gutenberg-products-block'
) }
</ExternalLink>
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Block />
</Disabled>
<AdditionalFields block={ innerBlockAreas.SHIPPING_METHODS } />
</FormStepBlock>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<AdditionalFieldsContent />
</div>
);
};

View File

@ -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 (
<FormStep
id="shipping-option"
disabled={ checkoutIsProcessing }
className="wc-block-checkout__shipping-option"
title={ title }
description={ description }
showStepNumber={ showStepNumber }
>
<Block />
{ children }
</FormStep>
);
};
export default withFilteredAttributes( attributes )( FrontendBlock );

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { Icon, truck } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import attributes from './attributes';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ truck } />,
foreground: '#874FB9',
},
attributes,
edit: Edit,
save: Save,
} );

View File

@ -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 (
<Placeholder
icon={ <Icon srcElement={ truck } /> }
label={ __( 'Shipping options', 'woocommerce' ) }
className="wc-block-checkout__no-shipping-placeholder"
>
<span className="wc-block-checkout__no-shipping-placeholder-description">
{ __(
'Your store does not have any Shipping Options configured. Once you have added your Shipping Options they will appear here.',
'woocommerce'
) }
</span>
<Button
isSecondary
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping` }
target="_blank"
rel="noopener noreferrer"
>
{ __(
'Configure Shipping Options',
'woocommerce'
) }
</Button>
</Placeholder>
);
};
export default NoShippingPlaceholder;

View File

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

View File

@ -0,0 +1,11 @@
.wc-block-checkout__shipping-option {
.wc-block-components-radio-control__option {
@include with-translucent-border(0 0 1px);
margin: 0;
padding: em($gap-small) 0 em($gap-small) em($gap-largest);
}
.wc-block-components-shipping-rates-control__no-results-notice {
margin: em($gap-small) 0;
}
}

View File

@ -0,0 +1,26 @@
{
"name": "woocommerce/checkout-terms-block",
"version": "1.0.0",
"title": "Terms and Conditions",
"description": "Ensure customers agree to your terms and conditions and privacy policy.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false
},
"attributes": {
"checkbox": {
"type": "boolean",
"default": false
},
"text": {
"type": "string",
"required": false
}
},
"parent": [ "woocommerce/checkout-fields-block" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings';
const termsPageLink = TERMS_URL
? `<a href="${ TERMS_URL }">${ __(
'Terms and Conditions',
'woo-gutenberg-product-blocks'
) }</a>`
: __( 'Terms and Conditions', 'woo-gutenberg-product-blocks' );
const privacyPageLink = PRIVACY_URL
? `<a href="${ PRIVACY_URL }">${ __(
'Privacy Policy',
'woo-gutenberg-product-blocks'
) }</a>`
: __( '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
);

View File

@ -0,0 +1,183 @@
/**
* 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';
import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings';
import { ADMIN_URL } from '@woocommerce/settings';
import { Icon, external } from '@wordpress/icons';
/**
* 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 blockProps = useBlockProps();
const defaultText = checkbox
? termsCheckboxDefaultText
: termsConsentDefaultText;
const currentText = text || defaultText;
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody
title={ __(
'Display options',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
'Require checkbox',
'woo-gutenberg-products-block'
) }
checked={ checkbox }
onChange={ () =>
setAttributes( {
checkbox: ! checkbox,
} )
}
/>
</PanelBody>
</InspectorControls>
<div className="wc-block-checkout__terms">
{ checkbox ? (
<>
<CheckboxControl
id="terms-condition"
checked={ false }
/>
<RichText
value={ currentText }
onChange={ ( value ) =>
setAttributes( { text: value } )
}
/>
</>
) : (
<RichText
tagName="span"
value={ currentText }
onChange={ ( value ) =>
setAttributes( { text: value } )
}
/>
) }
</div>
{ /* Show this notice if a terms page or a privacy page is not setup. */ }
{ ( ! TERMS_URL || ! PRIVACY_URL ) && (
<Notice
className="wc-block-checkout__terms_notice"
status="warning"
isDismissible={ false }
actions={ [
! TERMS_URL && {
className: 'wc-block-checkout__terms_notice-button',
label: (
<>
{ __(
'Setup a Terms and Conditions page',
'woo-gutenberg-products-block'
) }
<Icon
icon={ external }
size={ 16 }
className="wc-block-checkout__terms_notice-button__icon"
/>
</>
),
onClick: () =>
window.open(
`${ ADMIN_URL }admin.php?page=wc-settings&tab=advanced`,
'_blank'
),
},
! PRIVACY_URL && {
className: 'wc-block-checkout__terms_notice-button',
label: (
<>
{ __(
'Setup a Privacy Policy page',
'woo-gutenberg-products-block'
) }
<Icon
size={ 16 }
icon={ external }
className="wc-block-checkout__terms_notice-button__icon"
/>
</>
),
onClick: () =>
window.open(
`${ ADMIN_URL }options-privacy.php`,
'_blank'
),
},
].filter( Boolean ) }
>
<p>
{ __(
"You don't seem to have a Terms and Conditions and/or a Privacy Policy pages setup.",
'woo-gutenberg-products-block'
) }
</p>
</Notice>
) }
{ /* Show this notice if we have both a terms and privacy pages, but they're not present in the text. */ }
{ TERMS_URL &&
PRIVACY_URL &&
! (
currentText.includes( TERMS_URL ) &&
currentText.includes( PRIVACY_URL )
) && (
<Notice
className="wc-block-checkout__terms_notice"
status="warning"
isDismissible={ false }
actions={
termsConsentDefaultText !== text
? [
{
label: __(
'Restore default text',
'woo-gutenberg-products-block'
),
onClick: () =>
setAttributes( { text: '' } ),
},
]
: []
}
>
<p>
{ __(
'Ensure you add links to your policy pages in this section.',
'woo-gutenberg-products-block'
) }
</p>
</Notice>
) }
</div>
);
};
export const Save = (): JSX.Element => {
return <div { ...useBlockProps.save() } />;
};

View File

@ -0,0 +1,32 @@
// Adjust padding and margins in the editor to improve selected block outlines.
.wc-block-checkout__terms {
margin: 20px 0;
padding-top: 4px;
padding-bottom: 4px;
display: flex;
align-items: flex-start;
.block-editor-rich-text__editable {
vertical-align: middle;
line-height: em(24px);
}
}
.wc-block-components-checkbox {
margin-right: $gap;
}
.wc-block-checkout__terms_notice .components-notice__action {
margin-left: 0;
}
.wc-block-checkout__terms_notice-button {
display: flex;
flex-direction: row;
align-items: center;
.wc-block-checkout__terms_notice-button__icon {
margin-left: $gap-smallest;
}
}

View File

@ -0,0 +1,104 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { useState, useEffect } from '@wordpress/element';
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
import { useCheckoutSubmit } from '@woocommerce/base-context/hooks';
import { withInstanceId } from '@wordpress/compose';
import type { ValidationData } from '@woocommerce/type-defs/contexts';
/**
* Internal dependencies
*/
import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
import './style.scss';
const FrontendBlock = ( {
text,
checkbox,
instanceId,
validation,
}: {
text: string;
checkbox: boolean;
instanceId: string;
validation: ValidationData;
} ): JSX.Element => {
const [ checked, setChecked ] = useState( false );
const { isDisabled } = useCheckoutSubmit();
const validationErrorId = 'terms-and-conditions-' + instanceId;
const {
getValidationError,
setValidationErrors,
clearValidationError,
} = validation;
const error = getValidationError( validationErrorId ) || {};
const hasError = error.message && ! error.hidden;
// Track validation errors for this input.
useEffect( () => {
if ( ! checkbox ) {
return;
}
if ( checked ) {
clearValidationError( validationErrorId );
} else {
setValidationErrors( {
[ validationErrorId ]: {
message: __(
'Please read and accept the terms and conditions.',
'woo-gutenberg-products-block'
),
hidden: true,
},
} );
}
return () => {
clearValidationError( validationErrorId );
};
}, [
checkbox,
checked,
validationErrorId,
clearValidationError,
setValidationErrors,
] );
return (
<div
className={ classnames( 'wc-block-checkout__terms', {
'wc-block-checkout__terms--disabled': isDisabled,
} ) }
>
{ checkbox ? (
<>
<CheckboxControl
id="terms-and-conditions"
checked={ checked }
onChange={ () => setChecked( ( value ) => ! value ) }
hasError={ hasError }
disabled={ isDisabled }
>
<span
dangerouslySetInnerHTML={ {
__html: text || termsCheckboxDefaultText,
} }
/>
</CheckboxControl>
</>
) : (
<span
dangerouslySetInnerHTML={ {
__html: text || termsConsentDefaultText,
} }
/>
) }
</div>
);
};
export default withInstanceId( FrontendBlock );

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Icon, asterisk } from '@woocommerce/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon srcElement={ asterisk } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,13 @@
.wc-block-checkout__terms {
margin: 1.5em 0;
text-align: justify;
textarea {
top: -5px;
position: relative;
}
&.wc-block-checkout__terms--disabled {
opacity: 0.6;
}
}

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/checkout-totals-block",
"version": "1.0.0",
"title": "Checkout Totals",
"description": "Column containing the checkout totals.",
"category": "woocommerce",
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false
},
"attributes": {
"checkbox": {
"type": "boolean",
"default": false
},
"text": {
"type": "string",
"required": false
}
},
"parent": [ "woocommerce/checkout" ],
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2
}

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
/**
* Internal dependencies
*/
import './style.scss';
import { useForcedLayout } from '../../use-forced-layout';
import { getAllowedBlocks } from '../../editor-utils';
export const Edit = ( { clientId }: { clientId: string } ): JSX.Element => {
const blockProps = useBlockProps();
const allowedBlocks = getAllowedBlocks( innerBlockAreas.CHECKOUT_TOTALS );
useForcedLayout( {
clientId,
template: allowedBlocks,
} );
return (
<Sidebar className="wc-block-checkout__sidebar">
<div { ...blockProps }>
<InnerBlocks
allowedBlocks={ allowedBlocks }
templateLock={ false }
/>
</div>
</Sidebar>
);
};
export const Save = (): JSX.Element => {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
};

View File

@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { Sidebar } from '@woocommerce/base-components/sidebar-layout';
/**
* Internal dependencies
*/
import './style.scss';
const FrontendBlock = ( {
children,
}: {
children: JSX.Element;
} ): JSX.Element => {
return (
<Sidebar className="wc-block-checkout__sidebar">{ children }</Sidebar>
);
};
export default FrontendBlock;

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { Icon, column } from '@wordpress/icons';
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import { Edit, Save } from './edit';
import metadata from './block.json';
registerFeaturePluginBlockType( metadata, {
icon: {
src: <Icon icon={ column } />,
foreground: '#874FB9',
},
edit: Edit,
save: Save,
} );

View File

@ -0,0 +1,23 @@
.wc-block-checkout__sidebar {
.wc-block-components-product-name {
display: block;
color: inherit;
flex-grow: 1;
// Required by IE11.
flex-basis: 0;
}
.wc-block-components-totals-taxes,
.wc-block-components-totals-footer-item {
margin: 0;
padding: 0;
}
}
.is-mobile,
.is-small,
.is-medium {
.wc-block-checkout__sidebar {
margin-bottom: $gap-large;
order: 0;
}
}

View File

@ -0,0 +1,15 @@
/**
* 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';

View File

@ -0,0 +1,135 @@
/**
* External dependencies
*/
import { lazy } from '@wordpress/element';
import { WC_BLOCKS_BUILD_URL } from '@woocommerce/block-settings';
import { registerCheckoutBlock } from '@woocommerce/blocks-checkout';
// 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 checkoutActionsMetadata from './checkout-actions-block/block.json';
import checkoutBillingAddressMetadata from './checkout-billing-address-block/block.json';
import checkoutContactInformationMetadata from './checkout-contact-information-block/block.json';
import checkoutExpressPaymentMetadata from './checkout-express-payment-block/block.json';
import checkoutFieldsMetadata from './checkout-fields-block/block.json';
import checkoutOrderNoteMetadata from './checkout-order-note-block/block.json';
import checkoutOrderSummaryMetadata from './checkout-order-summary-block/block.json';
import checkoutPaymentMetadata from './checkout-payment-block/block.json';
import checkoutShippingAddressMetadata from './checkout-shipping-address-block/block.json';
import checkoutShippingMethodsMetadata from './checkout-shipping-methods-block/block.json';
import checkoutTermsMetadata from './checkout-terms-block/block.json';
import checkoutTotalsMetadata from './checkout-totals-block/block.json';
// @todo When forcing all blocks at once, they will append based on the order they are registered. Introduce formal sorting param.
registerCheckoutBlock( {
metadata: checkoutFieldsMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/fields" */ './checkout-fields-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutExpressPaymentMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutContactInformationMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/contact-information" */ './checkout-contact-information-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutShippingAddressMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/shipping-address" */ './checkout-shipping-address-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutBillingAddressMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/billing-address" */ './checkout-billing-address-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutShippingMethodsMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/shipping-methods" */ './checkout-shipping-methods-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutPaymentMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/payment" */ './checkout-payment-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutOrderNoteMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/order-note" */ './checkout-order-note-block/block'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutTermsMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/terms" */ './checkout-terms-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutActionsMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/actions" */ './checkout-actions-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutTotalsMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/totals" */ './checkout-totals-block/frontend'
)
),
} );
registerCheckoutBlock( {
metadata: checkoutOrderSummaryMetadata,
component: lazy( () =>
import(
/* webpackChunkName: "checkout-blocks/order-summary" */ './checkout-order-summary-block/block'
)
),
} );

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
import Textarea from '@woocommerce/base-components/textarea';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import './style.scss';
const CheckoutOrderNotes = ( { disabled, onChange, placeholder, value } ) => {
const [ withOrderNotes, setWithOrderNotes ] = useState( false );
// Store order notes when the textarea is hidden. This allows us to recover
// text entered previously by the user when the checkbox is re-enabled
// while keeping the context clean if the checkbox is disabled.
const [ hiddenOrderNotesText, setHiddenOrderNotesText ] = useState( '' );
return (
<div className="wc-block-checkout__add-note">
<CheckboxControl
disabled={ disabled }
label={ __(
'Add a note to your order',
'woocommerce'
) }
checked={ withOrderNotes }
onChange={ ( isChecked ) => {
setWithOrderNotes( isChecked );
if ( isChecked ) {
// When re-enabling the checkbox, store in context the
// order notes value previously stored in the component
// state.
if ( value !== hiddenOrderNotesText ) {
onChange( hiddenOrderNotesText );
}
} else {
// When un-checking the checkbox, clear the order notes
// value in the context but store it in the component
// state.
onChange( '' );
setHiddenOrderNotesText( value );
}
} }
/>
{ withOrderNotes && (
<Textarea
disabled={ disabled }
onTextChange={ onChange }
placeholder={ placeholder }
value={ value }
/>
) }
</div>
);
};
Textarea.propTypes = {
onTextChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
value: PropTypes.string,
};
export default CheckoutOrderNotes;

View File

@ -0,0 +1,22 @@
.wc-block-checkout__add-note {
margin: em($gap-large) 0;
}
.is-mobile,
.is-small,
.is-medium {
.wc-block-checkout__add-note {
@include with-translucent-border(1px 0);
margin-bottom: em($gap);
margin-top: em($gap);
padding: em($gap) 0;
}
}
.wc-block-checkout__add-note .wc-block-components-textarea {
margin-top: $gap;
}
.wc-block-checkout__order-notes.wc-block-components-checkout-step {
padding-left: 0;
}

View File

@ -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 (
<ValidatedTextInput
id={ id }
type="tel"
autoComplete="tel"
required={ isRequired }
label={
isRequired
? __( 'Phone', 'woo-gutenberg-products-block' )
: __( 'Phone (optional)', 'woo-gutenberg-products-block' )
}
value={ value }
onChange={ onChange }
/>
);
};
export default PhoneNumber;

View File

@ -0,0 +1,68 @@
.wp-block-woocommerce-checkout {
.wc-block-components-sidebar-layout {
display: block;
}
.block-editor-block-list__layout {
display: flex;
flex-flow: row wrap;
align-items: flex-start;
.wc-block-checkout__additional_fields {
padding: 0;
}
}
.wc-block-components-main,
.wc-block-components-sidebar,
.block-editor-block-list__layout {
> :first-child {
margin-top: 0;
}
}
.wp-block-woocommerce-checkout-totals-block,
.wp-block-woocommerce-checkout-fields-block {
.block-editor-block-list__layout {
display: block;
}
}
}
body.wc-lock-selected-block--move {
.block-editor-block-mover__move-button-container,
.block-editor-block-mover {
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;
}
}
}
.wc-block-checkout__controls-text {
color: #999;
font-style: italic;
}
.components-base-control--nested {
padding-left: 52px;
margin-top: -12px;
}
.wc-block-checkout__page-notice {
margin: 0;
}
.components-panel__body-title .components-button {
opacity: 1;
}
.wp-block-woocommerce-checkout.is-editor-preview {
max-height: 1000px;
overflow: hidden;
}

Some files were not shown because too many files have changed in this diff Show More