import React, { createContext, useContext, useEffect, useMemo, useRef, } from 'react'; import { BackHandler, Platform } from 'react-native'; import { memoryStorage } from './storage'; import { useCoachmarkStore } from './store'; import { defaultTheme } from './theme'; import type { CoachmarkTheme, Plugin, StorageAdapter } from './types'; export type CoachmarkContextValue = ReturnType & { theme: CoachmarkTheme; storage: StorageAdapter; }; const Ctx = createContext(undefined); export const CoachmarkProvider: React.FC<{ children: React.ReactNode; theme?: Partial; storage?: StorageAdapter; plugins?: Plugin[]; }> = ({ children, theme, storage, plugins = [] }) => { const memStorage: StorageAdapter = storage ?? memoryStorage(); const store = useCoachmarkStore(memStorage); const stopRef = useRef(store.stop); stopRef.current = store.stop; useEffect(() => { if (!store.state.isActive || Platform.OS !== 'android') { return undefined; } const sub = BackHandler.addEventListener('hardwareBackPress', () => { stopRef.current('completed').catch(() => {}); return true; }); return () => sub.remove(); }, [store.state.isActive]); const mergedTheme = useMemo( () => ({ ...defaultTheme, ...theme, tooltip: { ...defaultTheme.tooltip, ...(theme?.tooltip || {}) }, motion: { ...defaultTheme.motion, ...(theme?.motion || {}) }, }), [theme] ); useEffect(() => { store.addPlugins(plugins); // eslint-disable-next-line react-hooks/exhaustive-deps }, [plugins]); return ( {children} ); }; /** * Hook to access the coachmark context * @throws Error if used outside of CoachmarkProvider */ export const useCoachmarkContext = (): CoachmarkContextValue => { const context = useContext(Ctx); if (!context) { throw new Error( 'useCoachmarkContext must be used within a CoachmarkProvider. ' + 'Make sure your component is wrapped with .' ); } return context; };