import { useOnboardingDataProvider } from './DataProvider'; import { OnboardingModes } from '@/components/Onboarding/enums'; import { ITipDescriptor } from '@/components/Onboarding/types'; import _ from 'lodash'; import { PropsWithChildren, createContext, useEffect, useMemo, useReducer } from 'react'; enum OnboardingActionType { RESTART = 'RESTART', SET_SHOW = 'SET_SHOW', REGISTER_TIP = 'REGISTER_TIP', UNREGISTER_TIP = 'UNREGISTER_TIP', DISMISS_TIP = 'DISMISS_TIP', SET_ON_COMPLETE = 'SET_ON_COMPLETE' } type IOnboardingProviderProps = PropsWithChildren<{ mode: OnboardingModes; topic?: string; }>; type IOnCompleteCallback = () => void; type IOnboardingState = { show: boolean; tips: Array; currentStep: number; onComplete: IOnCompleteCallback; }; type IOnboardingAction = | { type: OnboardingActionType.RESTART } | { type: OnboardingActionType.SET_SHOW; payload: boolean } | { type: OnboardingActionType.REGISTER_TIP; payload: ITipDescriptor } | { type: OnboardingActionType.UNREGISTER_TIP; payload: ITipDescriptor | string } | { type: OnboardingActionType.DISMISS_TIP; payload: ITipDescriptor | string } | { type: OnboardingActionType.SET_ON_COMPLETE; payload: IOnCompleteCallback }; type IOnboardingContext = { state: IOnboardingState; dispatch: (action: IOnboardingAction) => void }; function reducer(state: IOnboardingState, action: IOnboardingAction): IOnboardingState { const newState = _.clone(state); const { type } = action; switch (type) { case OnboardingActionType.SET_SHOW: { const { payload } = action; return _.extend(newState, { show: payload }); } case OnboardingActionType.REGISTER_TIP: { const { payload } = action; const { id } = payload; const tips = _.chain(newState.tips) .clone() .reject((t) => t.id === id) .value(); const existingTip = _.find(newState.tips, { id: id }); const newTip = _.chain(existingTip ?? {}) .clone() .extend(payload) .value(); tips.push(newTip); return _.extend(newState, { tips: _.sortBy(tips, ['step']) }); } case OnboardingActionType.UNREGISTER_TIP: { const { payload } = action; const id = _.isString(payload) ? payload : payload.id; const tips = _.chain(newState.tips) .reject((t) => t.id === id) .sortBy(['step']) .value(); return _.extend(newState, { tips: tips }); } case OnboardingActionType.DISMISS_TIP: { const { payload } = action; const id = _.isString(payload) ? payload : payload.id; const tips = _.chain(newState.tips) .reject((t) => t.id === id) .value(); const tip = _.find(newState.tips, { id: id }); if (_.isNil(tip)) { return newState; } const newTip = _.chain(tip).clone().extend({ dismissed: true }).value(); tips.push(newTip); const updatedStep = _.chain(tips) .reject((t) => t.dismissed ?? false) .minBy('step') .value()?.step ?? 0; const isOnboardingComplete = newTip?.completeOnboarding && _.chain(tips) .reject((t) => t.step < updatedStep) .reject((t) => t.dismissed ?? false) .isEmpty() .value(); if (isOnboardingComplete) { state.onComplete(); } return _.extend(newState, { tips: _.sortBy(tips, ['step']), currentStep: updatedStep }); } case OnboardingActionType.SET_ON_COMPLETE: { const { payload } = action; return _.extend(newState, { onComplete: payload }); } case OnboardingActionType.RESTART: { const tips = _.chain(newState.tips) .clone() .map((t) => _.omit(t, 'dismissed')) .value(); return _.extend(newState, { tips: tips, currentStep: 0 }); } default: return newState; } } const OnboardingContext = createContext(undefined); function OnboardingProvider(props: IOnboardingProviderProps) { const { mode, topic = 'default' } = props; const { onboardingRequired, acknowledgeOnboarding } = useOnboardingDataProvider(mode, topic); const [state, dispatch] = useReducer(reducer, { show: onboardingRequired, tips: [], currentStep: 0, onComplete: () => {} }); const value = useMemo(() => ({ state, dispatch }), [state, dispatch]); useEffect(() => { dispatch({ type: OnboardingActionType.SET_SHOW, payload: onboardingRequired }); }, [onboardingRequired, dispatch]); useEffect(() => { function onComplete() { acknowledgeOnboarding().then(() => { dispatch({ type: OnboardingActionType.SET_SHOW, payload: false }); }); } dispatch({ type: OnboardingActionType.SET_ON_COMPLETE, payload: onComplete }); }, [acknowledgeOnboarding, dispatch]); return {props.children}; } export type { IOnboardingAction, IOnboardingContext, IOnboardingState }; export { OnboardingActionType, OnboardingContext, OnboardingProvider };