Add Google Analytics Product Block and Checkout Events (https://github.com/woocommerce/woocommerce-blocks/pull/3967)
* Move PHP GA code to service class * product search tracking * Improve inline script handling * trackViewProduct * Product link events * Rename events * Add checkout specific event hook * Prevent useStoreCart response changing on rerender * Move address step into form so progress can be more easily tracked * Checkout progress events * Checkout events * Tidy up tracking code * Track error exceptions * add_payment_info event * remove console log * Track product grid block views Closes woocommerce/woocommerce-blocks#3959 * Checkout context was unused * Add comments to checkout events * correct step number * Standardize prefixes for events * Tracking requires GA extension * Fix select content events * adjust product list render tracking so it's inline * Inline search event handling * remove render callback hook * revert render callback changes * Update GA bootstrap code
This commit is contained in:
parent
40c569d9ef
commit
86e54c19a0
|
@ -6,7 +6,8 @@ import Button from '@woocommerce/base-components/button';
|
|||
import { Icon, done as doneIcon } from '@woocommerce/icons';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { useAddToCartFormContext } from '@woocommerce/base-context';
|
||||
import { useStoreAddToCart } from '@woocommerce/base-hooks';
|
||||
import { useStoreAddToCart, useStoreEvents } from '@woocommerce/base-hooks';
|
||||
import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
|
||||
|
||||
/**
|
||||
* Add to Cart Form Button Component.
|
||||
|
@ -24,6 +25,8 @@ const AddToCartButton = () => {
|
|||
hasError,
|
||||
dispatchActions,
|
||||
} = useAddToCartFormContext();
|
||||
const { parentName } = useInnerBlockLayoutContext();
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
const { cartQuantity } = useStoreAddToCart( product.id || 0 );
|
||||
const [ addedToCart, setAddedToCart ] = useState( false );
|
||||
const addToCartButtonData = product.add_to_cart || {
|
||||
|
@ -69,7 +72,13 @@ const AddToCartButton = () => {
|
|||
isDisabled={ isDisabled }
|
||||
isProcessing={ isProcessing }
|
||||
isDone={ addedToCart }
|
||||
onClick={ () => dispatchActions.submitForm() }
|
||||
onClick={ () => {
|
||||
dispatchActions.submitForm();
|
||||
dispatchStoreEvent( 'cart-add-item', {
|
||||
product,
|
||||
listName: parentName,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
) : (
|
||||
<LinkComponent
|
||||
|
@ -79,6 +88,12 @@ const AddToCartButton = () => {
|
|||
addToCartButtonData.text ||
|
||||
__( 'View Product', 'woo-gutenberg-products-block' )
|
||||
}
|
||||
onClick={ () => {
|
||||
dispatchStoreEvent( 'product-view-link', {
|
||||
product,
|
||||
listName: parentName,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -90,10 +105,16 @@ const AddToCartButton = () => {
|
|||
* @param {string} props.className Css classnames.
|
||||
* @param {string} props.href Link for button.
|
||||
* @param {string} props.text Text content for button.
|
||||
* @param {function():any} props.onClick Callback to execute when button is clicked.
|
||||
*/
|
||||
const LinkComponent = ( { className, href, text } ) => {
|
||||
const LinkComponent = ( { className, href, text, onClick } ) => {
|
||||
return (
|
||||
<Button className={ className } href={ href } rel="nofollow">
|
||||
<Button
|
||||
className={ className }
|
||||
href={ href }
|
||||
onClick={ onClick }
|
||||
rel="nofollow"
|
||||
>
|
||||
{ text }
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -99,10 +99,15 @@ const AddToCartButton = ( { product } ) => {
|
|||
if ( ! allowAddToCart ) {
|
||||
buttonProps.href = permalink;
|
||||
buttonProps.rel = 'nofollow';
|
||||
buttonProps.onClick = () => {
|
||||
dispatchStoreEvent( 'product-view-link', {
|
||||
product,
|
||||
} );
|
||||
};
|
||||
} else {
|
||||
buttonProps.onClick = () => {
|
||||
addToCart();
|
||||
dispatchStoreEvent( 'add-cart-item', {
|
||||
dispatchStoreEvent( 'cart-add-item', {
|
||||
product,
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
useProductDataContext,
|
||||
} from '@woocommerce/shared-context';
|
||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||
import { useStoreEvents } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -39,6 +40,7 @@ export const Block = ( {
|
|||
const { parentClassName } = useInnerBlockLayoutContext();
|
||||
const { product } = useProductDataContext();
|
||||
const [ imageLoaded, setImageLoaded ] = useState( false );
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
|
||||
if ( ! product.id ) {
|
||||
return (
|
||||
|
@ -68,6 +70,11 @@ export const Block = ( {
|
|||
href: product.permalink,
|
||||
rel: 'nofollow',
|
||||
...( ! hasProductImages && { 'aria-label': anchorLabel } ),
|
||||
onClick: () => {
|
||||
dispatchStoreEvent( 'product-view-link', {
|
||||
product,
|
||||
} );
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,6 +12,7 @@ import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
|||
import { gatedStyledText } from '@woocommerce/atomic-utils';
|
||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||
import ProductName from '@woocommerce/base-components/product-name';
|
||||
import { useStoreEvents } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -45,6 +46,7 @@ export const Block = ( {
|
|||
} ) => {
|
||||
const { parentClassName } = useInnerBlockLayoutContext();
|
||||
const { product } = useProductDataContext();
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
const TagName = `h${ headingLevel }`;
|
||||
|
||||
const colorClass = getColorClassName( 'color', color );
|
||||
|
@ -104,6 +106,11 @@ export const Block = ( {
|
|||
color: customColor,
|
||||
fontSize: customFontSize,
|
||||
} ) }
|
||||
onClick={ () => {
|
||||
dispatchStoreEvent( 'product-view-link', {
|
||||
product,
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
</TagName>
|
||||
);
|
||||
|
|
|
@ -147,12 +147,10 @@ const ProductList = ( {
|
|||
|
||||
// If the product list changes, trigger an event.
|
||||
useEffect( () => {
|
||||
if ( products.length > 0 ) {
|
||||
dispatchStoreEvent( 'list-products', {
|
||||
products,
|
||||
listName: parentName,
|
||||
} );
|
||||
}
|
||||
dispatchStoreEvent( 'product-list-render', {
|
||||
products,
|
||||
listName: parentName,
|
||||
} );
|
||||
}, [ products, parentName, dispatchStoreEvent ] );
|
||||
|
||||
// If query state (excluding pagination/sorting attributes) changed, reset pagination to the first page.
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
useStoreNotices,
|
||||
useEmitResponse,
|
||||
usePrevious,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
|
@ -101,6 +102,7 @@ export const CheckoutStateProvider = ( {
|
|||
const currentObservers = useRef( observers );
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const { addErrorNotice, removeNotices } = useStoreNotices();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const isCalculating = checkoutState.calculatingCount > 0;
|
||||
const {
|
||||
isSuccessResponse,
|
||||
|
@ -350,8 +352,9 @@ export const CheckoutStateProvider = ( {
|
|||
] );
|
||||
|
||||
const onSubmit = useCallback( () => {
|
||||
dispatchCheckoutEvent( 'submit' );
|
||||
dispatch( actions.setBeforeProcessing() );
|
||||
}, [] );
|
||||
}, [ dispatchCheckoutEvent ] );
|
||||
|
||||
/**
|
||||
* @type {CheckoutDataContext}
|
||||
|
|
|
@ -11,7 +11,11 @@ import {
|
|||
useRef,
|
||||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { useStoreNotices, useEmitResponse } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
useStoreNotices,
|
||||
useEmitResponse,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -102,6 +106,7 @@ export const PaymentMethodDataProvider = ( {
|
|||
isFailResponse,
|
||||
noticeContexts,
|
||||
} = useEmitResponse();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const [ activePaymentMethod, setActive ] = useState( '' ); // The active payment method - e.g. Stripe CC or BACS.
|
||||
const [ activeSavedToken, setActiveSavedToken ] = useState( '' ); // If a previously saved payment method is active, the token for that method. For example, a for a Stripe CC card saved to user account.
|
||||
|
@ -122,8 +127,11 @@ export const PaymentMethodDataProvider = ( {
|
|||
( paymentMethodSlug ) => {
|
||||
setActive( paymentMethodSlug );
|
||||
dispatch( statusOnly( PRISTINE ) );
|
||||
dispatchCheckoutEvent( 'set-active-payment-method', {
|
||||
paymentMethodSlug,
|
||||
} );
|
||||
},
|
||||
[ setActive, dispatch ]
|
||||
[ setActive, dispatch, dispatchCheckoutEvent ]
|
||||
);
|
||||
|
||||
const paymentMethodsDispatcher = useCallback<
|
||||
|
|
|
@ -14,6 +14,11 @@ import {
|
|||
SnackbarNoticesContainer,
|
||||
} from '@woocommerce/base-components/store-notices-container';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreEvents } from '../hooks/use-store-events';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').NoticeContext} NoticeContext
|
||||
* @typedef {import('react')} React
|
||||
|
@ -61,6 +66,7 @@ export const StoreNoticesProvider = ( {
|
|||
} ) => {
|
||||
const { createNotice, removeNotice } = useDispatch( 'core/notices' );
|
||||
const [ isSuppressed, setIsSuppressed ] = useState( false );
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
|
||||
const createNoticeWithContext = useCallback(
|
||||
( status = 'default', content = '', options = {} ) => {
|
||||
|
@ -68,8 +74,13 @@ export const StoreNoticesProvider = ( {
|
|||
...options,
|
||||
context: options.context || context,
|
||||
} );
|
||||
dispatchStoreEvent( 'store-notice-create', {
|
||||
status,
|
||||
content,
|
||||
options,
|
||||
} );
|
||||
},
|
||||
[ createNotice, context ]
|
||||
[ createNotice, dispatchStoreEvent, context ]
|
||||
);
|
||||
|
||||
const removeNoticeWithContext = useCallback(
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isEqual } from 'lodash';
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useEditorContext } from '@woocommerce/base-context';
|
||||
|
@ -113,6 +115,7 @@ export const useStoreCart = (
|
|||
const { isEditor, previewData } = useEditorContext();
|
||||
const previewCart = previewData?.previewCart || {};
|
||||
const { shouldSelect } = options;
|
||||
const currentResults = useRef();
|
||||
|
||||
const results: StoreCart = useSelect(
|
||||
( select, { dispatch } ) => {
|
||||
|
@ -188,5 +191,13 @@ export const useStoreCart = (
|
|||
},
|
||||
[ shouldSelect ]
|
||||
);
|
||||
return results;
|
||||
|
||||
if (
|
||||
! currentResults.current ||
|
||||
! isEqual( currentResults.current, results )
|
||||
) {
|
||||
currentResults.current = results;
|
||||
}
|
||||
|
||||
return currentResults.current;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Rate } from '@woocommerce/type-defs/shipping';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useSelectShippingRates } from './use-select-shipping-rates';
|
||||
import { useStoreEvents } from '../use-store-events';
|
||||
|
||||
/**
|
||||
* Selected rates are derived by looping over the shipping rates.
|
||||
|
@ -39,6 +40,8 @@ export const useSelectShippingRate = (
|
|||
selectedShippingRate: string | undefined;
|
||||
isSelectingRate: boolean;
|
||||
} => {
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
// Rates are selected via the shipping data context provider.
|
||||
const { selectShippingRate, isSelectingRate } = useSelectShippingRates();
|
||||
|
||||
|
@ -62,8 +65,11 @@ export const useSelectShippingRate = (
|
|||
( newShippingRateId ) => {
|
||||
setSelectedShippingRate( newShippingRateId );
|
||||
selectShippingRate( newShippingRateId, packageId );
|
||||
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
|
||||
shippingRateId: newShippingRateId,
|
||||
} );
|
||||
},
|
||||
[ packageId, selectShippingRate ]
|
||||
[ packageId, selectShippingRate, dispatchCheckoutEvent ]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -4,40 +4,56 @@
|
|||
import { doAction } from '@wordpress/hooks';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreCart } from './cart/use-store-cart';
|
||||
|
||||
type StoreEvent = (
|
||||
eventName: string,
|
||||
eventParams?: Partial< Record< string, unknown > >
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Abstraction on top of @wordpress/hooks for dispatching events via doAction for 3rd parties to hook into.
|
||||
*/
|
||||
export const useStoreEvents = (
|
||||
namespace = 'experimental__woocommerce_blocks'
|
||||
): {
|
||||
dispatchStoreEvent: (
|
||||
eventName: string,
|
||||
eventParams: Partial< Record< string, unknown > >
|
||||
) => void;
|
||||
export const useStoreEvents = (): {
|
||||
dispatchStoreEvent: StoreEvent;
|
||||
dispatchCheckoutEvent: StoreEvent;
|
||||
} => {
|
||||
const dispatchStoreEvent = useCallback(
|
||||
(
|
||||
eventName: string,
|
||||
eventParams: Partial< Record< string, unknown > >
|
||||
) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log( {
|
||||
event: `${ namespace }-${ eventName }`,
|
||||
eventParams,
|
||||
} );
|
||||
const storeCart = useStoreCart();
|
||||
|
||||
const dispatchStoreEvent = useCallback( ( eventName, eventParams = {} ) => {
|
||||
try {
|
||||
doAction(
|
||||
`experimental__woocommerce_blocks-${ eventName }`,
|
||||
eventParams
|
||||
);
|
||||
} catch ( e ) {
|
||||
// We don't handle thrown errors but just console.log for troubleshooting.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error( e );
|
||||
}
|
||||
}, [] );
|
||||
|
||||
const dispatchCheckoutEvent = useCallback(
|
||||
( eventName, eventParams = {} ) => {
|
||||
try {
|
||||
doAction( `${ namespace }-${ eventName }`, eventParams );
|
||||
doAction(
|
||||
`experimental__woocommerce_blocks-checkout-${ eventName }`,
|
||||
{
|
||||
...eventParams,
|
||||
storeCart,
|
||||
}
|
||||
);
|
||||
} catch ( e ) {
|
||||
// We don't handle thrown errors but just console.log for troubleshooting.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error( e );
|
||||
}
|
||||
},
|
||||
[ namespace ]
|
||||
[ storeCart ]
|
||||
);
|
||||
|
||||
return {
|
||||
dispatchStoreEvent,
|
||||
};
|
||||
return { dispatchStoreEvent, dispatchCheckoutEvent };
|
||||
};
|
||||
|
|
|
@ -249,7 +249,7 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
|||
maximum={ quantityLimit }
|
||||
onChange={ ( newQuantity ) => {
|
||||
setItemQuantity( newQuantity );
|
||||
dispatchStoreEvent( 'set-cart-item-quantity', {
|
||||
dispatchStoreEvent( 'cart-set-item-quantity', {
|
||||
product: lineItem,
|
||||
quantity: newQuantity,
|
||||
} );
|
||||
|
@ -260,7 +260,7 @@ const CartLineItemRow = ( { lineItem = {} } ) => {
|
|||
className="wc-block-cart-item__remove-link"
|
||||
onClick={ () => {
|
||||
removeItem();
|
||||
dispatchStoreEvent( 'remove-cart-item', {
|
||||
dispatchStoreEvent( 'cart-remove-item', {
|
||||
product: lineItem,
|
||||
quantity,
|
||||
} );
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCheckoutAddress } from '@woocommerce/base-hooks';
|
||||
import { useShippingDataContext } from '@woocommerce/base-context';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import BillingFieldsStep from './billing-fields-step';
|
||||
import ContactFieldsStep from './contact-fields-step';
|
||||
import ShippingFieldsStep from './shipping-fields-step';
|
||||
import PhoneNumber from './phone-number';
|
||||
import './style.scss';
|
||||
|
||||
const AddressStep = ( {
|
||||
requireCompanyField,
|
||||
requirePhoneField,
|
||||
showApartmentField,
|
||||
showCompanyField,
|
||||
showPhoneField,
|
||||
allowCreateAccount,
|
||||
} ) => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
billingFields,
|
||||
setBillingFields,
|
||||
setEmail,
|
||||
setPhone,
|
||||
setShippingAsBilling,
|
||||
setShippingFields,
|
||||
shippingAsBilling,
|
||||
shippingFields,
|
||||
showBillingFields,
|
||||
} = useCheckoutAddress();
|
||||
const { needsShipping } = useShippingDataContext();
|
||||
|
||||
const addressFieldsConfig = useMemo( () => {
|
||||
return {
|
||||
company: {
|
||||
hidden: ! showCompanyField,
|
||||
required: requireCompanyField,
|
||||
},
|
||||
address_2: {
|
||||
hidden: ! showApartmentField,
|
||||
},
|
||||
};
|
||||
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactFieldsStep
|
||||
emailValue={ billingFields.email }
|
||||
onChangeEmail={ setEmail }
|
||||
allowCreateAccount={ allowCreateAccount }
|
||||
/>
|
||||
{ needsShipping && (
|
||||
<ShippingFieldsStep
|
||||
shippingAsBilling={ shippingAsBilling }
|
||||
setShippingAsBilling={ setShippingAsBilling }
|
||||
>
|
||||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ setShippingFields }
|
||||
values={ shippingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingFields.phone }
|
||||
onChange={ setPhone }
|
||||
/>
|
||||
) }
|
||||
</ShippingFieldsStep>
|
||||
) }
|
||||
{ showBillingFields && (
|
||||
<BillingFieldsStep>
|
||||
<AddressForm
|
||||
id="billing"
|
||||
type="billing"
|
||||
onChange={ setBillingFields }
|
||||
values={ billingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && ! needsShipping && (
|
||||
<PhoneNumber
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingFields.phone }
|
||||
onChange={ setPhone }
|
||||
/>
|
||||
) }
|
||||
</BillingFieldsStep>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AddressStep.propTypes = {
|
||||
requireCompanyField: PropTypes.bool.isRequired,
|
||||
requirePhoneField: PropTypes.bool.isRequired,
|
||||
showApartmentField: PropTypes.bool.isRequired,
|
||||
showCompanyField: PropTypes.bool.isRequired,
|
||||
showPhoneField: PropTypes.bool.isRequired,
|
||||
allowCreateAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default AddressStep;
|
|
@ -1,14 +1,23 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import Form from '@woocommerce/base-components/form';
|
||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useMemo } from '@wordpress/element';
|
||||
import {
|
||||
useCheckoutContext,
|
||||
useShippingDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-hooks';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
import Form from '@woocommerce/base-components/form';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import AddressStep from './address-step';
|
||||
import BillingFieldsStep from './billing-fields-step';
|
||||
import ContactFieldsStep from './contact-fields-step';
|
||||
import ShippingFieldsStep from './shipping-fields-step';
|
||||
import PhoneNumber from './phone-number';
|
||||
import OrderNotesStep from './order-notes-step';
|
||||
import PaymentMethodStep from './payment-method-step';
|
||||
import ShippingOptionsStep from './shipping-options-step';
|
||||
|
@ -24,17 +33,106 @@ const CheckoutForm = ( {
|
|||
allowCreateAccount,
|
||||
} ) => {
|
||||
const { onSubmit } = useCheckoutContext();
|
||||
const {
|
||||
defaultAddressFields,
|
||||
billingFields,
|
||||
setBillingFields,
|
||||
setEmail,
|
||||
setPhone,
|
||||
setShippingAsBilling,
|
||||
setShippingFields,
|
||||
shippingAsBilling,
|
||||
shippingFields,
|
||||
showBillingFields,
|
||||
} = useCheckoutAddress();
|
||||
const { needsShipping } = useShippingDataContext();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const addressFieldsConfig = useMemo( () => {
|
||||
return {
|
||||
company: {
|
||||
hidden: ! showCompanyField,
|
||||
required: requireCompanyField,
|
||||
},
|
||||
address_2: {
|
||||
hidden: ! showApartmentField,
|
||||
},
|
||||
};
|
||||
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
|
||||
|
||||
// Ignore changes to dispatchCheckoutEvent callback so this is ran on first mount only.
|
||||
useEffect( () => {
|
||||
dispatchCheckoutEvent( 'render-checkout-form' );
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Form className="wc-block-checkout__form" onSubmit={ onSubmit }>
|
||||
<AddressStep
|
||||
requireCompanyField={ requireCompanyField }
|
||||
requirePhoneField={ requirePhoneField }
|
||||
showApartmentField={ showApartmentField }
|
||||
showCompanyField={ showCompanyField }
|
||||
showPhoneField={ showPhoneField }
|
||||
<ContactFieldsStep
|
||||
emailValue={ billingFields.email }
|
||||
onChangeEmail={ ( value ) => {
|
||||
setEmail( value );
|
||||
dispatchCheckoutEvent( 'set-email-address' );
|
||||
} }
|
||||
allowCreateAccount={ allowCreateAccount }
|
||||
/>
|
||||
{ needsShipping && (
|
||||
<ShippingFieldsStep
|
||||
shippingAsBilling={ shippingAsBilling }
|
||||
setShippingAsBilling={ setShippingAsBilling }
|
||||
>
|
||||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ ( values ) => {
|
||||
setShippingFields( values );
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
} }
|
||||
values={ shippingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingFields.phone }
|
||||
onChange={ ( value ) => {
|
||||
setPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'shipping',
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</ShippingFieldsStep>
|
||||
) }
|
||||
{ showBillingFields && (
|
||||
<BillingFieldsStep>
|
||||
<AddressForm
|
||||
id="billing"
|
||||
type="billing"
|
||||
onChange={ ( values ) => {
|
||||
setBillingFields( values );
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
} }
|
||||
values={ billingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && ! needsShipping && (
|
||||
<PhoneNumber
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingFields.phone }
|
||||
onChange={ ( value ) => {
|
||||
setPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'billing',
|
||||
} );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</BillingFieldsStep>
|
||||
) }
|
||||
<ShippingOptionsStep />
|
||||
<PaymentMethodStep />
|
||||
{ showOrderNotes && <OrderNotesStep /> }
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { withProduct } from '@woocommerce/block-hocs';
|
||||
import {
|
||||
InnerBlockLayoutContextProvider,
|
||||
ProductDataContextProvider,
|
||||
} from '@woocommerce/shared-context';
|
||||
import { StoreNoticesProvider } from '@woocommerce/base-context';
|
||||
import { useStoreEvents } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -24,9 +26,17 @@ import { BLOCK_NAME } from './constants';
|
|||
* @param {React.ReactChildren} props.children
|
||||
*/
|
||||
const Block = ( { isLoading, product, children } ) => {
|
||||
const { dispatchStoreEvent } = useStoreEvents();
|
||||
const className = 'wc-block-single-product wc-block-layout';
|
||||
const noticeContext = `woocommerce/single-product/${ product?.id || 0 }`;
|
||||
|
||||
useEffect( () => {
|
||||
dispatchStoreEvent( 'product-render', {
|
||||
product,
|
||||
listName: BLOCK_NAME,
|
||||
} );
|
||||
}, [ product, dispatchStoreEvent ] );
|
||||
|
||||
return (
|
||||
<InnerBlockLayoutContextProvider
|
||||
parentName={ BLOCK_NAME }
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { addAction } from '@wordpress/hooks';
|
||||
import type { ProductResponseItem, CartResponseItem } from '@woocommerce/types';
|
||||
import type {
|
||||
ProductResponseItem,
|
||||
CartResponseItem,
|
||||
StoreCart,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -13,89 +17,288 @@ import {
|
|||
getProductFieldObject,
|
||||
getProductImpressionObject,
|
||||
trackEvent,
|
||||
trackCheckoutStep,
|
||||
trackCheckoutOption,
|
||||
} from './utils';
|
||||
|
||||
const trackListProducts = ( {
|
||||
products,
|
||||
listName = __( 'Product List', 'woo-gutenberg-products-block' ),
|
||||
}: {
|
||||
products: Array< ProductResponseItem >;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
trackEvent( 'view_item_list', {
|
||||
event_category: 'engagement',
|
||||
event_label: __( 'Viewing products', 'woo-gutenberg-products-block' ),
|
||||
items: products.map( ( product, index ) => ( {
|
||||
...getProductImpressionObject( product, listName ),
|
||||
list_position: index + 1,
|
||||
} ) ),
|
||||
} );
|
||||
};
|
||||
/**
|
||||
* Track customer progress through steps of the checkout. Triggers the event when the step changes:
|
||||
* 1 - Contact information
|
||||
* 2 - Shipping address
|
||||
* 3 - Billing address
|
||||
* 4 - Shipping options
|
||||
* 5 - Payment options
|
||||
*
|
||||
* @summary Track checkout progress with begin_checkout and checkout_progress
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#1_measure_checkout_steps
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-render-checkout-form`,
|
||||
namespace,
|
||||
trackCheckoutStep( 0 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-email-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 1 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-shipping-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 2 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-billing-address`,
|
||||
namespace,
|
||||
trackCheckoutStep( 3 )
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-phone-number`,
|
||||
namespace,
|
||||
( { step, ...rest }: { step: string; storeCart: StoreCart } ): void => {
|
||||
trackCheckoutStep( step === 'shipping' ? 2 : 3 )( rest );
|
||||
}
|
||||
);
|
||||
|
||||
const trackAddToCart = ( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'add_to_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __( 'Add to Cart', 'woo-gutenberg-products-block' ),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
};
|
||||
/**
|
||||
* Choose a shipping rate
|
||||
*
|
||||
* @summary Track the shipping rate being set using set_checkout_option
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-selected-shipping-rate`,
|
||||
namespace,
|
||||
( { shippingRateId }: { shippingRateId: string } ): void => {
|
||||
trackCheckoutOption( {
|
||||
step: 4,
|
||||
option: __( 'Shipping Method', 'woo-gutenberg-products-block' ),
|
||||
value: shippingRateId,
|
||||
} )();
|
||||
}
|
||||
);
|
||||
|
||||
const trackRemoveCartItem = ( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'remove_from_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __( 'Remove Cart Item', 'woo-gutenberg-products-block' ),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
};
|
||||
/**
|
||||
* Choose a payment method
|
||||
*
|
||||
* @summary Track the payment method being set using set_checkout_option
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#2_measure_checkout_options
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-checkout-set-active-payment-method`,
|
||||
namespace,
|
||||
( { paymentMethodSlug }: { paymentMethodSlug: string } ): void => {
|
||||
trackCheckoutOption( {
|
||||
step: 5,
|
||||
option: __( 'Payment Method', 'woo-gutenberg-products-block' ),
|
||||
value: paymentMethodSlug,
|
||||
} )();
|
||||
}
|
||||
);
|
||||
|
||||
const trackChangeCartItemQuantity = ( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'change_cart_quantity', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __(
|
||||
'Change Cart Item Quantity',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
};
|
||||
/**
|
||||
* Add Payment Information
|
||||
*
|
||||
* This event signifies a user has submitted their payment information. Note, this is used to indicate checkout
|
||||
* submission, not `purchase` which is triggered on the thanks page.
|
||||
*
|
||||
* @summary Track the add_payment_info event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#add_payment_info
|
||||
*/
|
||||
addAction( `${ actionPrefix }-checkout-submit`, namespace, (): void => {
|
||||
trackEvent( 'add_payment_info' );
|
||||
} );
|
||||
|
||||
function initialize() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log( `Google Analytics Tracking initialized` );
|
||||
addAction(
|
||||
`${ actionPrefix }-list-products`,
|
||||
namespace,
|
||||
trackListProducts
|
||||
);
|
||||
addAction( `${ actionPrefix }-add-cart-item`, namespace, trackAddToCart );
|
||||
addAction(
|
||||
`${ actionPrefix }-set-cart-item-quantity`,
|
||||
namespace,
|
||||
trackChangeCartItemQuantity
|
||||
);
|
||||
addAction(
|
||||
`${ actionPrefix }-remove-cart-item`,
|
||||
namespace,
|
||||
trackRemoveCartItem
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Add to cart.
|
||||
*
|
||||
* This event signifies that an item was added to a cart for purchase.
|
||||
*
|
||||
* @summary Track the add_to_cart event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#add_to_cart
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-add-item`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'add_to_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __( 'Add to Cart', 'woo-gutenberg-products-block' ),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
initialize();
|
||||
/**
|
||||
* Remove item from the cart
|
||||
*
|
||||
* @summary Track the remove_from_cart event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#remove_from_cart
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-remove-item`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'remove_from_cart', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __(
|
||||
'Remove Cart Item',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Change cart item quantities
|
||||
*
|
||||
* @summary Custom change_cart_quantity event.
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-cart-set-item-quantity`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
quantity = 1,
|
||||
}: {
|
||||
product: CartResponseItem;
|
||||
quantity: number;
|
||||
} ): void => {
|
||||
trackEvent( 'change_cart_quantity', {
|
||||
event_category: 'ecommerce',
|
||||
event_label: __(
|
||||
'Change Cart Item Quantity',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: [ getProductFieldObject( product, quantity ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product List View
|
||||
*
|
||||
* @summary Track the view_item_list event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#view_item_list
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-list-render`,
|
||||
namespace,
|
||||
( {
|
||||
products,
|
||||
listName = __( 'Product List', 'woo-gutenberg-products-block' ),
|
||||
}: {
|
||||
products: Array< ProductResponseItem >;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
if ( products.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
trackEvent( 'view_item_list', {
|
||||
event_category: 'engagement',
|
||||
event_label: __(
|
||||
'Viewing products',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
items: products.map( ( product, index ) => ( {
|
||||
...getProductImpressionObject( product, listName ),
|
||||
list_position: index + 1,
|
||||
} ) ),
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product View Link Clicked
|
||||
*
|
||||
* @summary Track the select_content event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#select_content
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-view-link`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
listName,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
trackEvent( 'select_content', {
|
||||
content_type: 'product',
|
||||
items: [ getProductImpressionObject( product, listName ) ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product Search
|
||||
*
|
||||
* @summary Track the search event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#search
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-search`,
|
||||
namespace,
|
||||
( { searchTerm }: { searchTerm: string } ): void => {
|
||||
trackEvent( 'search', {
|
||||
search_term: searchTerm,
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Single Product View
|
||||
*
|
||||
* @summary Track the view_item event
|
||||
* @see https://developers.google.com/gtagjs/reference/ga4-events#view_item
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-product-render`,
|
||||
namespace,
|
||||
( {
|
||||
product,
|
||||
listName,
|
||||
}: {
|
||||
product: ProductResponseItem;
|
||||
listName: string;
|
||||
} ): void => {
|
||||
if ( product ) {
|
||||
trackEvent( 'view_item', {
|
||||
items: [ getProductImpressionObject( product, listName ) ],
|
||||
} );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Track notices as Exception events.
|
||||
*
|
||||
* @summary Track the exception event
|
||||
* @see https://developers.google.com/analytics/devguides/collection/gtagjs/exceptions
|
||||
*/
|
||||
addAction(
|
||||
`${ actionPrefix }-store-notice-create`,
|
||||
namespace,
|
||||
( { status, content }: { status: string; content: string } ): void => {
|
||||
if ( status === 'error' ) {
|
||||
trackEvent( 'exception', {
|
||||
description: content,
|
||||
fatal: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"compilerOptions": {
|
||||
"types": [ "gtag.js" ]
|
||||
},
|
||||
"include": [ ".", "../../type-defs/**.ts", "../../mapped-types.ts" ]
|
||||
"include": [ ".", "../../type-defs", "../../mapped-types.ts" ]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { ProductResponseItem, CartResponseItem } from '@woocommerce/types';
|
||||
import type {
|
||||
ProductResponseItem,
|
||||
CartResponseItem,
|
||||
StoreCart,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
interface ImpressionItem extends Gtag.Item {
|
||||
// eslint-disable-next-line camelcase
|
||||
|
@ -73,3 +77,43 @@ export const trackEvent = (
|
|||
console.log( `Tracking event ${ eventName }` );
|
||||
window.gtag( 'event', eventName, eventParams );
|
||||
};
|
||||
|
||||
let currentStep = -1;
|
||||
|
||||
export const trackCheckoutStep = ( step: number ) => ( {
|
||||
storeCart,
|
||||
}: {
|
||||
storeCart: StoreCart;
|
||||
} ): void => {
|
||||
if ( currentStep === step ) {
|
||||
return;
|
||||
}
|
||||
trackEvent( step === 0 ? 'begin_checkout' : 'checkout_progress', {
|
||||
items: storeCart.cartItems.map( getProductFieldObject ),
|
||||
coupon: storeCart.cartCoupons[ 0 ]?.code || '',
|
||||
currency: storeCart.cartTotals.currency_code,
|
||||
value: (
|
||||
parseInt( storeCart.cartTotals.total_price, 10 ) /
|
||||
10 ** storeCart.cartTotals.currency_minor_unit
|
||||
).toString(),
|
||||
checkout_step: step,
|
||||
} );
|
||||
currentStep = step;
|
||||
};
|
||||
|
||||
export const trackCheckoutOption = ( {
|
||||
step,
|
||||
option,
|
||||
value,
|
||||
}: {
|
||||
step: number;
|
||||
option: string;
|
||||
value: string;
|
||||
} ) => (): void => {
|
||||
trackEvent( 'set_checkout_option', {
|
||||
checkout_step: step,
|
||||
checkout_option: option,
|
||||
value,
|
||||
} );
|
||||
currentStep = step;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,6 @@ class Assets {
|
|||
add_action( 'woocommerce_login_form_end', array( __CLASS__, 'redirect_to_field' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
|
||||
add_filter( 'script_loader_tag', array( __CLASS__, 'async_script_loader_tags' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,10 +54,6 @@ class Assets {
|
|||
$asset_api->register_script( 'wc-shared-hocs', 'build/wc-shared-hocs.js', [], false );
|
||||
$asset_api->register_script( 'wc-price-format', 'build/price-format.js', [], false );
|
||||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
$asset_api->register_script( 'wc-blocks-google-analytics', 'build/wc-blocks-google-analytics.js', [ 'google-tag-manager' ] );
|
||||
}
|
||||
|
||||
if ( Package::feature()->is_feature_plugin_build() ) {
|
||||
$asset_api->register_script( 'wc-blocks-checkout', 'build/blocks-checkout.js', [], false );
|
||||
}
|
||||
|
@ -73,49 +68,6 @@ class Assets {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings from the GA integration extension.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_google_analytics_settings() {
|
||||
return wp_parse_args(
|
||||
get_option( 'woocommerce_google_analytics_settings' ),
|
||||
[
|
||||
'ga_id' => '',
|
||||
'ga_event_tracking_enabled' => 'no',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Google Tag Manager script if prerequisites are met.
|
||||
*
|
||||
* @param AssetApi $asset_api Asset API class Instance.
|
||||
*/
|
||||
private static function maybe_enqueue_google_analytics( $asset_api ) {
|
||||
$settings = self::get_google_analytics_settings();
|
||||
|
||||
if ( is_admin() || ! stristr( $settings['ga_id'], 'G-' ) || apply_filters( 'woocommerce_ga_disable_tracking', ! wc_string_to_bool( $settings['ga_event_tracking_enabled'] ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) {
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false );
|
||||
wp_add_inline_script(
|
||||
'google-tag-manager',
|
||||
"
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '" . esc_js( $settings['ga_id'] ) . "', { 'send_page_view': false });"
|
||||
);
|
||||
}
|
||||
|
||||
wp_enqueue_script( 'wc-blocks-google-analytics' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the vendors style file. We need to do it after the other files
|
||||
* because we need to check if `wp-edit-post` has been enqueued.
|
||||
|
@ -132,10 +84,6 @@ class Assets {
|
|||
$block_style_dependencies = array();
|
||||
}
|
||||
self::register_style( 'wc-block-vendors-style', plugins_url( $asset_api->get_block_asset_build_path( 'vendors-style', 'css' ), __DIR__ ), $block_style_dependencies );
|
||||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
self::maybe_enqueue_google_analytics( $asset_api );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,23 +282,4 @@ class Assets {
|
|||
$ver = self::get_file_version( $filename );
|
||||
wp_register_style( $handle, $src, $deps, $ver, $media );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add async to script tags with defined handles.
|
||||
*
|
||||
* @param string $tag HTML for the script tag.
|
||||
* @param string $handle Handle of script.
|
||||
* @param string $src Src of script.
|
||||
* @return string
|
||||
*/
|
||||
public static function async_script_loader_tags( $tag, $handle, $src ) {
|
||||
if ( ! in_array( $handle, array( 'google-tag-manager' ), true ) ) {
|
||||
return $tag;
|
||||
}
|
||||
// If script was output manually in wp_head, abort.
|
||||
if ( did_action( 'woocommerce_gtag_snippet' ) ) {
|
||||
return '';
|
||||
}
|
||||
return str_replace( '<script src', '<script async src', $tag );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,12 @@ use Exception;
|
|||
* @since 2.5.0
|
||||
*/
|
||||
class Api {
|
||||
/**
|
||||
* Stores inline scripts already enqueued.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $inline_scripts = [];
|
||||
|
||||
/**
|
||||
* Reference to the Package instance
|
||||
|
@ -168,4 +174,24 @@ class Api {
|
|||
: '-legacy';
|
||||
return "build/$filename$suffix.$type";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an inline script, once.
|
||||
*
|
||||
* @param string $handle Script handle.
|
||||
* @param string $script Script contents.
|
||||
*/
|
||||
public function add_inline_script( $handle, $script ) {
|
||||
if ( ! empty( $this->inline_scripts[ $handle ] ) && in_array( $script, $this->inline_scripts[ $handle ], true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_add_inline_script( $handle, $script );
|
||||
|
||||
if ( isset( $this->inline_scripts[ $handle ] ) ) {
|
||||
$this->inline_scripts[ $handle ][] = $script;
|
||||
} else {
|
||||
$this->inline_scripts[ $handle ] = array( $script );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlocksWpQuery;
|
||||
use Automattic\WooCommerce\Blocks\StoreApi\SchemaController;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* AbstractProductGrid class.
|
||||
|
@ -62,16 +64,51 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
|
|||
$this->attributes = $this->parse_attributes( $attributes );
|
||||
$this->content = $content;
|
||||
$this->query_args = $this->parse_query_args();
|
||||
$products = $this->get_products();
|
||||
$products = array_filter( array_map( 'wc_get_product', $this->get_products() ) );
|
||||
|
||||
if ( ! $products ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$classes = $this->get_container_classes();
|
||||
$output = implode( '', array_map( array( $this, 'render_product' ), $products ) );
|
||||
/**
|
||||
* Product List Render event.
|
||||
*
|
||||
* Fires a WP Hook named `experimental__woocommerce_blocks-product-list-render` on render so that the client
|
||||
* can add event handling when certain products are displayed. This can be used by tracking extensions such
|
||||
* as Google Analytics to track impressions.
|
||||
*
|
||||
* Provides the list of product data (shaped like the Store API responses) and the block name.
|
||||
*/
|
||||
$this->asset_api->add_inline_script(
|
||||
'wp-hooks',
|
||||
'
|
||||
window.addEventListener( "DOMContentLoaded", () => {
|
||||
wp.hooks.doAction(
|
||||
"experimental__woocommerce_blocks-product-list-render",
|
||||
{
|
||||
products: JSON.parse( decodeURIComponent( "' . esc_js(
|
||||
rawurlencode(
|
||||
wp_json_encode(
|
||||
array_map(
|
||||
[ Package::container()->get( SchemaController::class )->get( 'product' ), 'get_item_response' ],
|
||||
$products
|
||||
)
|
||||
)
|
||||
)
|
||||
) . '" ) ),
|
||||
listName: "' . esc_js( $this->block_name ) . '"
|
||||
}
|
||||
);
|
||||
} );
|
||||
',
|
||||
'after'
|
||||
);
|
||||
|
||||
return sprintf( '<div class="%s"><ul class="wc-block-grid__products">%s</ul></div>', esc_attr( $classes ), $output );
|
||||
return sprintf(
|
||||
'<div class="%s"><ul class="wc-block-grid__products">%s</ul></div>',
|
||||
esc_attr( $this->get_container_classes() ),
|
||||
implode( '', array_map( array( $this, 'render_product' ), $products ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -305,16 +342,10 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
|
|||
/**
|
||||
* Render a single products.
|
||||
*
|
||||
* @param int $id Product ID.
|
||||
* @param \WC_Product $product Product object.
|
||||
* @return string Rendered product output.
|
||||
*/
|
||||
protected function render_product( $id ) {
|
||||
$product = wc_get_product( $id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function render_product( $product ) {
|
||||
$data = (object) array(
|
||||
'permalink' => esc_url( $product->get_permalink() ),
|
||||
'image' => $this->get_image_html( $product ),
|
||||
|
|
|
@ -22,4 +22,41 @@ class ProductSearch extends AbstractBlock {
|
|||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content ) {
|
||||
/**
|
||||
* Product Search event.
|
||||
*
|
||||
* Listens for product search form submission, and on submission fires a WP Hook named
|
||||
* `experimental__woocommerce_blocks-product-search`. This can be used by tracking extensions such as Google
|
||||
* Analytics to track searches.
|
||||
*/
|
||||
$this->asset_api->add_inline_script(
|
||||
'wp-hooks',
|
||||
"
|
||||
window.addEventListener( 'DOMContentLoaded', () => {
|
||||
const forms = document.querySelectorAll( '.wc-block-product-search form' );
|
||||
|
||||
for ( const form of forms ) {
|
||||
form.addEventListener( 'submit', ( event ) => {
|
||||
const field = form.querySelector( '.wc-block-product-search__field' );
|
||||
|
||||
if ( field && field.value ) {
|
||||
wp.hooks.doAction( 'experimental__woocommerce_blocks-product-search', { event: event, searchTerm: field.value } );
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
",
|
||||
'after'
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ use Automattic\WooCommerce\Blocks\StoreApi\Formatters\HtmlFormatter;
|
|||
use Automattic\WooCommerce\Blocks\StoreApi\Formatters\CurrencyFormatter;
|
||||
use Automattic\WooCommerce\Blocks\StoreApi\RoutesController;
|
||||
use Automattic\WooCommerce\Blocks\StoreApi\SchemaController;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
|
||||
|
||||
/**
|
||||
* Takes care of bootstrapping the plugin.
|
||||
|
@ -88,6 +89,7 @@ class Bootstrap {
|
|||
$this->container->get( ExtendRestApi::class );
|
||||
$this->container->get( PaymentsApi::class );
|
||||
$this->container->get( RestApi::class );
|
||||
$this->container->get( GoogleAnalytics::class );
|
||||
Library::init();
|
||||
}
|
||||
|
||||
|
@ -227,6 +229,17 @@ class Bootstrap {
|
|||
return new ExtendRestApi( $container->get( Package::class ), $container->get( Formatters::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
GoogleAnalytics::class,
|
||||
function( Container $container ) {
|
||||
// Require Google Analytics Integration to be activated.
|
||||
if ( ! class_exists( 'WC_Google_Analytics_Integration' ) ) {
|
||||
return;
|
||||
}
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new GoogleAnalytics( $asset_api );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
|
||||
|
||||
/**
|
||||
* Service class to integrate Blocks with the Google Analytics extension,
|
||||
*/
|
||||
class GoogleAnalytics {
|
||||
/**
|
||||
* Instance of the asset API.
|
||||
*
|
||||
* @var AssetApi
|
||||
*/
|
||||
protected $asset_api;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param AssetApi $asset_api Instance of the asset API.
|
||||
*/
|
||||
public function __construct( AssetApi $asset_api ) {
|
||||
if ( ! Package::feature()->is_experimental_build() ) {
|
||||
return;
|
||||
}
|
||||
$this->asset_api = $asset_api;
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WP.
|
||||
*/
|
||||
protected function init() {
|
||||
add_action( 'init', array( $this, 'register_assets' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register scripts.
|
||||
*/
|
||||
public function register_assets() {
|
||||
$this->asset_api->register_script( 'wc-blocks-google-analytics', 'build/wc-blocks-google-analytics.js', [ 'google-tag-manager' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Google Tag Manager script if prerequisites are met.
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
$settings = $this->get_google_analytics_settings();
|
||||
|
||||
// Require tracking to be enabled with a valid GA ID.
|
||||
if ( ! stristr( $settings['ga_id'], 'G-' ) || apply_filters( 'woocommerce_ga_disable_tracking', ! wc_string_to_bool( $settings['ga_event_tracking_enabled'] ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) {
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false );
|
||||
wp_add_inline_script(
|
||||
'google-tag-manager',
|
||||
"
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '" . esc_js( $settings['ga_id'] ) . "', { 'send_page_view': false });"
|
||||
);
|
||||
}
|
||||
wp_enqueue_script( 'wc-blocks-google-analytics' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings from the GA integration extension.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_google_analytics_settings() {
|
||||
return wp_parse_args(
|
||||
get_option( 'woocommerce_google_analytics_settings' ),
|
||||
[
|
||||
'ga_id' => '',
|
||||
'ga_event_tracking_enabled' => 'no',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add async to script tags with defined handles.
|
||||
*
|
||||
* @param string $tag HTML for the script tag.
|
||||
* @param string $handle Handle of script.
|
||||
* @param string $src Src of script.
|
||||
* @return string
|
||||
*/
|
||||
public function async_script_loader_tags( $tag, $handle, $src ) {
|
||||
if ( ! in_array( $handle, array( 'google-tag-manager' ), true ) ) {
|
||||
return $tag;
|
||||
}
|
||||
// If script was output manually in wp_head, abort.
|
||||
if ( did_action( 'woocommerce_gtag_snippet' ) ) {
|
||||
return '';
|
||||
}
|
||||
return str_replace( '<script src', '<script async src', $tag );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue