Create Cart i2 block (https://github.com/woocommerce/woocommerce-blocks/pull/4718)
This commit is contained in:
parent
37920b118d
commit
f44784ffca
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
const blockAttributes = {
|
||||
isPreview: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
save: false,
|
||||
},
|
||||
isShippingCalculatorEnabled: {
|
||||
type: 'boolean',
|
||||
default: getSetting( 'isShippingCalculatorEnabled', true ),
|
||||
},
|
||||
checkoutPageId: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
},
|
||||
hasDarkControls: {
|
||||
type: 'boolean',
|
||||
default: getSetting( 'hasDarkEditorStyleSupport', false ),
|
||||
},
|
||||
showRateAfterTaxName: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default blockAttributes;
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect, RawHTML } from '@wordpress/element';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { ValidationContextProvider } from '@woocommerce/base-context';
|
||||
import {
|
||||
dispatchEvent,
|
||||
translateJQueryEventToNative,
|
||||
} from '@woocommerce/base-utils';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FullCart from './full-cart';
|
||||
|
||||
const EmptyCart = ( { content } ) => {
|
||||
useEffect( () => {
|
||||
dispatchEvent( 'wc-blocks_render_blocks_frontend', {
|
||||
element: document.body.querySelector(
|
||||
'.wp-block-woocommerce-cart'
|
||||
),
|
||||
} );
|
||||
}, [] );
|
||||
return <RawHTML>{ content }</RawHTML>;
|
||||
};
|
||||
|
||||
const Block = ( { emptyCart, attributes, scrollToTop } ) => {
|
||||
const { cartItems, cartIsLoading } = useStoreCart();
|
||||
|
||||
useEffect( () => {
|
||||
const invalidateCartData = () => {
|
||||
dispatch( storeKey ).invalidateResolutionForStore();
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
// Make it so we can read jQuery events triggered by WC Core elements.
|
||||
const removeJQueryAddedToCartEvent = translateJQueryEventToNative(
|
||||
'added_to_cart',
|
||||
'wc-blocks_added_to_cart'
|
||||
);
|
||||
const removeJQueryRemovedFromCartEvent = translateJQueryEventToNative(
|
||||
'removed_from_cart',
|
||||
'wc-blocks_removed_from_cart'
|
||||
);
|
||||
|
||||
document.body.addEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
invalidateCartData
|
||||
);
|
||||
document.body.addEventListener(
|
||||
'wc-blocks_removed_from_cart',
|
||||
invalidateCartData
|
||||
);
|
||||
|
||||
return () => {
|
||||
removeJQueryAddedToCartEvent();
|
||||
removeJQueryRemovedFromCartEvent();
|
||||
|
||||
document.body.removeEventListener(
|
||||
'wc-blocks_added_to_cart',
|
||||
invalidateCartData
|
||||
);
|
||||
document.body.removeEventListener(
|
||||
'wc-blocks_removed_from_cart',
|
||||
invalidateCartData
|
||||
);
|
||||
};
|
||||
}, [ scrollToTop ] );
|
||||
|
||||
return (
|
||||
<>
|
||||
{ ! cartIsLoading && cartItems.length === 0 ? (
|
||||
<EmptyCart content={ emptyCart } />
|
||||
) : (
|
||||
<LoadingMask showSpinner={ true } isLoading={ cartIsLoading }>
|
||||
<ValidationContextProvider>
|
||||
<FullCart attributes={ attributes } />
|
||||
</ValidationContextProvider>
|
||||
</LoadingMask>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withScrollToTop( Block );
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { PaymentMethodIcons } from '@woocommerce/base-components/cart-checkout';
|
||||
import Button from '@woocommerce/base-components/button';
|
||||
import { CHECKOUT_URL } from '@woocommerce/block-settings';
|
||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||
import { usePaymentMethods } from '@woocommerce/base-context/hooks';
|
||||
import { usePositionRelativeToViewport } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import type PaymentMethodConfig from '../../../../blocks-registry/payment-methods/payment-method-config';
|
||||
|
||||
const getIconsFromPaymentMethods = (
|
||||
paymentMethods: PaymentMethodConfig[]
|
||||
) => {
|
||||
return Object.values( paymentMethods ).reduce( ( acc, paymentMethod ) => {
|
||||
if ( paymentMethod.icons !== null ) {
|
||||
acc = acc.concat( paymentMethod.icons );
|
||||
}
|
||||
return acc;
|
||||
}, [] );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkout button rendered in the full cart page.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.link What the button is linked to.
|
||||
*/
|
||||
const CheckoutButton = ( { link }: { link: string } ): JSX.Element => {
|
||||
const { isCalculating } = useCheckoutContext();
|
||||
const [
|
||||
positionReferenceElement,
|
||||
positionRelativeToViewport,
|
||||
] = usePositionRelativeToViewport();
|
||||
const [ showSpinner, setShowSpinner ] = useState( false );
|
||||
const { paymentMethods } = usePaymentMethods();
|
||||
|
||||
useEffect( () => {
|
||||
// Add a listener to remove the spinner on the checkout button, so the saved page snapshot does not
|
||||
// contain the spinner class. See https://archive.is/lOEW0 for why this is needed for Safari.
|
||||
|
||||
if (
|
||||
typeof global.addEventListener !== 'function' ||
|
||||
typeof global.removeEventListener !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hideSpinner = () => {
|
||||
setShowSpinner( false );
|
||||
};
|
||||
|
||||
global.addEventListener( 'pageshow', hideSpinner );
|
||||
|
||||
return () => {
|
||||
global.removeEventListener( 'pageshow', hideSpinner );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const submitContainerContents = (
|
||||
<>
|
||||
<Button
|
||||
className="wc-block-cart__submit-button"
|
||||
href={ link || CHECKOUT_URL }
|
||||
disabled={ isCalculating }
|
||||
onClick={ () => setShowSpinner( true ) }
|
||||
showSpinner={ showSpinner }
|
||||
>
|
||||
{ __( 'Proceed to Checkout', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
<PaymentMethodIcons
|
||||
icons={ getIconsFromPaymentMethods( paymentMethods ) }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="wc-block-cart__submit">
|
||||
{ positionReferenceElement }
|
||||
{ /* The non-sticky container must always be visible because it gives height to its parent, which is required to calculate when it becomes visible in the viewport. */ }
|
||||
<div className="wc-block-cart__submit-container">
|
||||
{ submitContainerContents }
|
||||
</div>
|
||||
{ /* If the positionReferenceElement is below the viewport, display the sticky container. */ }
|
||||
{ positionRelativeToViewport === 'below' && (
|
||||
<div className="wc-block-cart__submit-container wc-block-cart__submit-container--sticky">
|
||||
{ submitContainerContents }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckoutButton;
|
|
@ -0,0 +1,56 @@
|
|||
.wc-block-cart__submit {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wc-block-cart__submit-container {
|
||||
padding-bottom: $gap;
|
||||
}
|
||||
|
||||
.wc-block-cart__submit-button {
|
||||
width: 100%;
|
||||
margin: 0 0 $gap;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-mobile,
|
||||
.is-small,
|
||||
.is-medium {
|
||||
.wc-block-cart__submit-container:not(.wc-block-cart__submit-container--sticky) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint( ">782px" ) {
|
||||
.wc-block-cart__submit-container--sticky {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint( "<782px" ) {
|
||||
.wc-block-cart__submit-container--sticky {
|
||||
background: $white;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: $gap;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
|
||||
&::before {
|
||||
box-shadow: 0 -10px 20px 10px currentColor;
|
||||
color: transparentize($gray-400, 0.5);
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { CartCheckoutFeedbackPrompt } from '@woocommerce/editor-components/feedback-prompt';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import {
|
||||
Disabled,
|
||||
PanelBody,
|
||||
ToggleControl,
|
||||
Notice,
|
||||
} from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CartCheckoutCompatibilityNotice } from '@woocommerce/editor-components/compatibility-notices';
|
||||
import ViewSwitcher from '@woocommerce/editor-components/view-switcher';
|
||||
import PageSelector from '@woocommerce/editor-components/page-selector';
|
||||
import { CART_PAGE_ID } from '@woocommerce/block-settings';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import {
|
||||
EditorProvider,
|
||||
useEditorContext,
|
||||
CartProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
import { createInterpolateElement, useRef } from '@wordpress/element';
|
||||
import { getAdminLink, getSetting } from '@woocommerce/settings';
|
||||
import { previewCart } from '@woocommerce/resource-previews';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block.js';
|
||||
import EmptyCartEdit from './empty-cart-edit';
|
||||
import './editor.scss';
|
||||
|
||||
const BlockSettings = ( { attributes, setAttributes } ) => {
|
||||
const {
|
||||
isShippingCalculatorEnabled,
|
||||
checkoutPageId,
|
||||
hasDarkControls,
|
||||
showRateAfterTaxName,
|
||||
} = attributes;
|
||||
const { currentPostId } = useEditorContext();
|
||||
const { current: savedCheckoutPageId } = useRef( checkoutPageId );
|
||||
return (
|
||||
<InspectorControls>
|
||||
{ currentPostId !== CART_PAGE_ID && (
|
||||
<Notice
|
||||
className="wc-block-cart__page-notice"
|
||||
isDismissible={ false }
|
||||
status="warning"
|
||||
>
|
||||
{ createInterpolateElement(
|
||||
__(
|
||||
'If you would like to use this block as your default cart 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>
|
||||
) }
|
||||
{ getSetting( 'shippingEnabled', true ) && (
|
||||
<PanelBody
|
||||
title={ __(
|
||||
'Shipping rates',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Shipping calculator',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
help={ __(
|
||||
'Allow customers to estimate shipping by entering their address.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ isShippingCalculatorEnabled }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
isShippingCalculatorEnabled: ! isShippingCalculatorEnabled,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
) }
|
||||
{ getSetting( 'taxesEnabled' ) &&
|
||||
getSetting( 'displayItemizedTaxes', false ) &&
|
||||
! getSetting( 'displayCartPricesIncludingTax', false ) && (
|
||||
<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={ showRateAfterTaxName }
|
||||
onChange={ () =>
|
||||
setAttributes( {
|
||||
showRateAfterTaxName: ! showRateAfterTaxName,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
) }
|
||||
{ ! (
|
||||
currentPostId === CART_PAGE_ID && savedCheckoutPageId === 0
|
||||
) && (
|
||||
<PageSelector
|
||||
pageId={ checkoutPageId }
|
||||
setPageId={ ( id ) =>
|
||||
setAttributes( { checkoutPageId: id } )
|
||||
}
|
||||
labels={ {
|
||||
title: __(
|
||||
'Proceed to Checkout button',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
default: __(
|
||||
'WooCommerce Checkout Page',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of "Cart Block".
|
||||
*
|
||||
* Note: We need to always render `<InnerBlocks>` in the editor. Otherwise,
|
||||
* if the user saves the page without having triggered the 'Empty Cart'
|
||||
* view, inner blocks would not be saved and they wouldn't be visible
|
||||
* in the frontend.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.className CSS class used.
|
||||
* @param {Object} props.attributes Attributes available.
|
||||
* @param {function(any):any} props.setAttributes Setter for attributes.
|
||||
*/
|
||||
const CartEditor = ( { className, attributes, setAttributes } ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames( className, 'wp-block-woocommerce-cart', {
|
||||
'is-editor-preview': attributes.isPreview,
|
||||
} ) }
|
||||
>
|
||||
<ViewSwitcher
|
||||
label={ __( 'Edit', 'woo-gutenberg-products-block' ) }
|
||||
views={ [
|
||||
{
|
||||
value: 'full',
|
||||
name: __( 'Full Cart', 'woo-gutenberg-products-block' ),
|
||||
},
|
||||
{
|
||||
value: 'empty',
|
||||
name: __(
|
||||
'Empty Cart',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
},
|
||||
] }
|
||||
defaultView={ 'full' }
|
||||
render={ ( currentView ) => (
|
||||
<BlockErrorBoundary
|
||||
header={ __(
|
||||
'Cart Block Error',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
text={ __(
|
||||
'There was an error whilst rendering the cart block. If this problem continues, try re-creating the block.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
showErrorMessage={ true }
|
||||
errorMessagePrefix={ __(
|
||||
'Error message:',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
{ currentView === 'full' && (
|
||||
<>
|
||||
<EditorProvider previewData={ { previewCart } }>
|
||||
<BlockSettings
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
/>
|
||||
<Disabled>
|
||||
<CartProvider>
|
||||
<Block attributes={ attributes } />
|
||||
</CartProvider>
|
||||
</Disabled>
|
||||
</EditorProvider>
|
||||
<EmptyCartEdit hidden={ true } />
|
||||
</>
|
||||
) }
|
||||
{ currentView === 'empty' && <EmptyCartEdit /> }
|
||||
</BlockErrorBoundary>
|
||||
) }
|
||||
/>
|
||||
<CartCheckoutCompatibilityNotice blockName="cart" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CartEditor.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CartEditor;
|
|
@ -0,0 +1,8 @@
|
|||
.wc-block-cart__page-notice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-cart.is-editor-preview {
|
||||
max-height: 1000px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzgiIGhlaWdodD0iMzgiIHZpZXdCb3g9IjAgMCAzOCAzOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDBDOC41MDQwMyAwIDAgOC41MDQwMyAwIDE5QzAgMjkuNDk2IDguNTA0MDMgMzggMTkgMzhDMjkuNDk2IDM4IDM4IDI5LjQ5NiAzOCAxOUMzOCA4LjUwNDAzIDI5LjQ5NiAwIDE5IDBaTTI1LjEyOSAxMi44NzFDMjYuNDg1MSAxMi44NzEgMjcuNTgwNiAxMy45NjY1IDI3LjU4MDYgMTUuMzIyNkMyNy41ODA2IDE2LjY3ODYgMjYuNDg1MSAxNy43NzQyIDI1LjEyOSAxNy43NzQyQzIzLjc3MyAxNy43NzQyIDIyLjY3NzQgMTYuNjc4NiAyMi42Nzc0IDE1LjMyMjZDMjIuNjc3NCAxMy45NjY1IDIzLjc3MyAxMi44NzEgMjUuMTI5IDEyLjg3MVpNMTEuNjQ1MiAzMS4yNTgxQzkuNjE0OTIgMzEuMjU4MSA3Ljk2Nzc0IDI5LjY0OTIgNy45Njc3NCAyNy42NTczQzcuOTY3NzQgMjYuMTI1IDEwLjE1MTIgMjMuMDI5OCAxMS4xNTQ4IDIxLjY5NjhDMTEuNCAyMS4zNjczIDExLjg5MDMgMjEuMzY3MyAxMi4xMzU1IDIxLjY5NjhDMTMuMTM5MSAyMy4wMjk4IDE1LjMyMjYgMjYuMTI1IDE1LjMyMjYgMjcuNjU3M0MxNS4zMjI2IDI5LjY0OTIgMTMuNjc1NCAzMS4yNTgxIDExLjY0NTIgMzEuMjU4MVpNMTIuODcxIDE3Ljc3NDJDMTEuNTE0OSAxNy43NzQyIDEwLjQxOTQgMTYuNjc4NiAxMC40MTk0IDE1LjMyMjZDMTAuNDE5NCAxMy45NjY1IDExLjUxNDkgMTIuODcxIDEyLjg3MSAxMi44NzFDMTQuMjI3IDEyLjg3MSAxNS4zMjI2IDEzLjk2NjUgMTUuMzIyNiAxNS4zMjI2QzE1LjMyMjYgMTYuNjc4NiAxNC4yMjcgMTcuNzc0MiAxMi44NzEgMTcuNzc0MlpNMjUuOTEwNSAyOS41ODc5QzI0LjE5NDQgMjcuNTM0NyAyMS42NzM4IDI2LjM1NDggMTkgMjYuMzU0OEMxNy4zNzU4IDI2LjM1NDggMTcuMzc1OCAyMy45MDMyIDE5IDIzLjkwMzJDMjIuNDAxNiAyMy45MDMyIDI1LjYxMTcgMjUuNDA0OCAyNy43ODc1IDI4LjAyNUMyOC44NDQ4IDI5LjI4MTUgMjYuOTI5NCAzMC44MjE0IDI1LjkxMDUgMjkuNTg3OVoiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgo=';
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { InnerBlocks } from '@wordpress/block-editor';
|
||||
import { SHOP_URL } from '@woocommerce/block-settings';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import iconDataUri from './icon-data-uri.js';
|
||||
import './style.scss';
|
||||
|
||||
const templateItemBrowseStore = SHOP_URL
|
||||
? [
|
||||
'core/paragraph',
|
||||
{
|
||||
align: 'center',
|
||||
content: sprintf(
|
||||
/* translators: %s is the link to the store product directory. */
|
||||
__(
|
||||
'<a href="%s">Browse store</a>.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
SHOP_URL
|
||||
),
|
||||
dropCap: false,
|
||||
},
|
||||
]
|
||||
: null;
|
||||
|
||||
const templateItems = [
|
||||
[
|
||||
'core/image',
|
||||
{
|
||||
align: 'center',
|
||||
url: iconDataUri,
|
||||
sizeSlug: 'small',
|
||||
},
|
||||
],
|
||||
[
|
||||
'core/heading',
|
||||
{
|
||||
textAlign: 'center',
|
||||
content: __(
|
||||
'Your cart is currently empty!',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
level: 2,
|
||||
className: 'wc-block-cart__empty-cart__title',
|
||||
},
|
||||
],
|
||||
templateItemBrowseStore,
|
||||
[
|
||||
'core/separator',
|
||||
{
|
||||
className: 'is-style-dots',
|
||||
},
|
||||
],
|
||||
[
|
||||
'core/heading',
|
||||
{
|
||||
textAlign: 'center',
|
||||
content: __( 'New in store', 'woo-gutenberg-products-block' ),
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
[
|
||||
'woocommerce/product-new',
|
||||
{
|
||||
columns: 3,
|
||||
rows: 1,
|
||||
},
|
||||
],
|
||||
].filter( Boolean );
|
||||
|
||||
/**
|
||||
* Component to handle edit mode for the Cart block when cart is empty.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {boolean} props.hidden Whether this component is hidden or not.
|
||||
*/
|
||||
const EmptyCartEdit = ( { hidden = false } ) => {
|
||||
return (
|
||||
<div hidden={ hidden }>
|
||||
<InnerBlocks
|
||||
templateInsertUpdatesSelection={ false }
|
||||
template={ templateItems }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
EmptyCartEdit.propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default EmptyCartEdit;
|
|
@ -0,0 +1,4 @@
|
|||
.wc-block-cart__empty-cart__title,
|
||||
.editor-styles-wrapper .wc-block-cart__empty-cart__title {
|
||||
font-size: inherit;
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
withStoreCartApiHydration,
|
||||
withRestApiHydration,
|
||||
} from '@woocommerce/block-hocs';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
StoreNoticesProvider,
|
||||
StoreSnackbarNoticesProvider,
|
||||
CartProvider,
|
||||
} from '@woocommerce/base-context/providers';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import {
|
||||
renderFrontend,
|
||||
getValidBlockAttributes,
|
||||
} from '@woocommerce/base-utils';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block.js';
|
||||
import blockAttributes from './attributes';
|
||||
|
||||
const reloadPage = () => void window.location.reload( true );
|
||||
/**
|
||||
* Wrapper component to supply API data and show empty cart view as needed.
|
||||
*
|
||||
* @param {*} props
|
||||
*/
|
||||
const CartFrontend = ( props ) => {
|
||||
return (
|
||||
<StoreSnackbarNoticesProvider context="wc/cart">
|
||||
<StoreNoticesProvider context="wc/cart">
|
||||
<SlotFillProvider>
|
||||
<CartProvider>
|
||||
<Block { ...props } />
|
||||
</CartProvider>
|
||||
</SlotFillProvider>
|
||||
</StoreNoticesProvider>
|
||||
</StoreSnackbarNoticesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const getProps = ( el ) => {
|
||||
return {
|
||||
emptyCart: el.innerHTML,
|
||||
attributes: getValidBlockAttributes( blockAttributes, el.dataset ),
|
||||
};
|
||||
};
|
||||
|
||||
const getErrorBoundaryProps = () => {
|
||||
return {
|
||||
header: __( 'Something went wrong…', 'woo-gutenberg-products-block' ),
|
||||
text: __(
|
||||
'The cart has encountered an unexpected error. If the error persists, please get in touch with us for help.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
showErrorMessage: CURRENT_USER_IS_ADMIN,
|
||||
button: (
|
||||
<button className="wc-block-button" onClick={ reloadPage }>
|
||||
{ __( 'Reload the page', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
renderFrontend( {
|
||||
selector: '.wp-block-woocommerce-cart-i2',
|
||||
Block: withStoreCartApiHydration( withRestApiHydration( CartFrontend ) ),
|
||||
getProps,
|
||||
getErrorBoundaryProps,
|
||||
} );
|
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import QuantitySelector from '@woocommerce/base-components/quantity-selector';
|
||||
import ProductPrice from '@woocommerce/base-components/product-price';
|
||||
import ProductName from '@woocommerce/base-components/product-name';
|
||||
import {
|
||||
useStoreCartItemQuantity,
|
||||
useStoreEvents,
|
||||
useStoreCart,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import {
|
||||
ProductBackorderBadge,
|
||||
ProductImage,
|
||||
ProductLowStockBadge,
|
||||
ProductMetadata,
|
||||
ProductSaleBadge,
|
||||
} from '@woocommerce/base-components/cart-checkout';
|
||||
import {
|
||||
getCurrencyFromPriceResponse,
|
||||
Currency,
|
||||
} from '@woocommerce/price-format';
|
||||
import {
|
||||
__experimentalApplyCheckoutFilter,
|
||||
mustContain,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import Dinero from 'dinero.js';
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import type { CartItem } from '@woocommerce/type-defs/cart';
|
||||
import { objectHasProp } from '@woocommerce/types';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Convert a Dinero object with precision to store currency minor unit.
|
||||
*
|
||||
* @param {Dinero} priceObject Price object to convert.
|
||||
* @param {Object} currency Currency data.
|
||||
* @return {number} Amount with new minor unit precision.
|
||||
*/
|
||||
const getAmountFromRawPrice = (
|
||||
priceObject: Dinero.Dinero,
|
||||
currency: Currency
|
||||
) => {
|
||||
return priceObject.convertPrecision( currency.minorUnit ).getAmount();
|
||||
};
|
||||
|
||||
const productPriceValidation = ( value ) => mustContain( value, '<price/>' );
|
||||
|
||||
/**
|
||||
* Cart line item table row component.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {CartItem|Object} props.lineItem
|
||||
*/
|
||||
const CartLineItemRow = ( {
|
||||
lineItem,
|
||||
}: {
|
||||
lineItem: CartItem | Record< string, never >;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
name: initialName = '',
|
||||
catalog_visibility: catalogVisibility = 'visible',
|
||||
short_description: shortDescription = '',
|
||||
description: fullDescription = '',
|
||||
low_stock_remaining: lowStockRemaining = null,
|
||||
show_backorder_badge: showBackorderBadge = false,
|
||||
quantity_limit: quantityLimit = 99,
|
||||
permalink = '',
|
||||
images = [],
|
||||
variation = [],
|
||||
item_data: itemData = [],
|
||||
prices = {
|
||||
currency_code: 'USD',
|
||||
currency_minor_unit: 2,
|
||||
currency_symbol: '$',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
price: '0',
|
||||
regular_price: '0',
|
||||
sale_price: '0',
|
||||
price_range: null,
|
||||
raw_prices: {
|
||||
precision: 6,
|
||||
price: '0',
|
||||
regular_price: '0',
|
||||
sale_price: '0',
|
||||
},
|
||||
},
|
||||
totals = {
|
||||
currency_code: 'USD',
|
||||
currency_minor_unit: 2,
|
||||
currency_symbol: '$',
|
||||
currency_prefix: '$',
|
||||
currency_suffix: '',
|
||||
currency_decimal_separator: '.',
|
||||
currency_thousand_separator: ',',
|
||||
line_subtotal: '0',
|
||||
line_subtotal_tax: '0',
|
||||
},
|
||||
extensions,
|
||||
} = lineItem;
|
||||
|
||||
const {
|
||||
quantity,
|
||||
setItemQuantity,
|
||||
removeItem,
|
||||
isPendingDelete,
|
||||
} = useStoreCartItemQuantity( lineItem );
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
|
||||
// Prepare props to pass to the __experimentalApplyCheckoutFilter filter.
|
||||
// We need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { receiveCart, ...cart } = useStoreCart();
|
||||
const arg = useMemo(
|
||||
() => ( {
|
||||
context: 'cart',
|
||||
cartItem: lineItem,
|
||||
cart,
|
||||
} ),
|
||||
[ lineItem, cart ]
|
||||
);
|
||||
const priceCurrency = getCurrencyFromPriceResponse( prices );
|
||||
const name = __experimentalApplyCheckoutFilter( {
|
||||
filterName: 'itemName',
|
||||
defaultValue: initialName,
|
||||
extensions,
|
||||
arg,
|
||||
} );
|
||||
|
||||
const regularAmountSingle = Dinero( {
|
||||
amount: parseInt( prices.raw_prices.regular_price, 10 ),
|
||||
precision: prices.raw_prices.precision,
|
||||
} );
|
||||
const purchaseAmountSingle = Dinero( {
|
||||
amount: parseInt( prices.raw_prices.price, 10 ),
|
||||
precision: prices.raw_prices.precision,
|
||||
} );
|
||||
const saleAmountSingle = regularAmountSingle.subtract(
|
||||
purchaseAmountSingle
|
||||
);
|
||||
const saleAmount = saleAmountSingle.multiply( quantity );
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( totals );
|
||||
let lineSubtotal = parseInt( totals.line_subtotal, 10 );
|
||||
if ( getSetting( 'displayCartPricesIncludingTax', false ) ) {
|
||||
lineSubtotal += parseInt( totals.line_subtotal_tax, 10 );
|
||||
}
|
||||
const subtotalPrice = Dinero( {
|
||||
amount: lineSubtotal,
|
||||
precision: totalsCurrency.minorUnit,
|
||||
} );
|
||||
|
||||
const firstImage = images.length ? images[ 0 ] : {};
|
||||
const isProductHiddenFromCatalog =
|
||||
catalogVisibility === 'hidden' || catalogVisibility === 'search';
|
||||
|
||||
// Allow extensions to filter how the price is displayed. Ie: prepending or appending some values.
|
||||
|
||||
const productPriceFormat = __experimentalApplyCheckoutFilter( {
|
||||
filterName: 'cartItemPrice',
|
||||
defaultValue: '<price/>',
|
||||
extensions,
|
||||
arg,
|
||||
validation: productPriceValidation,
|
||||
} );
|
||||
|
||||
const subtotalPriceFormat = __experimentalApplyCheckoutFilter( {
|
||||
filterName: 'subtotalPriceFormat',
|
||||
defaultValue: '<price/>',
|
||||
extensions,
|
||||
arg,
|
||||
validation: productPriceValidation,
|
||||
} );
|
||||
|
||||
const saleBadgePriceFormat = __experimentalApplyCheckoutFilter( {
|
||||
filterName: 'saleBadgePriceFormat',
|
||||
defaultValue: '<price/>',
|
||||
extensions,
|
||||
arg,
|
||||
validation: productPriceValidation,
|
||||
} );
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={ classnames( 'wc-block-cart-items__row', {
|
||||
'is-disabled': isPendingDelete,
|
||||
} ) }
|
||||
>
|
||||
{ /* If the image has no alt text, this link is unnecessary and can be hidden. */ }
|
||||
<td
|
||||
className="wc-block-cart-item__image"
|
||||
aria-hidden={
|
||||
! objectHasProp( firstImage, 'alt' ) || ! firstImage.alt
|
||||
}
|
||||
>
|
||||
{ /* We don't need to make it focusable, because product name has the same link. */ }
|
||||
{ isProductHiddenFromCatalog ? (
|
||||
<ProductImage image={ firstImage } />
|
||||
) : (
|
||||
<a href={ permalink } tabIndex={ -1 }>
|
||||
<ProductImage image={ firstImage } />
|
||||
</a>
|
||||
) }
|
||||
</td>
|
||||
<td className="wc-block-cart-item__product">
|
||||
<ProductName
|
||||
disabled={ isPendingDelete || isProductHiddenFromCatalog }
|
||||
name={ name }
|
||||
permalink={ permalink }
|
||||
/>
|
||||
{ showBackorderBadge ? (
|
||||
<ProductBackorderBadge />
|
||||
) : (
|
||||
!! lowStockRemaining && (
|
||||
<ProductLowStockBadge
|
||||
lowStockRemaining={ lowStockRemaining }
|
||||
/>
|
||||
)
|
||||
) }
|
||||
|
||||
<div className="wc-block-cart-item__prices">
|
||||
<ProductPrice
|
||||
currency={ priceCurrency }
|
||||
regularPrice={ getAmountFromRawPrice(
|
||||
regularAmountSingle,
|
||||
priceCurrency
|
||||
) }
|
||||
price={ getAmountFromRawPrice(
|
||||
purchaseAmountSingle,
|
||||
priceCurrency
|
||||
) }
|
||||
format={ subtotalPriceFormat }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProductSaleBadge
|
||||
currency={ priceCurrency }
|
||||
saleAmount={ getAmountFromRawPrice(
|
||||
saleAmountSingle,
|
||||
priceCurrency
|
||||
) }
|
||||
format={ saleBadgePriceFormat }
|
||||
/>
|
||||
|
||||
<ProductMetadata
|
||||
shortDescription={ shortDescription }
|
||||
fullDescription={ fullDescription }
|
||||
itemData={ itemData }
|
||||
variation={ variation }
|
||||
/>
|
||||
|
||||
<div className="wc-block-cart-item__quantity">
|
||||
<QuantitySelector
|
||||
disabled={ isPendingDelete }
|
||||
quantity={ quantity }
|
||||
maximum={ quantityLimit }
|
||||
onChange={ ( newQuantity ) => {
|
||||
setItemQuantity( newQuantity );
|
||||
dispatchStoreEvent( 'cart-set-item-quantity', {
|
||||
product: lineItem,
|
||||
quantity: newQuantity,
|
||||
} );
|
||||
} }
|
||||
itemName={ name }
|
||||
/>
|
||||
<button
|
||||
className="wc-block-cart-item__remove-link"
|
||||
onClick={ () => {
|
||||
removeItem();
|
||||
dispatchStoreEvent( 'cart-remove-item', {
|
||||
product: lineItem,
|
||||
quantity,
|
||||
} );
|
||||
} }
|
||||
disabled={ isPendingDelete }
|
||||
>
|
||||
{ __( 'Remove item', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="wc-block-cart-item__total">
|
||||
<div className="wc-block-cart-item__total-price-and-sale-badge-wrapper">
|
||||
<ProductPrice
|
||||
currency={ totalsCurrency }
|
||||
format={ productPriceFormat }
|
||||
price={ subtotalPrice.getAmount() }
|
||||
/>
|
||||
|
||||
{ quantity > 1 && (
|
||||
<ProductSaleBadge
|
||||
currency={ priceCurrency }
|
||||
saleAmount={ getAmountFromRawPrice(
|
||||
saleAmount,
|
||||
priceCurrency
|
||||
) }
|
||||
format={ saleBadgePriceFormat }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartLineItemRow;
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { CartResponseItem } from '@woocommerce/type-defs/cart-response';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CartLineItemRow from './cart-line-item-row';
|
||||
|
||||
const placeholderRows = [ ...Array( 3 ) ].map( ( _x, i ) => (
|
||||
<CartLineItemRow lineItem={ {} } key={ i } />
|
||||
) );
|
||||
|
||||
interface CartLineItemsTableProps {
|
||||
lineItems: CartResponseItem[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const CartLineItemsTable = ( {
|
||||
lineItems = [],
|
||||
isLoading = false,
|
||||
}: CartLineItemsTableProps ): JSX.Element => {
|
||||
const products = isLoading
|
||||
? placeholderRows
|
||||
: lineItems.map( ( lineItem ) => {
|
||||
return (
|
||||
<CartLineItemRow
|
||||
key={ lineItem.key }
|
||||
lineItem={ lineItem }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
|
||||
return (
|
||||
<table className="wc-block-cart-items">
|
||||
<thead>
|
||||
<tr className="wc-block-cart-items__header">
|
||||
<th className="wc-block-cart-items__header-image">
|
||||
<span>
|
||||
{ __( 'Product', 'woo-gutenberg-products-block' ) }
|
||||
</span>
|
||||
</th>
|
||||
<th className="wc-block-cart-items__header-product">
|
||||
<span>
|
||||
{ __( 'Details', 'woo-gutenberg-products-block' ) }
|
||||
</span>
|
||||
</th>
|
||||
<th className="wc-block-cart-items__header-total">
|
||||
<span>
|
||||
{ __( 'Total', 'woo-gutenberg-products-block' ) }
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{ products }</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartLineItemsTable;
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
|
||||
const CartLineItemsTitle = ( {
|
||||
itemCount = 1,
|
||||
}: {
|
||||
itemCount: number;
|
||||
} ): JSX.Element => {
|
||||
return (
|
||||
<Title headingLevel="2">
|
||||
{ sprintf(
|
||||
/* translators: %d is the count of items in the cart. */
|
||||
_n(
|
||||
'Your cart (%d item)',
|
||||
'Your cart (%d items)',
|
||||
itemCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
itemCount
|
||||
) }
|
||||
</Title>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartLineItemsTitle;
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
TotalsCoupon,
|
||||
TotalsDiscount,
|
||||
TotalsFooterItem,
|
||||
TotalsShipping,
|
||||
} from '@woocommerce/base-components/cart-checkout';
|
||||
import {
|
||||
Subtotal,
|
||||
TotalsFees,
|
||||
TotalsTaxes,
|
||||
TotalsWrapper,
|
||||
ExperimentalOrderMeta,
|
||||
ExperimentalDiscountsMeta,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import {
|
||||
useStoreCartCoupons,
|
||||
useStoreCart,
|
||||
useStoreNotices,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarLayout,
|
||||
Main,
|
||||
} from '@woocommerce/base-components/sidebar-layout';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CheckoutButton from '../checkout-button';
|
||||
import CartLineItemsTitle from './cart-line-items-title';
|
||||
import CartLineItemsTable from './cart-line-items-table';
|
||||
import { CartExpressPayment } from '../../payment-methods';
|
||||
import './style.scss';
|
||||
|
||||
interface CartAttributes {
|
||||
hasDarkControls: boolean;
|
||||
isShippingCalculatorEnabled: boolean;
|
||||
checkoutPageId: number;
|
||||
isPreview: boolean;
|
||||
showRateAfterTaxName: boolean;
|
||||
}
|
||||
|
||||
interface CartProps {
|
||||
attributes: CartAttributes;
|
||||
}
|
||||
/**
|
||||
* Component that renders the Cart block when user has something in cart aka "full".
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {Object} props.attributes Incoming attributes for block.
|
||||
*/
|
||||
const Cart = ( { attributes }: CartProps ): JSX.Element => {
|
||||
const {
|
||||
isShippingCalculatorEnabled,
|
||||
hasDarkControls,
|
||||
showRateAfterTaxName,
|
||||
} = attributes;
|
||||
|
||||
const {
|
||||
cartItems,
|
||||
cartFees,
|
||||
cartTotals,
|
||||
cartIsLoading,
|
||||
cartItemsCount,
|
||||
cartItemErrors,
|
||||
cartNeedsPayment,
|
||||
cartNeedsShipping,
|
||||
} = useStoreCart();
|
||||
|
||||
const {
|
||||
applyCoupon,
|
||||
removeCoupon,
|
||||
isApplyingCoupon,
|
||||
isRemovingCoupon,
|
||||
appliedCoupons,
|
||||
} = useStoreCartCoupons();
|
||||
|
||||
const { addErrorNotice } = useStoreNotices();
|
||||
|
||||
// Ensures any cart errors listed in the API response get shown.
|
||||
useEffect( () => {
|
||||
cartItemErrors.forEach( ( error ) => {
|
||||
addErrorNotice( decodeEntities( error.message ), {
|
||||
isDismissible: true,
|
||||
id: error.code,
|
||||
} );
|
||||
} );
|
||||
}, [ addErrorNotice, cartItemErrors ] );
|
||||
|
||||
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
|
||||
|
||||
const cartClassName = classnames( 'wc-block-cart', {
|
||||
'wc-block-cart--is-loading': cartIsLoading,
|
||||
'has-dark-controls': hasDarkControls,
|
||||
} );
|
||||
|
||||
// Prepare props to pass to the ExperimentalOrderMeta slot fill.
|
||||
// We need to pluck out receiveCart.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { extensions, ...cart } = useStoreCart();
|
||||
const slotFillProps = {
|
||||
extensions,
|
||||
cart,
|
||||
};
|
||||
|
||||
const discountsSlotFillProps = {
|
||||
extensions,
|
||||
cart,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CartLineItemsTitle itemCount={ cartItemsCount } />
|
||||
<SidebarLayout className={ cartClassName }>
|
||||
<Main className="wc-block-cart__main">
|
||||
<CartLineItemsTable
|
||||
lineItems={ cartItems }
|
||||
isLoading={ cartIsLoading }
|
||||
/>
|
||||
</Main>
|
||||
<Sidebar className="wc-block-cart__sidebar">
|
||||
<Title
|
||||
headingLevel="2"
|
||||
className="wc-block-cart__totals-title"
|
||||
>
|
||||
{ __( 'Cart totals', 'woo-gutenberg-products-block' ) }
|
||||
</Title>
|
||||
<TotalsWrapper>
|
||||
<Subtotal
|
||||
currency={ totalsCurrency }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
<TotalsFees
|
||||
currency={ totalsCurrency }
|
||||
cartFees={ cartFees }
|
||||
/>
|
||||
<TotalsDiscount
|
||||
cartCoupons={ appliedCoupons }
|
||||
currency={ totalsCurrency }
|
||||
isRemovingCoupon={ isRemovingCoupon }
|
||||
removeCoupon={ removeCoupon }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
{ getSetting( 'couponsEnabled', true ) && (
|
||||
<TotalsWrapper>
|
||||
<TotalsCoupon
|
||||
onSubmit={ applyCoupon }
|
||||
isLoading={ isApplyingCoupon }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
) }
|
||||
<ExperimentalDiscountsMeta.Slot
|
||||
{ ...discountsSlotFillProps }
|
||||
/>
|
||||
{ cartNeedsShipping && (
|
||||
<TotalsWrapper>
|
||||
<TotalsShipping
|
||||
showCalculator={ isShippingCalculatorEnabled }
|
||||
showRateSelector={ true }
|
||||
values={ cartTotals }
|
||||
currency={ totalsCurrency }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
) }
|
||||
{ ! getSetting( 'displayCartPricesIncludingTax', false ) &&
|
||||
parseInt( cartTotals.total_tax, 10 ) > 0 && (
|
||||
<TotalsWrapper>
|
||||
<TotalsTaxes
|
||||
showRateAfterTaxName={
|
||||
showRateAfterTaxName
|
||||
}
|
||||
currency={ totalsCurrency }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
) }
|
||||
<TotalsWrapper>
|
||||
<TotalsFooterItem
|
||||
currency={ totalsCurrency }
|
||||
values={ cartTotals }
|
||||
/>
|
||||
</TotalsWrapper>
|
||||
|
||||
<ExperimentalOrderMeta.Slot { ...slotFillProps } />
|
||||
|
||||
<div className="wc-block-cart__payment-options">
|
||||
{ cartNeedsPayment && <CartExpressPayment /> }
|
||||
<CheckoutButton
|
||||
link={ getSetting(
|
||||
'page-' + attributes?.checkoutPageId,
|
||||
false
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</SidebarLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cart;
|
|
@ -0,0 +1,264 @@
|
|||
.wc-block-cart {
|
||||
.wc-block-components-shipping-calculator {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wc-block-components-address-form {
|
||||
.wc-block-components-text-input,
|
||||
.wc-block-components-country-input,
|
||||
.wc-block-components-state-input {
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.wc-block-cart-items,
|
||||
table.wc-block-cart-items th,
|
||||
table.wc-block-cart-items td {
|
||||
// Override Storefront theme gray table background.
|
||||
background: none !important;
|
||||
// Remove borders on default themes.
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper table.wc-block-cart-items,
|
||||
table.wc-block-cart-items {
|
||||
width: 100%;
|
||||
|
||||
.wc-block-cart-items__header {
|
||||
@include font-size(smaller);
|
||||
text-transform: uppercase;
|
||||
|
||||
.wc-block-cart-items__header-image {
|
||||
width: 100px;
|
||||
}
|
||||
.wc-block-cart-items__header-product {
|
||||
visibility: hidden;
|
||||
}
|
||||
.wc-block-cart-items__header-total {
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.wc-block-cart-items__row {
|
||||
.wc-block-cart-item__image img {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.wc-block-cart-item__quantity {
|
||||
.wc-block-cart-item__remove-link {
|
||||
@include link-button;
|
||||
@include font-size(smaller);
|
||||
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.wc-block-components-product-name {
|
||||
display: block;
|
||||
max-width: max-content;
|
||||
}
|
||||
.wc-block-cart-item__total {
|
||||
@include font-size(regular);
|
||||
text-align: right;
|
||||
line-height: inherit;
|
||||
}
|
||||
.wc-block-components-product-metadata {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-cart {
|
||||
.wc-block-components-totals-taxes,
|
||||
.wc-block-components-totals-footer-item {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading placeholder state.
|
||||
.wc-block-cart--is-loading,
|
||||
.wc-block-mini-cart__drawer.is-loading {
|
||||
th span,
|
||||
h2 span {
|
||||
@include placeholder();
|
||||
@include force-content();
|
||||
min-width: 84px;
|
||||
display: inline-block;
|
||||
}
|
||||
h2 span {
|
||||
min-width: 33%;
|
||||
}
|
||||
.wc-block-components-product-price,
|
||||
.wc-block-components-product-metadata,
|
||||
.wc-block-components-quantity-selector {
|
||||
@include placeholder();
|
||||
}
|
||||
.wc-block-components-product-name {
|
||||
@include placeholder();
|
||||
@include force-content();
|
||||
min-width: 84px;
|
||||
display: inline-block;
|
||||
}
|
||||
.wc-block-components-product-metadata {
|
||||
margin-top: 0.25em;
|
||||
min-width: 8em;
|
||||
}
|
||||
.wc-block-cart-item__remove-link {
|
||||
visibility: hidden;
|
||||
}
|
||||
.wc-block-cart-item__image > a {
|
||||
@include placeholder();
|
||||
display: block;
|
||||
}
|
||||
.wc-block-components-product-price {
|
||||
@include force-content();
|
||||
max-width: 3em;
|
||||
display: block;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.wc-block-cart__sidebar .components-card {
|
||||
@include placeholder();
|
||||
@include force-content();
|
||||
min-height: 460px;
|
||||
}
|
||||
}
|
||||
.wc-block-components-sidebar-layout.wc-block-cart--skeleton {
|
||||
display: none;
|
||||
}
|
||||
.is-loading + .wc-block-components-sidebar-layout.wc-block-cart--skeleton {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.wc-block-cart-item__total-price-and-sale-badge-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.wc-block-components-sale-badge {
|
||||
margin-top: $gap-smallest;
|
||||
}
|
||||
}
|
||||
|
||||
.is-small,
|
||||
.is-mobile {
|
||||
.wc-block-cart-item__total {
|
||||
.wc-block-components-sale-badge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-medium,
|
||||
.is-small,
|
||||
.is-mobile {
|
||||
&.wc-block-cart {
|
||||
.wc-block-components-sidebar {
|
||||
.wc-block-cart__totals-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
table.wc-block-cart-items {
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
.wc-block-cart-items__header {
|
||||
display: none;
|
||||
}
|
||||
.wc-block-cart-item__remove-link {
|
||||
display: none;
|
||||
}
|
||||
.wc-block-cart-items__row {
|
||||
@include with-translucent-border(0 0 1px);
|
||||
display: grid;
|
||||
grid-template-columns: 80px 132px;
|
||||
padding: $gap 0;
|
||||
|
||||
.wc-block-cart-item__image {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
padding-right: $gap;
|
||||
}
|
||||
.wc-block-cart-item__product {
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 4;
|
||||
grid-row-start: 1;
|
||||
justify-self: stretch;
|
||||
padding: 0 $gap $gap 0;
|
||||
}
|
||||
.wc-block-cart-item__quantity {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 2;
|
||||
vertical-align: bottom;
|
||||
padding-right: $gap;
|
||||
align-self: end;
|
||||
padding-top: $gap;
|
||||
}
|
||||
.wc-block-cart-item__total {
|
||||
grid-row-start: 1;
|
||||
|
||||
.wc-block-components-formatted-money-amount {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-large.wc-block-cart {
|
||||
.wc-block-cart-items {
|
||||
@include with-translucent-border(0 0 1px);
|
||||
|
||||
th {
|
||||
padding: 0.25rem $gap 0.25rem 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
td {
|
||||
@include with-translucent-border(1px 0 0);
|
||||
padding: $gap 0 $gap $gap;
|
||||
vertical-align: top;
|
||||
}
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
td:last-child {
|
||||
padding-right: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__input {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.wc-block-cart__totals-title {
|
||||
@include text-heading();
|
||||
@include font-size(smaller);
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wc-block-components-sidebar {
|
||||
.wc-block-components-shipping-calculator,
|
||||
.wc-block-components-shipping-rates-control__package:not(.wc-block-components-panel) {
|
||||
padding-left: $gap;
|
||||
padding-right: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-cart__payment-options {
|
||||
padding: $gap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InnerBlocks } from '@wordpress/block-editor';
|
||||
import { Icon, cart } from '@woocommerce/icons';
|
||||
import classnames from 'classnames';
|
||||
import { registerFeaturePluginBlockType } from '@woocommerce/block-settings';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import edit from './edit';
|
||||
import './style.scss';
|
||||
import blockAttributes from './attributes';
|
||||
|
||||
/**
|
||||
* Register and run the Cart block.
|
||||
*/
|
||||
const settings = {
|
||||
title: __( 'Cart i2', 'woo-gutenberg-products-block' ),
|
||||
icon: {
|
||||
src: <Icon srcElement={ cart } />,
|
||||
foreground: '#96588a',
|
||||
},
|
||||
category: 'woocommerce',
|
||||
keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ],
|
||||
description: __( 'Shopping cart.', 'woo-gutenberg-products-block' ),
|
||||
supports: {
|
||||
align: [ 'wide', 'full' ],
|
||||
html: false,
|
||||
multiple: false,
|
||||
},
|
||||
example: {
|
||||
attributes: {
|
||||
isPreview: true,
|
||||
},
|
||||
},
|
||||
attributes: blockAttributes,
|
||||
edit,
|
||||
|
||||
// Save the props to post content.
|
||||
save( { attributes } ) {
|
||||
return (
|
||||
<div className={ classnames( 'is-loading', attributes.className ) }>
|
||||
<InnerBlocks.Content />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
registerFeaturePluginBlockType( 'woocommerce/cart-i2', settings );
|
|
@ -0,0 +1,7 @@
|
|||
.wp-block-woocommerce-cart.is-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wp-block-woocommerce-cart {
|
||||
margin-bottom: 3em;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,165 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { previewCart } from '@woocommerce/resource-previews';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
||||
import { default as fetchMock } from 'jest-fetch-mock';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from '../block';
|
||||
import { defaultCartState } from '../../../../data/default-states';
|
||||
import { allSettings } from '../../../../settings/shared/settings-init';
|
||||
|
||||
const CartBlock = ( props ) => (
|
||||
<SlotFillProvider>
|
||||
<Block { ...props } />
|
||||
</SlotFillProvider>
|
||||
);
|
||||
describe( 'Testing cart', () => {
|
||||
beforeEach( async () => {
|
||||
fetchMock.mockResponse( ( req ) => {
|
||||
if ( req.url.match( /wc\/store\/cart/ ) ) {
|
||||
return Promise.resolve( JSON.stringify( previewCart ) );
|
||||
}
|
||||
return Promise.resolve( '' );
|
||||
} );
|
||||
// need to clear the store resolution state between tests.
|
||||
await dispatch( storeKey ).invalidateResolutionForStore();
|
||||
await dispatch( storeKey ).receiveCart( defaultCartState.cartData );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
fetchMock.resetMocks();
|
||||
} );
|
||||
|
||||
it( 'renders cart if there are items in the cart', async () => {
|
||||
render(
|
||||
<CartBlock
|
||||
emptyCart={ null }
|
||||
attributes={ {
|
||||
isShippingCalculatorEnabled: false,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect(
|
||||
screen.getByText( /Proceed to Checkout/i )
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
|
||||
// ["`select` control in `@wordpress/data-controls` is deprecated. Please use built-in `resolveSelect` control in `@wordpress/data` instead."]
|
||||
expect( console ).toHaveWarned();
|
||||
} );
|
||||
|
||||
it( 'Contains a Taxes section if Core options are set to show it', async () => {
|
||||
allSettings.displayCartPricesIncludingTax = false;
|
||||
// The criteria for showing the Taxes section is:
|
||||
// Display prices during basket and checkout: 'Excluding tax'.
|
||||
const { container } = render(
|
||||
<CartBlock
|
||||
emptyCart={ null }
|
||||
attributes={ {
|
||||
isShippingCalculatorEnabled: false,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'Shows individual tax lines if the store is set to do so', async () => {
|
||||
allSettings.displayCartPricesIncludingTax = false;
|
||||
allSettings.displayItemizedTaxes = true;
|
||||
// The criteria for showing the lines in the Taxes section is:
|
||||
// Display prices during basket and checkout: 'Excluding tax'.
|
||||
// Display tax totals: 'Itemized';
|
||||
const { container } = render(
|
||||
<CartBlock
|
||||
emptyCart={ null }
|
||||
attributes={ {
|
||||
isShippingCalculatorEnabled: false,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'Shows rate percentages after tax lines if the block is set to do so', async () => {
|
||||
allSettings.displayCartPricesIncludingTax = false;
|
||||
allSettings.displayItemizedTaxes = true;
|
||||
// The criteria for showing the lines in the Taxes section is:
|
||||
// Display prices during basket and checkout: 'Excluding tax'.
|
||||
// Display tax totals: 'Itemized';
|
||||
const { container } = render(
|
||||
<CartBlock
|
||||
emptyCart={ null }
|
||||
attributes={ {
|
||||
showRateAfterTaxName: true,
|
||||
isShippingCalculatorEnabled: false,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect( container ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'renders empty cart if there are no items in the cart', async () => {
|
||||
fetchMock.mockResponse( ( req ) => {
|
||||
if ( req.url.match( /wc\/store\/cart/ ) ) {
|
||||
return Promise.resolve(
|
||||
JSON.stringify( defaultCartState.cartData )
|
||||
);
|
||||
}
|
||||
return Promise.resolve( '' );
|
||||
} );
|
||||
render(
|
||||
<CartBlock
|
||||
emptyCart={ '<div>Empty Cart</div>' }
|
||||
attributes={ {
|
||||
isShippingCalculatorEnabled: false,
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect( screen.getByText( /Empty Cart/i ) ).toBeInTheDocument();
|
||||
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'renders correct cart line subtotal when currency has 0 decimals', async () => {
|
||||
fetchMock.mockResponse( ( req ) => {
|
||||
if ( req.url.match( /wc\/store\/cart/ ) ) {
|
||||
const cart = {
|
||||
...previewCart,
|
||||
// Make it so there is only one item to simplify things.
|
||||
items: [
|
||||
{
|
||||
...previewCart.items[ 0 ],
|
||||
totals: {
|
||||
...previewCart.items[ 0 ].totals,
|
||||
// Change price format so there are no decimals.
|
||||
currency_minor_unit: 0,
|
||||
currency_prefix: '',
|
||||
currency_suffix: '€',
|
||||
line_subtotal: '16',
|
||||
line_total: '18',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return Promise.resolve( JSON.stringify( cart ) );
|
||||
}
|
||||
} );
|
||||
render( <CartBlock emptyCart={ null } attributes={ {} } /> );
|
||||
|
||||
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
|
||||
expect( screen.getAllByRole( 'cell' )[ 1 ] ).toHaveTextContent( '16€' );
|
||||
} );
|
||||
} );
|
|
@ -46,6 +46,10 @@ const blocks = {
|
|||
checkout: {
|
||||
customDir: 'cart-checkout/checkout',
|
||||
},
|
||||
'cart-i2': {
|
||||
customDir: 'cart-checkout/cart-i2',
|
||||
isExperimental: true,
|
||||
},
|
||||
'mini-cart': {
|
||||
customDir: 'cart-checkout/mini-cart',
|
||||
isExperimental: true,
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
"./assets/js/settings/blocks/**",
|
||||
"./assets/js/middleware/**",
|
||||
"./assets/js/blocks/cart-checkout/checkout/inner-blocks/**/index.tsx",
|
||||
"./assets/js/blocks/cart-checkout/checkout/inner-blocks/register-components.ts"
|
||||
"./assets/js/blocks/cart-checkout/checkout/inner-blocks/register-components.ts",
|
||||
"./assets/js/blocks/cart-checkout/cart-i2/inner-blocks/**/index.tsx",
|
||||
"./assets/js/blocks/cart-checkout/cart-i2/inner-blocks/register-components.ts"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Assets;
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
|
||||
/**
|
||||
* Cart class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CartI2 extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'cart-i2';
|
||||
|
||||
/**
|
||||
* Get the editor script handle for this block type.
|
||||
*
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string;
|
||||
*/
|
||||
protected function get_block_type_editor_script( $key = null ) {
|
||||
$script = [
|
||||
'handle' => 'wc-' . $this->block_name . '-block',
|
||||
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
|
||||
'dependencies' => [ 'wc-blocks' ],
|
||||
];
|
||||
return $key ? $script[ $key ] : $script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @see $this->register_block_type()
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
$script = [
|
||||
'handle' => 'wc-' . $this->block_name . '-block-frontend',
|
||||
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
|
||||
'dependencies' => [],
|
||||
];
|
||||
return $key ? $script[ $key ] : $script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend assets for this block, just in time for rendering.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
*/
|
||||
protected function enqueue_assets( array $attributes ) {
|
||||
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' );
|
||||
parent::enqueue_assets( $attributes );
|
||||
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Append frontend scripts when rendering the Cart block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content ) {
|
||||
// Deregister core cart scripts and styles.
|
||||
wp_dequeue_script( 'wc-cart' );
|
||||
wp_dequeue_script( 'wc-password-strength-meter' );
|
||||
wp_dequeue_script( 'selectWoo' );
|
||||
wp_dequeue_style( 'select2' );
|
||||
|
||||
return $this->inject_html_data_attributes( $content . $this->get_skeleton(), $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
$this->asset_data_registry->add(
|
||||
'shippingCountries',
|
||||
function() {
|
||||
return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() );
|
||||
},
|
||||
true
|
||||
);
|
||||
$this->asset_data_registry->add(
|
||||
'shippingStates',
|
||||
function() {
|
||||
return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() );
|
||||
},
|
||||
true
|
||||
);
|
||||
$this->asset_data_registry->add(
|
||||
'countryLocale',
|
||||
function() {
|
||||
// Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
|
||||
$country_locale = wc()->countries->get_country_locale();
|
||||
$states = wc()->countries->get_states();
|
||||
|
||||
foreach ( $states as $country => $states ) {
|
||||
if ( empty( $states ) ) {
|
||||
$country_locale[ $country ]['state']['required'] = false;
|
||||
$country_locale[ $country ]['state']['hidden'] = true;
|
||||
}
|
||||
}
|
||||
return $country_locale;
|
||||
},
|
||||
true
|
||||
);
|
||||
$this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true );
|
||||
$this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true );
|
||||
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true );
|
||||
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true );
|
||||
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true );
|
||||
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true );
|
||||
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true );
|
||||
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
|
||||
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
|
||||
|
||||
// Hydrate the following data depending on admin or frontend context.
|
||||
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
|
||||
$this->hydrate_from_api();
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_blocks_cart_enqueue_data' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes accents from an array of values, sorts by the values, then returns the original array values sorted.
|
||||
*
|
||||
* @param array $array Array of values to sort.
|
||||
* @return array Sorted array.
|
||||
*/
|
||||
protected function deep_sort_with_accents( $array ) {
|
||||
if ( ! is_array( $array ) || empty( $array ) ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
if ( is_array( reset( $array ) ) ) {
|
||||
return array_map( [ $this, 'deep_sort_with_accents' ], $array );
|
||||
}
|
||||
|
||||
$array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) );
|
||||
asort( $array_without_accents );
|
||||
return array_replace( $array_without_accents, $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the cart block with data from the API.
|
||||
*/
|
||||
protected function hydrate_from_api() {
|
||||
$this->asset_data_registry->hydrate_api_request( '/wc/store/cart' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render skeleton markup for the cart block.
|
||||
*/
|
||||
protected function get_skeleton() {
|
||||
return '
|
||||
<div class="wc-block-skeleton wc-block-components-sidebar-layout wc-block-cart wc-block-cart--is-loading wc-block-cart--skeleton hidden" aria-hidden="true">
|
||||
<div class="wc-block-components-main wc-block-cart__main">
|
||||
<h2 class="wc-block-components-title"><span></span></h2>
|
||||
<table class="wc-block-cart-items">
|
||||
<thead>
|
||||
<tr class="wc-block-cart-items__header">
|
||||
<th class="wc-block-cart-items__header-image"><span /></th>
|
||||
<th class="wc-block-cart-items__header-product"><span /></th>
|
||||
<th class="wc-block-cart-items__header-total"><span /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="wc-block-cart-items__row">
|
||||
<td class="wc-block-cart-item__image">
|
||||
<a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__product">
|
||||
<div class="wc-block-components-product-name"></div>
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
<div class="wc-block-components-product-metadata"></div>
|
||||
<div class="wc-block-components-quantity-selector">
|
||||
<input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" />
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__total">
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wc-block-cart-items__row">
|
||||
<td class="wc-block-cart-item__image">
|
||||
<a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__product">
|
||||
<div class="wc-block-components-product-name"></div>
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
<div class="wc-block-components-product-metadata"></div>
|
||||
<div class="wc-block-components-quantity-selector">
|
||||
<input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" />
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__total">
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wc-block-cart-items__row">
|
||||
<td class="wc-block-cart-item__image">
|
||||
<a href=""><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" width="1" height="1" /></a>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__product">
|
||||
<div class="wc-block-components-product-name"></div>
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
<div class="wc-block-components-product-metadata"></div>
|
||||
<div class="wc-block-components-quantity-selector">
|
||||
<input class="wc-block-components-quantity-selector__input" type="number" step="1" min="0" value="1" />
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>
|
||||
<button class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="wc-block-cart-item__total">
|
||||
<div class="wc-block-components-product-price"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="wc-block-components-sidebar wc-block-cart__sidebar">
|
||||
<div class="components-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
' . $this->get_skeleton_inline_script();
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@ final class BlockTypesController {
|
|||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
$block_types[] = 'SingleProduct';
|
||||
$block_types[] = 'CartI2';
|
||||
$block_types[] = 'MiniCart';
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class Api {
|
|||
* @return array
|
||||
*/
|
||||
public function add_payment_method_script_dependencies( $dependencies, $handle ) {
|
||||
if ( ! in_array( $handle, [ 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-block', 'wc-cart-block-frontend' ], true ) ) {
|
||||
if ( ! in_array( $handle, [ 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-block', 'wc-cart-block-frontend', 'wc-cart-i2-block', 'wc-cart-i2-block-frontend' ], true ) ) {
|
||||
return $dependencies;
|
||||
}
|
||||
return array_merge( $dependencies, $this->payment_method_registry->get_all_active_payment_method_script_dependencies() );
|
||||
|
@ -214,7 +214,7 @@ class Api {
|
|||
sprintf( 'console.error( "%s" );', $error_message )
|
||||
);
|
||||
|
||||
$cart_checkout_scripts = [ 'wc-cart-block', 'wc-cart-block-frontend', 'wc-checkout-block', 'wc-checkout-block-frontend' ];
|
||||
$cart_checkout_scripts = [ 'wc-cart-block', 'wc-cart-block-frontend', 'wc-checkout-block', 'wc-checkout-block-frontend', 'wc-cart-i2-block', 'wc-cart-i2-block-frontend' ];
|
||||
foreach ( $cart_checkout_scripts as $script_handle ) {
|
||||
if (
|
||||
! array_key_exists( $script_handle, $wp_scripts->registered ) ||
|
||||
|
|
Loading…
Reference in New Issue