import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react" // eslint-disable-next-line no-restricted-imports import { Animated, Modal, TextStyle, View, ViewStyle } from "react-native" import { useSafeAreaInsets } from "react-native-safe-area-context" import { Text } from "@/components/Text" import { useAppTheme } from "@/theme/context" import type { ThemedStyle } from "@/theme/types" import { s } from "@/utils/responsive" export type ToastType = "success" | "error" | "warning" | "info" export interface ToastConfig { message: string type?: ToastType duration?: number } export interface ToastContextType { show: (config: ToastConfig) => void success: (message: string, duration?: number) => void error: (message: string, duration?: number) => void warning: (message: string, duration?: number) => void info: (message: string, duration?: number) => void hide: () => void } // Default durations by type (in ms) const DEFAULT_DURATIONS: Record = { success: 3000, error: 5000, // Errors show longer warning: 4000, info: 3000, } const ToastContext = createContext(null) export const ToastProvider: FC = ({ children }) => { const [visible, setVisible] = useState(false) const [config, setConfig] = useState({ message: "" }) const timeoutRef = useRef | null>(null) const fadeAnim = useRef(new Animated.Value(0)).current const insets = useSafeAreaInsets() const { themed, theme: { colors }, } = useAppTheme() // Clear timeout on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, []) const hide = useCallback(() => { // Clear any pending timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } // Fade out animation Animated.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true, }).start(() => { setVisible(false) }) }, [fadeAnim]) const show = useCallback( (newConfig: ToastConfig) => { // Clear any existing timeout to prevent premature hide if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } setConfig(newConfig) setVisible(true) // Fade in animation fadeAnim.setValue(0) Animated.timing(fadeAnim, { toValue: 1, duration: 200, useNativeDriver: true, }).start() // Auto hide after duration (use type-specific default if not provided) const type = newConfig.type ?? "info" const duration = newConfig.duration ?? DEFAULT_DURATIONS[type] timeoutRef.current = setTimeout(() => { hide() }, duration) }, [hide, fadeAnim], ) const success = useCallback( (message: string, duration?: number) => { show({ message, type: "success", duration }) }, [show], ) const error = useCallback( (message: string, duration?: number) => { show({ message, type: "error", duration }) }, [show], ) const warning = useCallback( (message: string, duration?: number) => { show({ message, type: "warning", duration }) }, [show], ) const info = useCallback( (message: string, duration?: number) => { show({ message, type: "info", duration }) }, [show], ) const value = useMemo( () => ({ show, success, error, warning, info, hide }), [show, success, error, warning, info, hide], ) const getBackgroundColor = (type: ToastType = "info") => { switch (type) { case "success": return colors.palette.primary500 case "error": return colors.error case "warning": return colors.palette.accent500 case "info": default: return colors.palette.secondary400 } } const getTextColor = (type: ToastType = "info") => { switch (type) { case "warning": return colors.palette.neutral800 default: return colors.palette.neutral100 } } return ( {children} ) } export const useToast = (): ToastContextType => { const context = useContext(ToastContext) if (!context) { throw new Error("useToast must be used within a ToastProvider") } return context } const $overlay: ViewStyle = { flex: 1, alignItems: "center", } const $toastContainer: ThemedStyle = ({ spacing }) => ({ minWidth: s(120), maxWidth: "80%", paddingVertical: spacing.md, paddingHorizontal: spacing.lg, borderRadius: s(8), shadowColor: "#000", shadowOffset: { width: 0, height: s(2) }, shadowOpacity: 0.25, shadowRadius: s(4), elevation: 5, }) const $toastText: ThemedStyle = () => ({ textAlign: "center", })