2022-07-05 05:02:52 +00:00
/ * *
* External dependencies
* /
import { TourKit , TourKitTypes } from '@woocommerce/components' ;
import { __ } from '@wordpress/i18n' ;
import { useDispatch , useSelect } from '@wordpress/data' ;
2022-07-11 09:17:02 +00:00
import {
useLayoutEffect ,
useEffect ,
useState ,
useRef ,
createPortal ,
} from '@wordpress/element' ;
2022-07-05 05:02:52 +00:00
import { OPTIONS_STORE_NAME , PLUGINS_STORE_NAME } from '@woocommerce/data' ;
const REVIEWED_DEFAULTS_OPTION =
'woocommerce_admin_reviewed_default_shipping_zones' ;
const CREATED_DEFAULTS_OPTION =
'woocommerce_admin_created_default_shipping_zones' ;
2022-07-11 09:17:02 +00:00
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' ;
2022-07-05 05:02:52 +00:00
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 :
! isLoading &&
hasCreatedDefaultShippingZones &&
! hasReviewedDefaultShippingOptions ,
} ;
} ;
2022-07-11 09:17:02 +00:00
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
'tr.wc-shipping-zone-worldwide > td.wc-shipping-zone-region' ,
] ,
[
// selectors for rightmost column (shipping methods)
'th.wc-shipping-zone-methods' ,
'tr.wc-shipping-zone-worldwide > 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
) ;
} ;
2022-07-05 05:02:52 +00:00
export const ShippingTour = ( ) = > {
const { updateOptions } = useDispatch ( OPTIONS_STORE_NAME ) ;
const { show : showTour } = useShowShippingTour ( ) ;
const activePlugins = useSelect ( ( select ) = >
select ( PLUGINS_STORE_NAME ) . getActivePlugins ( )
) ;
2022-07-11 09:17:02 +00:00
const [ step , setStepNumber ] = useState ( 0 ) ;
2022-07-05 05:02:52 +00:00
const tourConfig : TourKitTypes.WooConfig = {
placement : 'auto' ,
2022-07-11 09:17:02 +00:00
options : {
effects : {
spotlight : {
interactivity : {
enabled : false ,
} ,
} ,
liveResize : {
mutation : true ,
resize : true ,
} ,
} ,
callbacks : {
onNextStep : ( currentStepIndex ) = >
setStepNumber ( currentStepIndex + 1 ) ,
onPreviousStep : ( currentStepIndex ) = >
setStepNumber ( currentStepIndex - 1 ) ,
} ,
} ,
2022-07-05 05:02:52 +00:00
steps : [
{
referenceElements : {
2022-07-11 09:17:02 +00:00
desktop : ` . ${ FLOATER_CLASS } ` ,
2022-07-05 05:02:52 +00:00
} ,
meta : {
name : 'shipping-zones' ,
heading : __ ( 'Shipping zones' , 'woocommerce' ) ,
descriptions : {
desktop : (
< >
< span >
{ __ (
'We added a few shipping zones for you based on your location, but you can manage them at any time.' ,
'woocommerce'
) }
< / span >
< br / >
< span >
{ __ (
2022-07-13 15:41:22 +00:00
'A shipping zone is a geographic area where a certain set of shipping methods are offered.' ,
2022-07-05 05:02:52 +00:00
'woocommerce'
) }
< / span >
< / >
) ,
} ,
} ,
} ,
{
referenceElements : {
2022-07-11 09:17:02 +00:00
desktop : ` . ${ FLOATER_CLASS } ` ,
2022-07-05 05:02:52 +00:00
} ,
meta : {
name : 'shipping-methods' ,
heading : __ ( 'Shipping methods' , 'woocommerce' ) ,
descriptions : {
desktop : __ (
2022-07-13 15:41:22 +00:00
'We defaulted to some recommended shipping methods based on your store location, but you can manage them at any time within each shipping zone settings.' ,
2022-07-05 05:02:52 +00:00
'woocommerce'
) ,
} ,
} ,
} ,
] ,
closeHandler : ( ) = > {
updateOptions ( {
[ REVIEWED_DEFAULTS_OPTION ] : 'yes' ,
} ) ;
} ,
} ;
const isWcsSectionPresent = document . querySelector ( WCS_LINK_SELECTOR ) ;
const isShippingRecommendationsPresent = ! activePlugins . includes (
'woocommerce-services'
) ;
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 WooCommerce dashboard and save on shipping thanks to discounted rates. You can manage WooCommerce Shipping in this section.' ,
'woocommerce'
) ,
} ,
} ,
} ) ;
}
if ( isShippingRecommendationsPresent ) {
tourConfig . steps . push ( {
referenceElements : {
desktop : SHIPPING_RECOMMENDATIONS_SELECTOR ,
} ,
meta : {
name : 'shipping-recommendations' ,
2022-07-13 15:41:22 +00:00
heading : __ ( 'WooCommerce Shipping' , 'woocommerce' ) ,
2022-07-05 05:02:52 +00:00
descriptions : {
desktop : __ (
'If you’ d like to speed up your process and print your shipping label straight from your WooCommerce dashboard, WooCommerce Shipping may be for you! ' ,
'woocommerce'
) ,
} ,
} ,
} ) ;
}
if ( showTour ) {
return (
< div >
2022-07-11 09:17:02 +00:00
< TourFloaterWrapper step = { step } / >
2022-07-05 05:02:52 +00:00
< TourKit config = { tourConfig } > < / TourKit >
< / div >
) ;
}
return null ;
} ;