Created
January 20, 2022 14:41
-
-
Save NoriSte/49e7fa070849ef016e593456cfbd1cf7 to your computer and use it in GitHub Desktop.
Explicit React hook-based FSM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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