import mitt, { type Emitter } from 'mitt'; import { createContext, useCallback, useMemo, useRef, useState, type PropsWithChildren, } from 'react'; import { findNodeHandle, type ScrollView } from 'react-native'; import { TourModal } from '../components/TourModal'; import { OFFSET_WIDTH } from '../components/style'; import { useStateWithAwait } from '../hooks/useStateWithAwait'; import { useStepsMap } from '../hooks/useStepsMap'; import { type TourOptions, type Step, type TourModalHandle } from '../types'; type Events = { start: undefined; stop: undefined; stepChange: Step | undefined; }; interface TourContextType { registerStep: (step: Step) => void; unregisterStep: (stepName: string) => void; currentStep: Step | undefined; start: ( fromStep?: string, suppliedScrollView?: ScrollView | null ) => Promise; stop: () => Promise; goToNext: () => Promise; goToNth: (n: number) => Promise; goToPrev: () => Promise; visible: boolean; tourEvents: Emitter; isFirstStep: boolean; isLastStep: boolean; currentStepNumber: number; totalStepsNumber: number; } export type { TourContextType }; /* This is the maximum wait time for the steps to be registered before starting the tutorial At 60fps means 2 seconds */ const MAX_START_TRIES = 120; const TourContext = createContext(undefined); export { TourContext }; export const TourProvider = ({ verticalOffset = 0, children, ...rest }: PropsWithChildren) => { const startTries = useRef(0); const tourEvents = useRef(mitt()).current; const modal = useRef(null); const [visible, setVisibility] = useStateWithAwait(false); const [scrollView, setScrollView] = useState(null); const { currentStep, currentStepNumber, totalStepsNumber, getFirstStep, getPrevStep, getNextStep, getNthStep, isFirstStep, isLastStep, setCurrentStepState, steps, registerStep, unregisterStep, } = useStepsMap(); const moveModalToStep = useCallback( async (step: Step) => { const size = await step?.measure(); if (!size) { return; } await modal.current?.animateMove({ width: size.width + OFFSET_WIDTH, height: size.height + OFFSET_WIDTH, x: size.x - OFFSET_WIDTH / 2, y: size.y - OFFSET_WIDTH / 2 + verticalOffset, }); }, [verticalOffset] ); const setCurrentStep = useCallback( async (step?: Step, move: boolean = true) => { setCurrentStepState(step); tourEvents.emit('stepChange', step); if (scrollView != null) { const nodeHandle = findNodeHandle(scrollView); if (nodeHandle) { step?.wrapperRef.current?.measureLayout( nodeHandle, (_x, y, _w, h) => { const yOffset = y > 0 ? y - h / 2 : 0; scrollView.scrollTo({ y: yOffset, animated: false }); } ); } } setTimeout( () => { if (move && step) { moveModalToStep(step).catch(() => {}); } }, scrollView != null ? 100 : 0 ); }, [tourEvents, moveModalToStep, scrollView, setCurrentStepState] ); const start = useCallback( async (fromStep?: string, suppliedScrollView: ScrollView | null = null) => { if (scrollView == null) { setScrollView(suppliedScrollView); } const stepToStart = fromStep ? steps[fromStep] : getFirstStep(); if (startTries.current > MAX_START_TRIES) { startTries.current = 0; return; } if (stepToStart == null) { startTries.current += 1; requestAnimationFrame(() => { start(fromStep).catch(() => {}); }); } else { tourEvents.emit('start'); await setCurrentStep(stepToStart); await moveModalToStep(stepToStart); await setVisibility(true); startTries.current = 0; } }, [ tourEvents, getFirstStep, moveModalToStep, scrollView, setCurrentStep, setVisibility, steps, ] ); const stop = useCallback(async () => { await setVisibility(false); tourEvents.emit('stop'); }, [tourEvents, setVisibility]); const next = useCallback(async () => { await setCurrentStep(getNextStep()); }, [getNextStep, setCurrentStep]); const nth = useCallback( async (n: number) => { await setCurrentStep(getNthStep(n)); }, [getNthStep, setCurrentStep] ); const prev = useCallback(async () => { await setCurrentStep(getPrevStep()); }, [getPrevStep, setCurrentStep]); const value = useMemo( () => ({ registerStep, unregisterStep, currentStep, start, stop, visible, tourEvents, goToNext: next, goToNth: nth, goToPrev: prev, isFirstStep, isLastStep, currentStepNumber, totalStepsNumber, }), [ registerStep, unregisterStep, currentStep, start, stop, visible, tourEvents, next, nth, prev, isFirstStep, isLastStep, currentStepNumber, totalStepsNumber, ] ); return ( <> {children} ); };