woocommerce/plugins/woocommerce-admin/client/guided-tours/shipping-tour.tsx

382 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* External dependencies
*/
import { TourKit, TourKitTypes } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import {
useLayoutEffect,
useEffect,
useState,
useRef,
createPortal,
} from '@wordpress/element';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
const REVIEWED_DEFAULTS_OPTION =
'woocommerce_admin_reviewed_default_shipping_zones';
const CREATED_DEFAULTS_OPTION =
'woocommerce_admin_created_default_shipping_zones';
const FLOATER_WRAPPER_CLASS =
'woocommerce-settings-shipping-tour-floater-wrapper';
const FLOATER_CLASS =
'woocommerce-settings-smart-defaults-shipping-tour-floater';
const SHIPPING_ZONES_SETTINGS_TABLE_CLASS = 'table.wc-shipping-zones';
const WCS_LINK_SELECTOR = 'a[href*="woocommerce-services-settings"]';
const SHIPPING_RECOMMENDATIONS_SELECTOR =
'div.woocommerce-recommended-shipping-extensions';
const useShowShippingTour = () => {
const {
hasCreatedDefaultShippingZones,
hasReviewedDefaultShippingOptions,
isLoading,
} = useSelect( ( select ) => {
const { hasFinishedResolution, getOption } =
select( OPTIONS_STORE_NAME );
return {
isLoading:
! hasFinishedResolution( 'getOption', [
CREATED_DEFAULTS_OPTION,
] ) &&
! hasFinishedResolution( 'getOption', [
REVIEWED_DEFAULTS_OPTION,
] ),
hasCreatedDefaultShippingZones:
getOption( CREATED_DEFAULTS_OPTION ) === 'yes',
hasReviewedDefaultShippingOptions:
getOption( REVIEWED_DEFAULTS_OPTION ) === 'yes',
};
} );
return {
isLoading,
show:
window.wcAdminFeatures[ 'shipping-setting-tour' ] &&
! isLoading &&
hasCreatedDefaultShippingZones &&
! hasReviewedDefaultShippingOptions,
};
};
type NonEmptySelectorArray = readonly [ string, ...string[] ];
const computeDims = ( elementsSelectors: NonEmptySelectorArray ) => {
const rects = elementsSelectors.map( ( elementSelector ) => {
const rect = document
?.querySelector( elementSelector )
?.getBoundingClientRect();
if ( ! rect ) {
throw new Error(
"Shipping tour: Couldn't find element with selector: " +
elementSelector
);
}
return rect;
} );
const originCoords = document
.querySelector( `.${ FLOATER_WRAPPER_CLASS }` )
?.getBoundingClientRect() || { top: 0, left: 0 };
const top = Math.min( ...rects.map( ( rect ) => rect.top ) );
const left = Math.min( ...rects.map( ( rect ) => rect.left ) );
const right = Math.max( ...rects.map( ( rect ) => rect.right ) );
const bottom = Math.max( ...rects.map( ( rect ) => rect.bottom ) );
const width = right - left;
const height = bottom - top;
// offset top and left from origin
const topOffset = top - originCoords.top;
const leftOffset = left - originCoords.left;
return { left: leftOffset, top: topOffset, width, height };
};
const TourFloater = ( { dims }: { dims: Partial< DOMRect > } ) => {
return (
<div
style={ {
position: 'relative',
pointerEvents: 'none',
...dims,
} }
className={ FLOATER_CLASS }
></div>
);
};
// this is defines the elements to be spotlit for each step
const spotlitElementsSelectors: Array< NonEmptySelectorArray > = [
[
// just use bottom right element and top left element instead of all rects
// top left = table header cell for sort handles
'th.wc-shipping-zone-sort',
// bottom right = worldwide region cell
'tfoot.wc-shipping-zone-worldwide tr > td.wc-shipping-zone-region',
],
[
// selectors for rightmost column (shipping methods)
'th.wc-shipping-zone-methods',
'tfoot.wc-shipping-zone-worldwide tr > td.wc-shipping-zone-methods',
],
];
const TourFloaterWrapper = ( { step }: { step: number } ) => {
const thisRef = useRef< HTMLDivElement >( null );
useLayoutEffect( () => {
// this moves the element to the correct place which is right before the table element
if ( thisRef.current?.parentElement ) {
thisRef.current.parentElement.insertBefore(
thisRef.current,
document.querySelector( 'table.wc-shipping-zones' )
);
}
}, [] );
const currentStepSelectors =
spotlitElementsSelectors[ step ] ??
spotlitElementsSelectors[ spotlitElementsSelectors.length - 1 ];
const [ dims, setDims ] = useState( computeDims( currentStepSelectors ) );
useEffect( () => {
setDims( computeDims( currentStepSelectors ) );
const observer = new ResizeObserver( () => {
setDims( computeDims( currentStepSelectors ) );
} );
const shippingSettingsTableElement = document.querySelector(
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
);
if ( ! shippingSettingsTableElement ) {
throw new Error(
"Shipping tour: Couldn't find shipping settings table element with selector: " +
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
);
}
observer.observe( shippingSettingsTableElement );
return () => {
observer.disconnect();
};
}, [ currentStepSelectors ] );
const shippingSettingsTableParentElement = document.querySelector(
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
)?.parentElement;
if ( ! shippingSettingsTableParentElement ) {
throw new Error(
"Shipping tour: Couldn't find shipping settings table parent element with selector: " +
SHIPPING_ZONES_SETTINGS_TABLE_CLASS
);
}
/**
* use ReactDOM.createPortal to inject our element into non-React controlled DOM
* unfortunately createPortal uses .appendChild which puts it in the wrong place,
* we want it to be before the table
*/
return createPortal(
<div
ref={ thisRef }
className={ FLOATER_WRAPPER_CLASS }
style={ { position: 'absolute' } }
>
{ <TourFloater dims={ dims } /> }
</div>,
shippingSettingsTableParentElement
);
};
export const ShippingTour: React.FC< {
showShippingRecommendationsStep: boolean;
} > = ( { showShippingRecommendationsStep } ) => {
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { show: showTour } = useShowShippingTour();
const [ step, setStepNumber ] = useState( 0 );
const { createNotice } = useDispatch( 'core/notices' );
const tourConfig: TourKitTypes.WooConfig = {
placement: 'auto',
options: {
effects: {
spotlight: {
interactivity: {
enabled: false,
},
},
liveResize: {
mutation: true,
resize: true,
},
},
callbacks: {
onNextStep: ( currentStepIndex ) => {
setStepNumber( currentStepIndex + 1 );
recordEvent( 'walkthrough_settings_shipping_next_click', {
step_name:
tourConfig.steps[ currentStepIndex ].meta.name,
} );
},
onPreviousStep: ( currentStepIndex ) => {
setStepNumber( currentStepIndex - 1 );
recordEvent( 'walkthrough_settings_shipping_back_click', {
step_name:
tourConfig.steps[ currentStepIndex ].meta.name,
} );
},
},
},
steps: [
{
referenceElements: {
desktop: `.${ FLOATER_CLASS }`,
},
meta: {
name: 'shipping-zones',
heading: __( 'Shipping zones', 'woocommerce' ),
descriptions: {
desktop: (
<>
<span>
{ __(
"Specify the areas you'd like to ship to! Give each zone a name, then list the regions you'd like to include. Your regions can be as specific as a zip code or as broad as a country. Shoppers will only see the methods available in their region.",
'woocommerce'
) }
</span>
<br />
<span>
{ __(
"We've added some shipping zones to get you started — you can manage them by selecting Edit or Delete.",
'woocommerce'
) }
</span>
</>
),
},
},
},
{
referenceElements: {
desktop: `.${ FLOATER_CLASS }`,
},
meta: {
name: 'shipping-methods',
heading: __( 'Shipping methods', 'woocommerce' ),
descriptions: {
desktop: (
<>
<span>
{ __(
"Add one or more shipping methods you'd like to offer to shoppers in your zones.",
'woocommerce'
) }
</span>
<br />
<span>
{ __(
"For example, we've added the “Free shipping” method for shoppers in your country. You can edit, add to, or remove shipping methods by selecting Edit or Delete.",
'woocommerce'
) }
</span>
</>
),
},
},
},
],
closeHandler: async ( steps, stepIndex, source ) => {
const update = await updateOptions( {
[ REVIEWED_DEFAULTS_OPTION ]: 'yes',
} );
if ( ! update.success ) {
createNotice(
'error',
__(
'There was a problem marking the shipping tour as completed.',
'woocommerce'
)
);
recordEvent(
'walkthrough_settings_shipping_updated_option_error'
);
}
if ( source === 'close-btn' ) {
recordEvent( 'walkthrough_settings_shipping_dismissed', {
step_name: steps[ stepIndex ].meta.name,
} );
} else if ( source === 'done-btn' ) {
recordEvent( 'walkthrough_settings_shipping_completed' );
}
},
};
const isWcsSectionPresent = document.querySelector( WCS_LINK_SELECTOR );
if ( isWcsSectionPresent ) {
tourConfig.steps.push( {
referenceElements: {
desktop: WCS_LINK_SELECTOR,
},
meta: {
name: 'woocommerce-shipping',
heading: __( 'WooCommerce Shipping', 'woocommerce' ),
descriptions: {
desktop: __(
'Print USPS and DHL labels straight from your Woo dashboard and save on shipping thanks to discounted rates. You can manage WooCommerce Shipping in this section.',
'woocommerce'
),
},
},
} );
}
if ( showShippingRecommendationsStep ) {
tourConfig.steps.push( {
referenceElements: {
desktop: SHIPPING_RECOMMENDATIONS_SELECTOR,
},
meta: {
name: 'shipping-recommendations',
heading: __( 'WooCommerce Shipping', 'woocommerce' ),
descriptions: {
desktop: __(
'If youd like to speed up your process and print your shipping label straight from your Woo dashboard, WooCommerce Shipping may be for you! ',
'woocommerce'
),
},
},
} );
}
useEffect( () => {
if ( showTour ) {
recordEvent( 'walkthrough_settings_shipping_view' );
}
}, [ showTour ] );
if ( showTour ) {
return (
<div>
<TourFloaterWrapper step={ step } />
<TourKit config={ tourConfig }></TourKit>
</div>
);
}
return null;
};