diff --git a/package-lock.json b/package-lock.json index 2641fdf..8a6cbf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,8 +84,7 @@ "react-native-video": "^6.17.0", "react-native-web": "^0.21.0", "react-native-wheel-color-picker": "^1.3.1", - "react-native-worklets": "^0.6.1", - "toastify-react-native": "^7.2.3" + "react-native-worklets": "^0.6.1" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -12854,19 +12853,6 @@ "node": ">=8.0" } }, - "node_modules/toastify-react-native": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/toastify-react-native/-/toastify-react-native-7.2.3.tgz", - "integrity": "sha512-ngmpTKlTo0IRddwSsNWK+YKbB2veqotHy7Zpil4eksoLAlq0RPSgdVOk5QDEDUONJQ4r7ljGYeRW68KBztirsg==", - "license": "MIT", - "dependencies": { - "react-native-vector-icons": "*" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index f07ab8e..267e782 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,7 @@ "react-native-video": "^6.17.0", "react-native-web": "^0.21.0", "react-native-wheel-color-picker": "^1.3.1", - "react-native-worklets": "^0.6.1", - "toastify-react-native": "^7.2.3" + "react-native-worklets": "^0.6.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index 87ba6c7..ed3cc89 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Platform } from 'react-native'; -import { Toast } from 'toastify-react-native'; +import { toastService } from '../services/toastService'; import UpdateService, { UpdateInfo } from '../services/updateService'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -78,13 +78,13 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { // The app will automatically reload with the new version console.log('Update installed successfully'); } else { - Toast.error('Unable to install the update. Please try again later or check your internet connection.'); + toastService.showError('Installation Failed', 'Unable to install the update. Please try again later or check your internet connection.'); // Show popup again after failed installation setShowUpdatePopup(true); } } catch (error) { if (__DEV__) console.error('Error installing update:', error); - Toast.error('An error occurred while installing the update. Please try again later.'); + toastService.showError('Installation Error', 'An error occurred while installing the update. Please try again later.'); // Show popup again after error setShowUpdatePopup(true); } finally { @@ -135,7 +135,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { (async () => { try { await AsyncStorage.setItem(UPDATE_BADGE_KEY, 'true'); } catch {} })(); - try { Toast.info('Update available — go to Settings → App Updates'); } catch {} + toastService.showInfo('Update Available', 'Update available — go to Settings → App Updates'); setShowUpdatePopup(false); } else { setShowUpdatePopup(true); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c805799..d987229 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -15,7 +15,6 @@ import { HeaderVisibility } from '../contexts/HeaderVisibility'; import { Stream } from '../types/streams'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; -import ToastManager from 'toastify-react-native'; import { PostHogProvider } from 'posthog-react-native'; // Optional iOS Glass effect (expo-glass-effect) with safe fallback @@ -1499,85 +1498,6 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta - {/* Global toast customization using ThemeContext */} - ( - - {props.text1} - {props.text2 ? ( - {props.text2} - ) : null} - - ), - success: (props: any) => ( - - {props.text1} - {props.text2 ? ( - {props.text2} - ) : null} - - ), - error: (props: any) => ( - - {props.text1} - {props.text2 ? ( - {props.text2} - ) : null} - - ), - }} - /> ); }; diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index edb36e5..fa9eda6 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 ToastManager, { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width, height } = Dimensions.get('window'); @@ -19,6 +19,7 @@ const AuthScreen: React.FC = () => { const route = useRoute(); const fromOnboarding = !!route?.params?.fromOnboarding; const insets = useSafeAreaInsets(); + const { showError, showSuccess } = useToast(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -149,7 +150,7 @@ const AuthScreen: React.FC = () => { if (mode === 'signup' && signupDisabled) { const msg = 'Sign up is currently disabled due to upcoming system changes'; setError(msg); - Toast.error(msg); + showError('Sign Up Disabled', 'Sign up is currently disabled due to upcoming system changes'); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } @@ -157,21 +158,21 @@ const AuthScreen: React.FC = () => { if (!isEmailValid) { const msg = 'Enter a valid email address'; setError(msg); - Toast.error(msg); + showError('Invalid Email', 'Enter a valid email address'); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (!isPasswordValid) { const msg = 'Password must be at least 6 characters'; setError(msg); - Toast.error(msg); + showError('Password Too Short', 'Password must be at least 6 characters'); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } if (mode === 'signup' && !passwordsMatch) { const msg = 'Passwords do not match'; setError(msg); - Toast.error(msg); + showError('Passwords Don\'t Match', 'Passwords do not match'); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); return; } @@ -180,11 +181,11 @@ const AuthScreen: React.FC = () => { const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password); if (err) { setError(err); - Toast.error(err); + showError('Authentication Failed', err); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); } else { const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; - Toast.success(msg); + showSuccess('Success', msg); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); // Navigate to main tabs after successful authentication diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index a5f3591..9873673 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -29,7 +29,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import FastImage from '@d11/react-native-fast-image'; import { useDownloads } from '../contexts/DownloadsContext'; import type { DownloadItem } from '../contexts/DownloadsContext'; -import { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import CustomAlert from '../components/CustomAlert'; const { height, width } = Dimensions.get('window'); @@ -98,6 +98,7 @@ const DownloadItemComponent: React.FC<{ onRequestRemove: (item: DownloadItem) => void; }> = React.memo(({ item, onPress, onAction, onRequestRemove }) => { const { currentTheme } = useTheme(); + const { showSuccess, showInfo } = useToast(); const [posterUrl, setPosterUrl] = useState(item.posterUrl || null); // Try to fetch poster if not available @@ -113,18 +114,18 @@ const DownloadItemComponent: React.FC<{ if (item.status === 'completed' && item.fileUri) { Clipboard.setString(item.fileUri); if (Platform.OS === 'android') { - Toast.success('Local file path copied to clipboard'); + showSuccess('Path Copied', 'Local file path copied to clipboard'); } else { Alert.alert('Copied', 'Local file path copied to clipboard'); } } else if (item.status !== 'completed') { if (Platform.OS === 'android') { - Toast.info('Download is not complete yet'); + showInfo('Download Incomplete', 'Download is not complete yet'); } else { Alert.alert('Not Available', 'The local file path is available only after the download is complete.'); } } - }, [item.status, item.fileUri]); + }, [item.status, item.fileUri, showSuccess, showInfo]); const formatBytes = (bytes?: number) => { if (!bytes || bytes <= 0) return '0 B'; @@ -343,6 +344,7 @@ const DownloadsScreen: React.FC = () => { const { currentTheme } = useTheme(); const { top: safeAreaTop } = useSafeAreaInsets(); const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads(); + const { showSuccess, showInfo } = useToast(); const [isRefreshing, setIsRefreshing] = useState(false); const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all'); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index ee93c7e..c432e2a 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -59,7 +59,7 @@ import { useLoading } from '../contexts/LoadingContext'; 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 { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import FirstTimeWelcome from '../components/FirstTimeWelcome'; import { HeaderVisibility } from '../contexts/HeaderVisibility'; @@ -111,6 +111,7 @@ const HomeScreen = () => { const continueWatchingRef = useRef(null); const { settings } = useSettings(); const { lastUpdate } = useCatalogContext(); // Add catalog context to listen for addon changes + const { showInfo } = useToast(); const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection); const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource); const refreshTimeoutRef = useRef(null); @@ -351,7 +352,7 @@ const HomeScreen = () => { await AsyncStorage.removeItem('showLoginHintToastOnce'); hideTimer = setTimeout(() => setHintVisible(false), 2000); // Also show a global toast for consistency across screens - try { Toast.info('You can sign in anytime from Settings → Account', 'bottom'); } catch {} + showInfo('Sign In Available', 'You can sign in anytime from Settings → Account'); } } catch {} })(); diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 26ffb0a..0239c2d 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { DeviceEventEmitter } from 'react-native'; import { Share } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import DropUpMenu from '../components/home/DropUpMenu'; import { View, @@ -208,6 +208,7 @@ const LibraryScreen = () => { const [filter, setFilter] = useState<'trakt' | 'movies' | 'series'>('movies'); const [showTraktContent, setShowTraktContent] = useState(false); const [selectedTraktFolder, setSelectedTraktFolder] = useState(null); + const { showInfo, showError } = useToast(); // DropUpMenu state const [menuVisible, setMenuVisible] = useState(false); const [selectedItem, setSelectedItem] = useState(null); @@ -1005,11 +1006,11 @@ const LibraryScreen = () => { case 'library': { try { await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); - Toast.info('Removed from Library'); + showInfo('Removed from Library', 'Item removed from your library'); setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type))); setMenuVisible(false); } catch (error) { - Toast.error('Failed to update Library'); + showError('Failed to update Library', 'Unable to remove item from library'); } break; } @@ -1019,7 +1020,7 @@ const LibraryScreen = () => { const key = `watched:${selectedItem.type}:${selectedItem.id}`; const newWatched = !selectedItem.watched; await AsyncStorage.setItem(key, newWatched ? 'true' : 'false'); - Toast.info(newWatched ? 'Marked as Watched' : 'Marked as Unwatched'); + showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); // Instantly update local state setLibraryItems(prev => prev.map(item => item.id === selectedItem.id && item.type === selectedItem.type @@ -1027,7 +1028,7 @@ const LibraryScreen = () => { : item )); } catch (error) { - Toast.error('Failed to update watched status'); + showError('Failed to update watched status', 'Unable to update watched status'); } break; } diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 6f90751..f3177ab 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -47,7 +47,7 @@ import QualityBadge from '../components/metadata/QualityBadge'; import { logger } from '../utils/logger'; import { isMkvStream } from '../utils/mkvDetection'; import CustomAlert from '../components/CustomAlert'; -import { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import { useDownloads } from '../contexts/DownloadsContext'; import { PaperProvider } from 'react-native-paper'; @@ -227,6 +227,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the const { useSettings } = require('../hooks/useSettings'); const { settings } = useSettings(); const { startDownload } = useDownloads(); + const { showSuccess, showInfo } = useToast(); // Handle long press to copy stream URL to clipboard const handleLongPress = useCallback(async () => { @@ -236,7 +237,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the // Use toast for Android, custom alert for iOS if (Platform.OS === 'android') { - Toast.success('Stream URL copied to clipboard!', 'bottom'); + showSuccess('URL Copied', 'Stream URL copied to clipboard!'); } else { // iOS uses custom alert showAlert('Copied!', 'Stream URL has been copied to clipboard.'); @@ -244,13 +245,13 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the } catch (error) { // Fallback: show URL in alert if clipboard fails if (Platform.OS === 'android') { - Toast.info(`Stream URL: ${stream.url}`, 'bottom'); + showInfo('Stream URL', `Stream URL: ${stream.url}`); } else { showAlert('Stream URL', stream.url); } } } - }, [stream.url, showAlert]); + }, [stream.url, showAlert, showSuccess, showInfo]); const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const streamInfo = useMemo(() => { @@ -513,6 +514,7 @@ export const StreamsScreen = () => { const { currentTheme } = useTheme(); const { colors } = currentTheme; const { pauseTrailer, resumeTrailer } = useTrailer(); + const { showSuccess, showInfo } = useToast(); // Add refs to prevent excessive updates and duplicate loads const isMounted = useRef(true); diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index 9d9ea48..e795e03 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -11,7 +11,7 @@ import { Dimensions, Linking } from 'react-native'; -import { Toast } from 'toastify-react-native'; +import { useToast } from '../contexts/ToastContext'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -70,6 +70,7 @@ const UpdateScreen: React.FC = () => { const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); const github = useGithubMajorUpdate(); + const { showInfo } = useToast(); // CustomAlert state const [alertVisible, setAlertVisible] = useState(false); @@ -152,7 +153,7 @@ const UpdateScreen: React.FC = () => { // Also refresh GitHub section on mount (works in dev and prod) try { github.refresh(); } catch {} if (Platform.OS === 'android') { - try { Toast.info('Checking for updates…'); } catch {} + showInfo('Checking for Updates', 'Checking for updates…'); } }, []);