diff --git a/package-lock.json b/package-lock.json index 723b846..1e1eded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "name": "nuvio", "version": "1.0.0", "dependencies": { + "@backpackapp-io/react-native-toast": "^0.14.0", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/blur": "^4.4.1", + "@react-native-community/netinfo": "^11.4.1", "@react-native-community/slider": "^4.5.6", "@react-native-picker/picker": "^2.11.0", "@react-navigation/bottom-tabs": "^7.3.10", @@ -2257,6 +2259,35 @@ "node": ">=6.9.0" } }, + "node_modules/@backpackapp-io/react-native-toast": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.14.0.tgz", + "integrity": "sha512-vt75tajD+kgHnKiuUlDIEPqqg1qtZ8PMG1mce4WJOEeF+BVbsFrSmoDGCRbPUHkq7TFuIc/Hn89lYAUqPlTFVA==", + "license": "MIT", + "dependencies": { + "@backpackapp-io/react-native-toast": "^0.13.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">=2.2.1", + "react-native-reanimated": ">=2.8.0", + "react-native-safe-area-context": ">=4.2.4" + } + }, + "node_modules/@backpackapp-io/react-native-toast/node_modules/@backpackapp-io/react-native-toast": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.13.0.tgz", + "integrity": "sha512-kutFSE1vi77ybNV24JSnKQ4WUgWZ+LcYsrdAyl5ztEWGP7FRsfinsuaOrBQwDBGxm0rzMFeKM5K7fRHa/Hvy8A==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">=2.2.1", + "react-native-reanimated": ">=2.8.0", + "react-native-safe-area-context": ">=4.2.4" + } + }, "node_modules/@callstack/react-theme-provider": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", @@ -3469,6 +3500,15 @@ "react-native": "*" } }, + "node_modules/@react-native-community/netinfo": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", + "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.59" + } + }, "node_modules/@react-native-community/slider": { "version": "4.5.6", "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz", diff --git a/package.json b/package.json index af842c3..8ab9089 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "web": "expo start --web" }, "dependencies": { + "@backpackapp-io/react-native-toast": "^0.14.0", "@expo/metro-runtime": "~4.0.1", "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/blur": "^4.4.1", + "@react-native-community/netinfo": "^11.4.1", "@react-native-community/slider": "^4.5.6", "@react-native-picker/picker": "^2.11.0", "@react-navigation/bottom-tabs": "^7.3.10", diff --git a/src/components/common/ToastOverlay.tsx b/src/components/common/ToastOverlay.tsx deleted file mode 100644 index 7a28dc6..0000000 --- a/src/components/common/ToastOverlay.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useEffect, useMemo, useRef } from 'react'; -import { Animated, Easing, StyleSheet, Text, ViewStyle } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -export type ToastType = 'success' | 'error' | 'info'; - -type Props = { - visible: boolean; - message: string; - type?: ToastType; - duration?: number; // ms - onHide?: () => void; - bottomOffset?: number; // extra offset above safe area / tab bar - containerStyle?: ViewStyle; -}; - -const colorsByType: Record = { - success: 'rgba(46,160,67,0.95)', - error: 'rgba(229, 62, 62, 0.95)', - info: 'rgba(99, 102, 241, 0.95)', -}; - -export const ToastOverlay: React.FC = ({ - visible, - message, - type = 'info', - duration = 1800, - onHide, - bottomOffset = 90, - containerStyle, -}) => { - const insets = useSafeAreaInsets(); - const opacity = useRef(new Animated.Value(0)).current; - const translateY = useRef(new Animated.Value(12)).current; - const hideTimer = useRef(null); - - useEffect(() => { - if (visible) { - // clear any existing timer - if (hideTimer.current) { - clearTimeout(hideTimer.current); - hideTimer.current = null; - } - opacity.setValue(0); - translateY.setValue(12); - Animated.parallel([ - Animated.timing(opacity, { toValue: 1, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }), - Animated.timing(translateY, { toValue: 0, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }), - ]).start(() => { - hideTimer.current = setTimeout(() => { - Animated.parallel([ - Animated.timing(opacity, { toValue: 0, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }), - Animated.timing(translateY, { toValue: 12, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }), - ]).start(() => { - if (onHide) onHide(); - }); - }, Math.max(800, duration)); - }); - } else { - // If toggled off externally, hide instantly - Animated.parallel([ - Animated.timing(opacity, { toValue: 0, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }), - Animated.timing(translateY, { toValue: 12, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }), - ]).start(); - if (hideTimer.current) { - clearTimeout(hideTimer.current); - hideTimer.current = null; - } - } - - return () => { - if (hideTimer.current) { - clearTimeout(hideTimer.current); - hideTimer.current = null; - } - }; - }, [visible, duration, onHide, opacity, translateY]); - - const bg = useMemo(() => colorsByType[type], [type]); - const bottom = (insets?.bottom || 0) + bottomOffset; - - if (!visible && opacity.__getValue() === 0) { - // Avoid mounting when fully hidden to minimize layout cost - return null; - } - - return ( - - - {message} - - - ); -}; - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - left: 16, - right: 16, - zIndex: 999, - }, - text: { - color: '#fff', - fontWeight: '700', - textAlign: 'center', - paddingVertical: 8, - paddingHorizontal: 12, - borderRadius: 12, - overflow: 'hidden', - fontSize: 12, - }, -}); - -export default ToastOverlay; - - diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index d8af6a5..fa98fc0 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -14,6 +14,7 @@ import { NuvioHeader } from '../components/NuvioHeader'; import { Stream } from '../types/streams'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; +import { Toasts } from '@backpackapp-io/react-native-toast'; // Import screens with their proper types import HomeScreen from '../screens/HomeScreen'; @@ -1073,6 +1074,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta + ); }; diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index eeb9741..fa87101 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -7,7 +7,7 @@ import { useTheme } from '../contexts/ThemeContext'; import { useAccount } from '../contexts/AccountContext'; import { useNavigation, useRoute } from '@react-navigation/native'; import * as Haptics from 'expo-haptics'; -import ToastOverlay from '../components/common/ToastOverlay'; +import { toast } from '@backpackapp-io/react-native-toast'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width, height } = Dimensions.get('window'); @@ -41,7 +41,7 @@ const AuthScreen: React.FC = () => { const ctaTextTranslateY = useRef(new Animated.Value(0)).current; const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup const [switchWidth, setSwitchWidth] = useState(0); - const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'success' | 'error' | 'info' }>({ visible: false, message: '', type: 'info' }); + // Legacy local toast state removed in favor of global toast const [headerHeight, setHeaderHeight] = useState(0); const headerHideAnim = useRef(new Animated.Value(0)).current; // 0 visible, 1 hidden const [keyboardHeight, setKeyboardHeight] = useState(0); @@ -144,21 +144,21 @@ const AuthScreen: React.FC = () => { if (!isEmailValid) { const msg = 'Enter a valid email address'; setError(msg); - showToast(msg, 'error'); + toast.error(msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (!isPasswordValid) { const msg = 'Password must be at least 6 characters'; setError(msg); - showToast(msg, 'error'); + toast.error(msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (mode === 'signup' && !passwordsMatch) { const msg = 'Passwords do not match'; setError(msg); - showToast(msg, 'error'); + toast.error(msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } @@ -167,11 +167,11 @@ const AuthScreen: React.FC = () => { const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password); if (err) { setError(err); - showToast(err, 'error'); + toast.error(err); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); } else { const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; - showToast(msg, 'success'); + toast.success(msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); } setLoading(false); @@ -184,9 +184,7 @@ const AuthScreen: React.FC = () => { navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any); }; - const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => { - setToast({ visible: true, message, type }); - }; + // showToast helper replaced with direct calls to toast.* API return ( @@ -521,15 +519,7 @@ const AuthScreen: React.FC = () => { - {/* Screen-level toast overlay so it is not clipped by the card */} - 0 ? keyboardHeight + 8 : 24)} - onHide={() => setToast(prev => ({ ...prev, visible: false }))} - /> + {/* Toasts rendered globally in App root */} ); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 4da7b4b..0a8486d 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -64,7 +64,7 @@ import type { Theme } from '../contexts/ThemeContext'; import * as ScreenOrientation from 'expo-screen-orientation'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import ToastOverlay from '../components/common/ToastOverlay'; +import { toast, ToastPosition } from '@backpackapp-io/react-native-toast'; import FirstTimeWelcome from '../components/FirstTimeWelcome'; import { imageCacheService } from '../services/imageCacheService'; @@ -321,6 +321,13 @@ const HomeScreen = () => { setHintVisible(true); await AsyncStorage.removeItem('showLoginHintToastOnce'); hideTimer = setTimeout(() => setHintVisible(false), 2000); + // Also show a global toast for consistency across screens + try { + toast('You can sign in anytime from Settings → Account', { + duration: 1600, + position: ToastPosition.BOTTOM, + }); + } catch {} } } catch {} })(); @@ -728,15 +735,7 @@ const HomeScreen = () => { disableIntervalMomentum={true} scrollEventThrottle={16} /> - {/* One-time hint toast after skipping sign-in */} - setHintVisible(false)} - /> + {/* Toasts are rendered globally at root */} ); }, [