import { CircleCheck, CircleX, Info, X } from 'lucide-react-native'; import * as React from 'react'; import { ActivityIndicator, Pressable, Text, View } from 'react-native'; import Animated from 'react-native-reanimated'; import { ANIMATION_DURATION, useToastLayoutAnimations } from './animations'; import { toastDefaultValues } from './constants'; import { useToastContext } from './context'; import { ToastSwipeHandler } from './gestures'; import { cn } from './tailwind-utils'; import { isToastAction, type ToastProps } from './types'; import { useColors } from './use-colors'; export const Toast: React.FC = ({ id, title, jsx, description, icon, duration: durationProps, variant, action, cancel, onDismiss, onAutoClose, dismissible = toastDefaultValues.dismissible, closeButton: closeButtonProps, actionButtonStyle, actionButtonTextStyle, actionButtonClassName, actionButtonTextClassName, cancelButtonStyle, cancelButtonTextStyle, cancelButtonClassName, cancelButtonTextClassName, style, className, classNames, styles, promiseOptions, unstyled: unstyledProps, }) => { const { duration: durationCtx, addToast, closeButton: closeButtonCtx, unstyled: unstyledCtx, styles: stylesCtx, classNames: classNamesCtx, icons, } = useToastContext(); const unstyled = unstyledProps ?? unstyledCtx; const duration = durationProps ?? durationCtx; const closeButton = closeButtonProps ?? closeButtonCtx; const colors = useColors(); const { entering, exiting } = useToastLayoutAnimations(); const isDragging = React.useRef(false); const timer = React.useRef(); const timerStart = React.useRef(); const isResolvingPromise = React.useRef(false); React.useEffect(() => { if (isResolvingPromise.current) { return; } if (promiseOptions?.promise) { try { isResolvingPromise.current = true; promiseOptions.promise.then((data) => { addToast({ title: promiseOptions.success(data) ?? 'Success', id, variant: 'success', promiseOptions: undefined, }); isResolvingPromise.current = false; }); } catch (error) { addToast({ title: promiseOptions.error ?? 'Error', id, variant: 'error', promiseOptions: undefined, }); isResolvingPromise.current = false; } return; } if (duration === Infinity) { return; } // Start the timer only if it hasn't been started yet if (!timerStart.current) { timerStart.current = Date.now(); timer.current = setTimeout(() => { if (!isDragging.current) { onAutoClose?.(id); } }, ANIMATION_DURATION + duration); } // Cleanup function to clear the timer if it's still the same timer return () => { if (timer.current) { clearTimeout(timer.current); timer.current = undefined; timerStart.current = undefined; } }; }, [duration, id, onDismiss, promiseOptions, addToast, onAutoClose]); if (jsx) { return jsx; } return ( { onDismiss?.(id); }} onBegin={() => { isDragging.current = true; }} onFinalize={() => { isDragging.current = false; const timeElapsed = Date.now() - timerStart.current!; if (timeElapsed < duration) { timer.current = setTimeout(() => { onDismiss?.(id); }, duration - timeElapsed); } else { onDismiss?.(id); } }} enabled={!promiseOptions && dismissible} style={[stylesCtx.toastContainer, styles?.toastContainer]} className={cn(classNamesCtx.toastContainer, classNames?.toastContainer)} unstyled={unstyled} > {promiseOptions || variant === 'loading' ? ( ) : icon || variant in icons ? ( icons[variant] ) : ( )} {title} {description ? ( {description} ) : null} {isToastAction(action) ? ( {action.label} ) : ( action || undefined )} {isToastAction(cancel) ? ( { cancel.onClick(); onDismiss?.(id); }} className={cancelButtonClassName} style={[ unstyled ? undefined : { flexGrow: 0, }, cancelButtonStyle, ]} > {cancel.label} ) : ( cancel || undefined )} {closeButton && dismissible ? ( onDismiss?.(id)} hitSlop={10} style={[stylesCtx.closeButton, styles?.closeButton]} className={cn(classNamesCtx.closeButton, classNames?.closeButton)} > ) : null} ); }; export const ToastIcon: React.FC> = ({ variant, }) => { const colors = useColors(); switch (variant) { case 'success': return ; case 'error': return ; default: case 'info': return ; } }; const elevationStyle = { shadowOpacity: 0.0015 * 4 + 0.1, shadowRadius: 3 * 4, shadowOffset: { height: 4, width: 0, }, elevation: 4, };