import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
TouchableOpacity,
Platform,
} from 'react-native';
import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-react-native';
const NotificationContext = createContext(null);
const TYPES = {
success: { bg: '#f0fdf4', border: '#bbf7d0', text: '#15803d', icon: CheckCircle2, iconColor: '#16a34a' },
error: { bg: '#fef2f2', border: '#fecaca', text: '#b91c1c', icon: XCircle, iconColor: '#dc2626' },
warning: { bg: '#fffbeb', border: '#fed7aa', text: '#92400e', icon: AlertTriangle, iconColor: '#f59e0b' },
info: { bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af', icon: Info, iconColor: '#3b82f6' },
};
const Toast = ({ id, type = 'info', title, message, onDismiss }) => {
const config = TYPES[type] || TYPES.info;
const IconComponent = config.icon;
const slideAnim = useRef(new Animated.Value(-120)).current;
const opacityAnim = useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.parallel([
Animated.spring(slideAnim, { toValue: 0, damping: 18, stiffness: 200, useNativeDriver: true }),
Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true }),
]).start();
}, []);
const dismiss = useCallback(() => {
Animated.parallel([
Animated.timing(slideAnim, { toValue: -120, duration: 250, useNativeDriver: true }),
Animated.timing(opacityAnim, { toValue: 0, duration: 200, useNativeDriver: true }),
]).start(() => onDismiss(id));
}, [id, onDismiss, slideAnim, opacityAnim]);
return (
{title ? {title} : null}
{message ? {message} : null}
);
};
// topInset is passed in from App.js (which has access to SafeAreaProvider context)
export const NotificationProvider = ({ children, topInset = 50 }) => {
const [toasts, setToasts] = useState([]);
const timerRefs = useRef({});
const dismiss = useCallback((id) => {
clearTimeout(timerRefs.current[id]);
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const notify = useCallback(({ type = 'info', title, message, duration = 4000 }) => {
const id = Date.now().toString();
setToasts((prev) => [...prev.slice(-2), { id, type, title, message }]);
timerRefs.current[id] = setTimeout(() => dismiss(id), duration);
return id;
}, [dismiss]);
const success = useCallback((title, message, opts) => notify({ type: 'success', title, message, ...opts }), [notify]);
const error = useCallback((title, message, opts) => notify({ type: 'error', title, message, ...opts }), [notify]);
const warning = useCallback((title, message, opts) => notify({ type: 'warning', title, message, ...opts }), [notify]);
const info = useCallback((title, message, opts) => notify({ type: 'info', title, message, ...opts }), [notify]);
return (
{children}
{toasts.map((toast) => (
))}
);
};
export const useNotification = () => {
const ctx = useContext(NotificationContext);
if (!ctx) throw new Error('useNotification must be used inside NotificationProvider');
return ctx;
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 16,
right: 16,
zIndex: 9999,
gap: 8,
},
toast: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 16,
borderWidth: 1.5,
padding: 14,
gap: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 8,
},
toastIcon: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0,
},
toastContent: { flex: 1 },
toastTitle: { fontSize: 14, fontWeight: '700', marginBottom: 2 },
toastMessage: { fontSize: 13, fontWeight: '500', lineHeight: 18 },
toastClose: { padding: 4, flexShrink: 0 },
});