import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'; import { View, Text, StyleSheet, Animated, TouchableOpacity, Dimensions, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, } from '@/constants/theme'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); type ToastType = 'success' | 'error' | 'info' | 'warning'; interface ToastConfig { type: ToastType; title: string; message?: string; duration?: number; action?: { label: string; onPress: () => void; }; } interface ToastContextValue { show: (config: ToastConfig) => void; success: (title: string, message?: string) => void; error: (title: string, message?: string) => void; info: (title: string, message?: string) => void; hide: () => void; } const ToastContext = createContext(null); export function useToast() { const context = useContext(ToastContext); if (!context) { throw new Error('useToast must be used within a ToastProvider'); } return context; } const toastConfig = { success: { icon: 'checkmark-circle' as const, color: AppColors.success, bgColor: AppColors.successLight, }, error: { icon: 'close-circle' as const, color: AppColors.error, bgColor: AppColors.errorLight, }, info: { icon: 'information-circle' as const, color: AppColors.info, bgColor: AppColors.infoLight, }, warning: { icon: 'warning' as const, color: AppColors.warning, bgColor: AppColors.warningLight, }, }; interface ToastProviderProps { children: React.ReactNode; } export function ToastProvider({ children }: ToastProviderProps) { const insets = useSafeAreaInsets(); const [visible, setVisible] = useState(false); const [config, setConfig] = useState(null); const translateY = useRef(new Animated.Value(-100)).current; const opacity = useRef(new Animated.Value(0)).current; const timeoutRef = useRef(null); const hide = useCallback(() => { Animated.parallel([ Animated.timing(translateY, { toValue: -100, duration: 250, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true, }), ]).start(() => { setVisible(false); setConfig(null); }); }, [translateY, opacity]); const show = useCallback((newConfig: ToastConfig) => { // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setConfig(newConfig); setVisible(true); // Animate in Animated.parallel([ Animated.spring(translateY, { toValue: 0, tension: 80, friction: 10, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); // Auto hide const duration = newConfig.duration ?? 3000; timeoutRef.current = setTimeout(() => { hide(); }, duration); }, [translateY, opacity, hide]); const success = useCallback((title: string, message?: string) => { show({ type: 'success', title, message }); }, [show]); const error = useCallback((title: string, message?: string) => { show({ type: 'error', title, message }); }, [show]); const info = useCallback((title: string, message?: string) => { show({ type: 'info', title, message }); }, [show]); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); const typeConfig = config ? toastConfig[config.type] : toastConfig.info; return ( {children} {visible && config && ( {/* Icon */} {/* Content */} {config.title} {config.message && ( {config.message} )} {/* Action or Close */} {config.action ? ( { config.action?.onPress(); hide(); }} > {config.action.label} ) : ( )} )} ); } // Legacy Toast component for backwards compatibility interface LegacyToastProps { visible: boolean; message: string; icon?: keyof typeof Ionicons.glyphMap; duration?: number; onHide: () => void; } export function Toast({ visible, message, icon = 'checkmark-circle', duration = 2000, onHide }: LegacyToastProps) { const fadeAnim = useRef(new Animated.Value(0)).current; const translateY = useRef(new Animated.Value(20)).current; useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), Animated.timing(translateY, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(); const timer = setTimeout(() => { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(translateY, { toValue: 20, duration: 200, useNativeDriver: true, }), ]).start(() => onHide()); }, duration); return () => clearTimeout(timer); } }, [visible, duration, onHide]); if (!visible) return null; return ( {message} ); } const styles = StyleSheet.create({ // New Toast Provider styles container: { position: 'absolute', left: Spacing.md, right: Spacing.md, zIndex: 9999, }, toast: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.md, gap: Spacing.md, ...Shadows.lg, borderWidth: 1, borderColor: AppColors.borderLight, }, iconContainer: { width: 44, height: 44, borderRadius: BorderRadius.lg, justifyContent: 'center', alignItems: 'center', }, content: { flex: 1, }, title: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, message: { fontSize: FontSizes.sm, color: AppColors.textSecondary, marginTop: 2, }, actionButton: { paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, backgroundColor: AppColors.primaryLighter, borderRadius: BorderRadius.md, }, actionText: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.primary, }, closeButton: { width: 32, height: 32, borderRadius: BorderRadius.md, backgroundColor: AppColors.surfaceSecondary, justifyContent: 'center', alignItems: 'center', }, // Legacy Toast styles legacyContainer: { position: 'absolute', bottom: 100, left: Spacing.xl, right: Spacing.xl, backgroundColor: AppColors.textPrimary, borderRadius: BorderRadius.lg, paddingVertical: Spacing.md, paddingHorizontal: Spacing.lg, flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, ...Shadows.lg, }, legacyIconContainer: { width: 28, height: 28, borderRadius: 14, backgroundColor: AppColors.success, justifyContent: 'center', alignItems: 'center', }, legacyMessage: { flex: 1, fontSize: FontSizes.base, fontWeight: FontWeights.medium, color: AppColors.white, }, });