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-29 07:34:00 +00:00
import { OPTIONS_STORE_NAME } from '@woocommerce/data' ;
2022-09-08 04:39:53 +00:00
import { recordEvent } from '@woocommerce/tracks' ;
2024-02-20 05:33:01 +00:00
/ * *
* Internal dependencies
* /
import { getCountryCode } from '~/dashboard/utils' ;
2022-07-05 05:02:52 +00:00
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 ,
2024-02-20 05:33:01 +00:00
businessCountry ,
2022-07-05 05:02:52 +00:00
isLoading ,
} = useSelect ( ( select ) = > {
const { hasFinishedResolution , getOption } =
select ( OPTIONS_STORE_NAME ) ;
return {
isLoading :
! hasFinishedResolution ( 'getOption' , [
CREATED_DEFAULTS_OPTION ,
] ) &&
! hasFinishedResolution ( 'getOption' , [
REVIEWED_DEFAULTS_OPTION ,
2024-02-20 05:33:01 +00:00
] ) &&
! hasFinishedResolution ( 'getOption' , [
'woocommerce_default_country' ,
2022-07-05 05:02:52 +00:00
] ) ,
hasCreatedDefaultShippingZones :
getOption ( CREATED_DEFAULTS_OPTION ) === 'yes' ,
hasReviewedDefaultShippingOptions :
getOption ( REVIEWED_DEFAULTS_OPTION ) === 'yes' ,
2024-02-20 05:33:01 +00:00
businessCountry : getCountryCode (
getOption ( 'woocommerce_default_country' ) as string
) ,
2022-07-05 05:02:52 +00:00
} ;
} ) ;
return {
isLoading ,
show :
2022-08-29 09:25:24 +00:00
window . wcAdminFeatures [ 'shipping-setting-tour' ] &&
2022-07-05 05:02:52 +00:00
! isLoading &&
hasCreatedDefaultShippingZones &&
! hasReviewedDefaultShippingOptions ,
2024-02-20 05:33:01 +00:00
isUspsDhlEligible : businessCountry === 'US' ,
2022-07-05 05:02:52 +00:00
} ;
} ;
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 (
2024-08-20 09:13:17 +00:00
'Shipping tour: Couldn’ t find element with selector: ' +
2022-07-11 09:17:02 +00:00
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
2023-10-31 03:42:45 +00:00
'tfoot.wc-shipping-zone-worldwide tr > td.wc-shipping-zone-region' ,
2022-07-11 09:17:02 +00:00
] ,
[
// selectors for rightmost column (shipping methods)
'th.wc-shipping-zone-methods' ,
2023-10-31 03:42:45 +00:00
'tfoot.wc-shipping-zone-worldwide tr > td.wc-shipping-zone-methods' ,
2022-07-11 09:17:02 +00:00
] ,
] ;
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 (
2024-08-20 09:13:17 +00:00
'Shipping tour: Couldn’ t find shipping settings table element with selector: ' +
2022-07-11 09:17:02 +00:00
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 (
2024-08-20 09:13:17 +00:00
'Shipping tour: Couldn’ t find shipping settings table parent element with selector: ' +
2022-07-11 09:17:02 +00:00
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-08-29 09:25:24 +00:00
export const ShippingTour : React.FC < {
showShippingRecommendationsStep : boolean ;
} > = ( { showShippingRecommendationsStep } ) = > {
2022-07-05 05:02:52 +00:00
const { updateOptions } = useDispatch ( OPTIONS_STORE_NAME ) ;
2024-02-20 05:33:01 +00:00
const { show : showTour , isUspsDhlEligible } = useShowShippingTour ( ) ;
2022-07-11 09:17:02 +00:00
const [ step , setStepNumber ] = useState ( 0 ) ;
2023-05-10 05:01:37 +00:00
const { createNotice } = useDispatch ( 'core/notices' ) ;
2022-07-11 09:17:02 +00:00
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 ,
} ,
2024-05-09 00:56:08 +00:00
autoScroll : true ,
2022-07-11 09:17:02 +00:00
} ,
callbacks : {
2024-05-09 00:56:08 +00:00
onNextStep : ( newStepIndex ) = > {
setStepNumber ( newStepIndex ) ;
2022-09-08 04:39:53 +00:00
recordEvent ( 'walkthrough_settings_shipping_next_click' , {
step_name :
2024-05-09 00:56:08 +00:00
tourConfig . steps [ newStepIndex - 1 ] . meta . name ,
2022-09-08 04:39:53 +00:00
} ) ;
} ,
2024-05-09 00:56:08 +00:00
onPreviousStep : ( newStepIndex ) = > {
setStepNumber ( newStepIndex ) ;
2022-09-08 04:39:53 +00:00
recordEvent ( 'walkthrough_settings_shipping_back_click' , {
step_name :
2024-05-09 00:56:08 +00:00
tourConfig . steps [ newStepIndex + 1 ] . meta . name ,
2022-09-08 04:39:53 +00:00
} ) ;
} ,
2022-07-11 09:17:02 +00:00
} ,
} ,
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 >
{ __ (
2024-08-20 09:13:17 +00:00
'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.' ,
2022-07-05 05:02:52 +00:00
'woocommerce'
) }
< / span >
< br / >
< span >
{ __ (
2024-08-20 09:13:17 +00:00
'We’ ve added some shipping zones to get you started — you can manage them by selecting Edit or Delete.' ,
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 : {
2023-10-31 03:42:45 +00:00
desktop : (
< >
< span >
{ __ (
2024-08-20 09:13:17 +00:00
'Add one or more shipping methods you’ d like to offer to shoppers in your zones.' ,
2023-10-31 03:42:45 +00:00
'woocommerce'
) }
< / span >
< br / >
< span >
{ __ (
2024-08-20 09:13:17 +00:00
'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.' ,
2023-10-31 03:42:45 +00:00
'woocommerce'
) }
< / span >
< / >
2022-07-05 05:02:52 +00:00
) ,
} ,
} ,
} ,
] ,
2023-05-10 05:01:37 +00:00
closeHandler : async ( steps , stepIndex , source ) = > {
const update = await updateOptions ( {
2022-07-05 05:02:52 +00:00
[ REVIEWED_DEFAULTS_OPTION ] : 'yes' ,
} ) ;
2023-05-10 05:01:37 +00:00
if ( ! update . success ) {
createNotice (
'error' ,
__ (
'There was a problem marking the shipping tour as completed.' ,
'woocommerce'
)
) ;
recordEvent (
'walkthrough_settings_shipping_updated_option_error'
) ;
}
2022-09-08 04:39:53 +00:00
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' ) ;
}
2022-07-05 05:02:52 +00:00
} ,
} ;
const isWcsSectionPresent = document . querySelector ( WCS_LINK_SELECTOR ) ;
2024-02-20 05:33:01 +00:00
if ( isWcsSectionPresent && isUspsDhlEligible ) {
2022-07-05 05:02:52 +00:00
tourConfig . steps . push ( {
referenceElements : {
desktop : WCS_LINK_SELECTOR ,
} ,
meta : {
name : 'woocommerce-shipping' ,
heading : __ ( 'WooCommerce Shipping' , 'woocommerce' ) ,
descriptions : {
desktop : __ (
2023-10-31 03:42:45 +00:00
'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.' ,
2022-07-05 05:02:52 +00:00
'woocommerce'
) ,
} ,
} ,
} ) ;
}
2022-08-29 09:25:24 +00:00
if ( showShippingRecommendationsStep ) {
2022-07-05 05:02:52 +00:00
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 : __ (
2023-10-31 03:42:45 +00:00
'If you’ d like to speed up your process and print your shipping label straight from your Woo dashboard, WooCommerce Shipping may be for you! ' ,
2022-07-05 05:02:52 +00:00
'woocommerce'
) ,
} ,
} ,
} ) ;
}
2022-09-08 04:39:53 +00:00
useEffect ( ( ) = > {
if ( showTour ) {
recordEvent ( 'walkthrough_settings_shipping_view' ) ;
}
} , [ showTour ] ) ;
2022-07-05 05:02:52 +00:00
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 ;
} ;