Skip to content

Instantly share code, notes, and snippets.

@NoriSte
Created January 20, 2022 14:41
Show Gist options
  • Save NoriSte/49e7fa070849ef016e593456cfbd1cf7 to your computer and use it in GitHub Desktop.
Save NoriSte/49e7fa070849ef016e593456cfbd1cf7 to your computer and use it in GitHub Desktop.
Explicit React hook-based FSM
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import {
setShopifyShop,
useAuthSession,
useConnectStatus,
useShopifyArguments,
useShopifyUserData,
} from '@/atoms'
import { useDetectPage } from './useDetectPage'
import { useRetrieveShop } from './useRetrieveShop'
import { useLoadUserSession } from './useLoadUserSession'
import { useHowUserNavigated } from './useHowUserNavigated'
type AppStatus =
// initial value
| { status: 'idle' }
| { status: 'showLogin' }
| { status: 'showImport' }
| { status: 'showConnect' }
| { status: 'showNonAdminError' }
| { status: 'loadingUserSession' }
| { status: 'showGenericInstructions' }
| { status: 'showAllOrdersSelectedError' }
| { status: 'showSelectOrdersInstructions' }
/**
* Manage all the Users flows inside the the application.
*/
export function useAppStatus(appInitStatus: 'ready' | 'error' | 'loading'): AppStatus {
const { shop } = useShopifyUserData()
const { ids: orderIds } = useShopifyArguments()
const { status: authStatus } = useAuthSession()
const [currentPage, redirectTo] = useDetectPage()
const { status: connectStatus } = useConnectStatus()
const retrieveShop = useRetrieveShop()
const loadUserSession = useLoadUserSession()
const howUserNavigated = useHowUserNavigated()
// ------------------------------------------------------------------
// Hook data
const [status, setStatus] = useState<AppStatus>({ status: 'idle' })
const api = useRef({
shop,
orderIds,
redirectTo,
currentPage,
retrieveShop,
loadUserSession,
howUserNavigated,
})
useLayoutEffect(() => {
api.current = {
shop,
orderIds,
redirectTo,
currentPage,
retrieveShop,
loadUserSession,
howUserNavigated,
}
}, [currentPage, redirectTo, orderIds, retrieveShop, loadUserSession, howUserNavigated, shop])
// ------------------------------------------------------------------
// App status management
// The following useEffect manages the users flows from a high-level perspective, deciding:
// - if the users must be redirected to another page
// - which contents the user sees
// - recalculating the above data when the users log in and log out
useEffect(() => {
const {
shop,
orderIds,
redirectTo,
currentPage,
retrieveShop,
loadUserSession,
howUserNavigated,
} = api.current
if (appInitStatus !== 'ready') return
switch (currentPage) {
case 'connect':
switch (howUserNavigated('connect')) {
// ------------------------------------------------------------------
// SCENARIO: the server redirected the user to the connect page
// ------------------------------------------------------------------
case 'sentFromServer':
switch (connectStatus.status) {
case 'notRequestedYet':
case 'requesting':
case 'failed':
// when the connect succeeds, this effect is re-triggered
setStatus({ status: 'showConnect' })
break
case 'succeeded':
setStatus({ status: 'showSelectOrdersInstructions' })
break
}
break
// ------------------------------------------------------------------
// SCENARIO: the user navigated directly to the connect page
// ------------------------------------------------------------------
case 'directNavigation':
redirectTo('home') // as a result, this effect is re-triggered
break
}
break
case 'home':
switch (howUserNavigated('home')) {
// ------------------------------------------------------------------
// SCENARIO: the server redirected the user to the home page
// ------------------------------------------------------------------
case 'sentFromServer':
switch (authStatus) {
case 'notLoadedYet':
setStatus({ status: 'loadingUserSession' })
loadUserSession() // no need to wait for the completion, `loadUserSession` will set the atoms that re-trigger this effect
break
case 'loggedOut':
// when the login succeeds, this effect is re-triggered
setStatus({ status: 'showLogin' })
break
case 'authorized':
setStatus({ status: 'showSelectOrdersInstructions' })
break
case 'notAuthorized':
setStatus({ status: 'showNonAdminError' })
break
case 'loading':
// ATTENTION: do nothing here, other components could currently loading the user session
// and they manage the loading phase internally
break
}
break
// ------------------------------------------------------------------
// SCENARIO: the user navigated directly to the home page
// ------------------------------------------------------------------
case 'directNavigation':
switch (authStatus) {
case 'notLoadedYet':
setStatus({ status: 'loadingUserSession' })
// Trying to get the user and his shop from the active session
loadUserSession() // no need to wait for the completion, `loadUserSession` will set the atoms that re-trigger this effect
break
case 'loggedOut':
setStatus({ status: 'showGenericInstructions' })
break
case 'authorized':
// sets the shop as it has been received from the server
setShopifyShop(shop)
setStatus({ status: 'showSelectOrdersInstructions' })
break
case 'notAuthorized':
setStatus({ status: 'showNonAdminError' })
break
}
break
}
break
case 'import':
switch (howUserNavigated('import')) {
// ------------------------------------------------------------------
// SCENARIO 1: the server redirected the user to the import page
//
// SCENARIO 2: the server redirected the user to the import page, then the user reloaded the page.
// In this case, the data sent from the server are stored in the session storage
// ------------------------------------------------------------------
case 'sentFromServer':
switch (authStatus) {
case 'notLoadedYet':
setStatus({ status: 'loadingUserSession' })
loadUserSession() // no need to wait for the completion, `loadUserSession` will set the atoms that re-trigger this effect
break
case 'loggedOut':
setStatus({ status: 'showLogin' })
break
case 'authorized':
if (orderIds.length > 0) {
setStatus({ status: 'showImport' })
} else {
// Due to a Shopify bug, when the user selects *all* the orders, Shopify doesn't
// send the order ids. Therefore, importing the orders is not possible.
setStatus({ status: 'showAllOrdersSelectedError' })
}
break
case 'notAuthorized':
setStatus({ status: 'showNonAdminError' })
break
}
break
// ------------------------------------------------------------------
// SCENARIO: the user navigated directly to the import page
// ------------------------------------------------------------------
case 'directNavigation': {
const execute = async () => {
const shopResult = await retrieveShop()
// If Product doesn't change their mind, retrieving the shop is useless since the user
// is redirected to the home page in all the cases.
switch (shopResult.type) {
case 'unauthorized':
case 'notLoggedIn':
case 'shopFound':
redirectTo('home') // as a result, this effect is re-triggered
break
}
}
execute()
}
}
break
}
}, [appInitStatus, currentPage, authStatus, connectStatus.status])
return status
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment