525 lines
13 KiB
TypeScript
525 lines
13 KiB
TypeScript
/**
|
|
* External dependencies
|
|
*/
|
|
import {
|
|
ActorRefFrom,
|
|
sendTo,
|
|
setup,
|
|
fromCallback,
|
|
fromPromise,
|
|
assign,
|
|
spawnChild,
|
|
enqueueActions,
|
|
} from 'xstate5';
|
|
import React from 'react';
|
|
import clsx from 'clsx';
|
|
import { getQuery, navigateTo } from '@woocommerce/navigation';
|
|
import {
|
|
OPTIONS_STORE_NAME,
|
|
SETTINGS_STORE_NAME,
|
|
TaskListType,
|
|
TaskType,
|
|
} from '@woocommerce/data';
|
|
import { dispatch, resolveSelect } from '@wordpress/data';
|
|
import { recordEvent } from '@woocommerce/tracks';
|
|
import apiFetch from '@wordpress/api-fetch';
|
|
|
|
/**
|
|
* Internal dependencies
|
|
*/
|
|
import { LaunchYourStoreHubSidebar } from './components/launch-store-hub';
|
|
import type { LaunchYourStoreComponentProps } from '..';
|
|
import type { mainContentMachine } from '../main-content/xstate';
|
|
import { updateQueryParams, createQueryParamsListener } from '../common';
|
|
import { taskClickedAction, getLysTasklist } from './tasklist';
|
|
import { fetchCongratsData } from '../main-content/pages/launch-store-success/services';
|
|
import { getTimeFrame } from '~/utils';
|
|
|
|
export type LYSAugmentedTaskListType = TaskListType & {
|
|
recentlyActionedTasks: string[];
|
|
fullLysTaskList: TaskType[];
|
|
};
|
|
|
|
export type SidebarMachineContext = {
|
|
externalUrl: string | null;
|
|
mainContentMachineRef: ActorRefFrom< typeof mainContentMachine >;
|
|
tasklist?: LYSAugmentedTaskListType;
|
|
testOrderCount: number;
|
|
removeTestOrders?: boolean;
|
|
launchStoreAttemptTimestamp?: number;
|
|
launchStoreError?: {
|
|
message: string;
|
|
};
|
|
siteIsShowingCachedContent?: boolean;
|
|
};
|
|
export type SidebarComponentProps = LaunchYourStoreComponentProps & {
|
|
context: SidebarMachineContext;
|
|
};
|
|
export type SidebarMachineEvents =
|
|
| { type: 'EXTERNAL_URL_UPDATE' }
|
|
| { type: 'OPEN_EXTERNAL_URL'; url: string }
|
|
| { type: 'TASK_CLICKED'; task: TaskType }
|
|
| { type: 'OPEN_WC_ADMIN_URL'; url: string }
|
|
| { type: 'OPEN_WC_ADMIN_URL_IN_CONTENT_AREA'; url: string }
|
|
| { type: 'LAUNCH_STORE'; removeTestOrders: boolean }
|
|
| { type: 'LAUNCH_STORE_SUCCESS' }
|
|
| { type: 'POP_BROWSER_STACK' };
|
|
|
|
const sidebarQueryParamListener = fromCallback( ( { sendBack } ) => {
|
|
return createQueryParamsListener( 'sidebar', sendBack );
|
|
} );
|
|
|
|
const launchStoreAction = async () => {
|
|
const results = await dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
|
woocommerce_coming_soon: 'no',
|
|
} );
|
|
if ( results.success ) {
|
|
return results;
|
|
}
|
|
throw new Error( JSON.stringify( results ) );
|
|
};
|
|
|
|
const getTestOrderCount = async () => {
|
|
const result = ( await apiFetch( {
|
|
path: '/wc-admin/launch-your-store/woopayments/test-orders/count',
|
|
method: 'GET',
|
|
} ) ) as { count: number };
|
|
|
|
return result.count;
|
|
};
|
|
|
|
export const pageHasComingSoonMetaTag = async ( {
|
|
url,
|
|
}: {
|
|
url: string;
|
|
} ): Promise< boolean > => {
|
|
try {
|
|
const response = await fetch( url, {
|
|
method: 'GET',
|
|
credentials: 'omit',
|
|
cache: 'no-store',
|
|
} );
|
|
if ( ! response.ok ) {
|
|
throw new Error( `Failed to fetch ${ url }` );
|
|
}
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString( html, 'text/html' );
|
|
const metaTag = doc.querySelector(
|
|
'meta[name="woo-coming-soon-page"]'
|
|
);
|
|
|
|
if ( metaTag ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch ( error ) {
|
|
throw new Error( `Error fetching ${ url }: ${ error }` );
|
|
}
|
|
};
|
|
|
|
export const getSiteCachedStatus = async () => {
|
|
const settings = await resolveSelect( SETTINGS_STORE_NAME ).getSettings(
|
|
'wc_admin'
|
|
);
|
|
|
|
// if store URL exists, check both storeUrl and siteUrl otherwise only check siteUrl
|
|
// we want to check both because there's a chance that caching is especially disabled for woocommerce pages, e.g WPEngine
|
|
const requests = [] as Promise< boolean >[];
|
|
if ( settings?.shopUrl ) {
|
|
requests.push(
|
|
pageHasComingSoonMetaTag( {
|
|
url: settings.shopUrl,
|
|
} )
|
|
);
|
|
}
|
|
|
|
if ( settings?.siteUrl ) {
|
|
requests.push(
|
|
pageHasComingSoonMetaTag( {
|
|
url: settings.siteUrl,
|
|
} )
|
|
);
|
|
}
|
|
|
|
const results = await Promise.all( requests );
|
|
return results.some( ( result ) => result );
|
|
};
|
|
|
|
const deleteTestOrders = async ( {
|
|
input,
|
|
}: {
|
|
input: {
|
|
removeTestOrders: boolean;
|
|
};
|
|
} ) => {
|
|
if ( ! input.removeTestOrders ) {
|
|
return null;
|
|
}
|
|
return await apiFetch( {
|
|
path: '/wc-admin/launch-your-store/woopayments/test-orders',
|
|
method: 'DELETE',
|
|
} );
|
|
};
|
|
|
|
const recordStoreLaunchAttempt = ( {
|
|
context,
|
|
}: {
|
|
context: SidebarMachineContext;
|
|
} ) => {
|
|
const total_count = context.tasklist?.fullLysTaskList.length || 0;
|
|
const incomplete_tasks =
|
|
context.tasklist?.tasks
|
|
.filter( ( task ) => ! task.isComplete )
|
|
.map( ( task ) => task.id ) || [];
|
|
|
|
const completed =
|
|
context.tasklist?.fullLysTaskList
|
|
.filter( ( task ) => task.isComplete )
|
|
.map( ( task ) => task.id ) || [];
|
|
|
|
const tasks_completed_in_lys = completed.filter( ( task ) =>
|
|
context.tasklist?.recentlyActionedTasks.includes( task )
|
|
); // recently actioned tasks can include incomplete tasks
|
|
|
|
recordEvent( 'launch_your_store_hub_store_launch_attempted', {
|
|
tasks_total_count: total_count, // all lys eligible tasks
|
|
tasks_completed: completed, // all lys eligible tasks that are completed
|
|
tasks_completed_count: completed.length,
|
|
tasks_completed_in_lys,
|
|
tasks_completed_in_lys_count: tasks_completed_in_lys.length,
|
|
incomplete_tasks,
|
|
incomplete_tasks_count: incomplete_tasks.length,
|
|
delete_test_orders: context.removeTestOrders || false,
|
|
} );
|
|
return performance.now();
|
|
};
|
|
|
|
const recordStoreLaunchResults = ( timestamp: number, success: boolean ) => {
|
|
recordEvent( 'launch_your_store_hub_store_launch_results', {
|
|
success,
|
|
duration: getTimeFrame( performance.now() - timestamp ),
|
|
} );
|
|
};
|
|
export const sidebarMachine = setup( {
|
|
types: {} as {
|
|
context: SidebarMachineContext;
|
|
events: SidebarMachineEvents;
|
|
input: {
|
|
mainContentMachineRef: ActorRefFrom< typeof mainContentMachine >;
|
|
};
|
|
},
|
|
actions: {
|
|
openExternalUrl: ( { event } ) => {
|
|
if ( event.type === 'OPEN_EXTERNAL_URL' ) {
|
|
navigateTo( { url: event.url } );
|
|
}
|
|
},
|
|
showLaunchStoreSuccessPage: sendTo(
|
|
( { context } ) => context.mainContentMachineRef,
|
|
{ type: 'SHOW_LAUNCH_STORE_SUCCESS' }
|
|
),
|
|
showLaunchStorePendingCache: sendTo(
|
|
( { context } ) => context.mainContentMachineRef,
|
|
{ type: 'SHOW_LAUNCH_STORE_PENDING_CACHE' }
|
|
),
|
|
showLoadingPage: sendTo(
|
|
( { context } ) => context.mainContentMachineRef,
|
|
{ type: 'SHOW_LOADING' }
|
|
),
|
|
updateQueryParams: (
|
|
_,
|
|
params: { sidebar?: string; content?: string }
|
|
) => {
|
|
updateQueryParams( params );
|
|
},
|
|
taskClicked: ( { event } ) => {
|
|
if ( event.type === 'TASK_CLICKED' ) {
|
|
taskClickedAction( event );
|
|
}
|
|
},
|
|
openWcAdminUrl: ( { event } ) => {
|
|
if ( event.type === 'OPEN_WC_ADMIN_URL' ) {
|
|
navigateTo( { url: event.url } );
|
|
}
|
|
},
|
|
windowHistoryBack: () => {
|
|
window.history.back();
|
|
},
|
|
recordStoreLaunchAttempt: assign( {
|
|
launchStoreAttemptTimestamp: recordStoreLaunchAttempt,
|
|
} ),
|
|
recordStoreLaunchResults: (
|
|
{ context },
|
|
{ success }: { success: boolean }
|
|
) => {
|
|
recordStoreLaunchResults(
|
|
context.launchStoreAttemptTimestamp || 0,
|
|
success
|
|
);
|
|
},
|
|
recordStoreLaunchCachedContentDetected: () => {
|
|
recordEvent(
|
|
'launch_your_store_hub_store_launch_cached_content_detected'
|
|
);
|
|
},
|
|
},
|
|
guards: {
|
|
hasSidebarLocation: (
|
|
_,
|
|
{ sidebarLocation }: { sidebarLocation: string }
|
|
) => {
|
|
const { sidebar } = getQuery() as { sidebar?: string };
|
|
return !! sidebar && sidebar === sidebarLocation;
|
|
},
|
|
hasWooPayments: () => {
|
|
return window?.wcSettings?.admin?.plugins?.activePlugins.includes(
|
|
'woocommerce-payments'
|
|
);
|
|
},
|
|
siteIsShowingCachedContent: ( { context } ) => {
|
|
return !! context.siteIsShowingCachedContent;
|
|
},
|
|
},
|
|
actors: {
|
|
sidebarQueryParamListener,
|
|
getTasklist: fromPromise( getLysTasklist ),
|
|
getTestOrderCount: fromPromise( getTestOrderCount ),
|
|
getSiteCachedStatus: fromPromise( getSiteCachedStatus ),
|
|
updateLaunchStoreOptions: fromPromise( launchStoreAction ),
|
|
deleteTestOrders: fromPromise( deleteTestOrders ),
|
|
fetchCongratsData,
|
|
},
|
|
} ).createMachine( {
|
|
id: 'sidebar',
|
|
initial: 'navigate',
|
|
context: ( { input } ) => ( {
|
|
externalUrl: null,
|
|
testOrderCount: 0,
|
|
mainContentMachineRef: input.mainContentMachineRef,
|
|
} ),
|
|
invoke: {
|
|
id: 'sidebarQueryParamListener',
|
|
src: 'sidebarQueryParamListener',
|
|
},
|
|
states: {
|
|
navigate: {
|
|
always: [
|
|
{
|
|
guard: {
|
|
type: 'hasSidebarLocation',
|
|
params: { sidebarLocation: 'hub' },
|
|
},
|
|
target: 'launchYourStoreHub',
|
|
},
|
|
{
|
|
guard: {
|
|
type: 'hasSidebarLocation',
|
|
params: { sidebarLocation: 'launch-success' },
|
|
},
|
|
target: 'storeLaunchSuccessful',
|
|
},
|
|
{
|
|
target: 'launchYourStoreHub',
|
|
},
|
|
],
|
|
},
|
|
launchYourStoreHub: {
|
|
initial: 'preLaunchYourStoreHub',
|
|
states: {
|
|
preLaunchYourStoreHub: {
|
|
entry: [
|
|
spawnChild( 'fetchCongratsData', {
|
|
id: 'prefetch-congrats-data ',
|
|
} ),
|
|
],
|
|
invoke: {
|
|
src: 'getTasklist',
|
|
onDone: {
|
|
actions: assign( {
|
|
tasklist: ( { event } ) => event.output,
|
|
} ),
|
|
target: 'maybeCountTestOrders',
|
|
},
|
|
},
|
|
},
|
|
maybeCountTestOrders: {
|
|
always: [
|
|
{
|
|
guard: 'hasWooPayments',
|
|
target: 'countTestOrders',
|
|
},
|
|
{
|
|
target: 'launchYourStoreHub',
|
|
},
|
|
],
|
|
},
|
|
countTestOrders: {
|
|
invoke: {
|
|
src: 'getTestOrderCount',
|
|
onDone: {
|
|
actions: assign( {
|
|
testOrderCount: ( { event } ) => event.output,
|
|
} ),
|
|
target: 'launchYourStoreHub',
|
|
},
|
|
onError: {
|
|
target: 'launchYourStoreHub',
|
|
},
|
|
},
|
|
},
|
|
launchYourStoreHub: {
|
|
id: 'launchYourStoreHub',
|
|
tags: 'sidebar-visible',
|
|
meta: {
|
|
component: LaunchYourStoreHubSidebar,
|
|
},
|
|
on: {
|
|
LAUNCH_STORE: {
|
|
target: '#storeLaunching',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
storeLaunching: {
|
|
id: 'storeLaunching',
|
|
initial: 'launching',
|
|
states: {
|
|
launching: {
|
|
entry: [
|
|
assign( { launchStoreError: undefined } ), // clear the errors if any from previously
|
|
'recordStoreLaunchAttempt',
|
|
],
|
|
invoke: [
|
|
{
|
|
src: 'updateLaunchStoreOptions',
|
|
onDone: {
|
|
actions: [
|
|
{
|
|
type: 'recordStoreLaunchResults',
|
|
params: { success: true },
|
|
},
|
|
],
|
|
target: 'checkingForCachedContent',
|
|
},
|
|
onError: {
|
|
actions: [
|
|
assign( {
|
|
launchStoreError: ( { event } ) => {
|
|
return {
|
|
message: JSON.stringify(
|
|
event.error
|
|
), // for some reason event.error is an empty object, worth investigating if we decide to use the error message somewhere
|
|
};
|
|
},
|
|
} ),
|
|
{
|
|
type: 'recordStoreLaunchResults',
|
|
params: {
|
|
success: false,
|
|
},
|
|
},
|
|
],
|
|
target: '#launchYourStoreHub',
|
|
},
|
|
},
|
|
{
|
|
src: 'deleteTestOrders',
|
|
input: ( { event } ) => {
|
|
return {
|
|
removeTestOrders: (
|
|
event as {
|
|
removeTestOrders: boolean;
|
|
}
|
|
).removeTestOrders,
|
|
};
|
|
},
|
|
},
|
|
],
|
|
},
|
|
checkingForCachedContent: {
|
|
invoke: [
|
|
{
|
|
src: 'getSiteCachedStatus',
|
|
onDone: {
|
|
target: '#storeLaunchSuccessful',
|
|
actions: assign( {
|
|
siteIsShowingCachedContent: ( { event } ) =>
|
|
event.output,
|
|
} ),
|
|
},
|
|
onError: {
|
|
target: '#storeLaunchSuccessful',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
storeLaunchSuccessful: {
|
|
id: 'storeLaunchSuccessful',
|
|
tags: 'fullscreen',
|
|
entry: [
|
|
{
|
|
type: 'updateQueryParams',
|
|
params: {
|
|
sidebar: 'launch-success',
|
|
content: 'launch-store-success',
|
|
},
|
|
},
|
|
enqueueActions( ( { check, enqueue } ) => {
|
|
if ( check( 'siteIsShowingCachedContent' ) ) {
|
|
enqueue( {
|
|
type: 'showLaunchStorePendingCache',
|
|
} );
|
|
enqueue( {
|
|
type: 'recordStoreLaunchCachedContentDetected',
|
|
} );
|
|
return;
|
|
}
|
|
enqueue( { type: 'showLaunchStoreSuccessPage' } );
|
|
} ),
|
|
],
|
|
},
|
|
openExternalUrl: {
|
|
id: 'openExternalUrl',
|
|
tags: 'sidebar-visible', // unintuitive but it prevents a layout shift just before leaving
|
|
entry: [ 'openExternalUrl' ],
|
|
},
|
|
},
|
|
on: {
|
|
EXTERNAL_URL_UPDATE: {
|
|
target: '.navigate',
|
|
},
|
|
OPEN_EXTERNAL_URL: {
|
|
target: '#openExternalUrl',
|
|
},
|
|
TASK_CLICKED: {
|
|
actions: 'taskClicked',
|
|
},
|
|
OPEN_WC_ADMIN_URL: {
|
|
actions: 'openWcAdminUrl',
|
|
},
|
|
POP_BROWSER_STACK: {
|
|
actions: 'windowHistoryBack',
|
|
},
|
|
OPEN_WC_ADMIN_URL_IN_CONTENT_AREA: {},
|
|
},
|
|
} );
|
|
export const SidebarContainer = ( {
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
} ) => {
|
|
return (
|
|
<div
|
|
className={ clsx( 'launch-your-store-layout__sidebar', className ) }
|
|
>
|
|
{ children }
|
|
</div>
|
|
);
|
|
};
|