From 12c7d8f86077ccffada0fbebc4205f2788750ded Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 30 Jun 2025 13:13:08 +0530 Subject: [PATCH] added onboarding --- App.tsx | 26 +- src/components/FirstTimeWelcome.tsx | 106 ++++++++ src/navigation/AppNavigator.tsx | 17 +- src/screens/HomeScreen.tsx | 19 +- src/screens/OnboardingScreen.tsx | 373 ++++++++++++++++++++++++++++ src/screens/SettingsScreen.tsx | 26 +- src/screens/index.ts | 4 +- 7 files changed, 560 insertions(+), 11 deletions(-) create mode 100644 src/components/FirstTimeWelcome.tsx create mode 100644 src/screens/OnboardingScreen.tsx diff --git a/App.tsx b/App.tsx index a0707f5..82d898f 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,7 @@ * @format */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, StyleSheet @@ -25,6 +25,7 @@ import { GenreProvider } from './src/contexts/GenreContext'; import { TraktProvider } from './src/contexts/TraktContext'; import { ThemeProvider, useTheme } from './src/contexts/ThemeContext'; import SplashScreen from './src/components/SplashScreen'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Sentry from '@sentry/react-native'; Sentry.init({ @@ -50,6 +51,23 @@ enableScreens(true); const ThemedApp = () => { const { currentTheme } = useTheme(); const [isAppReady, setIsAppReady] = useState(false); + const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(null); + + // Check onboarding status + useEffect(() => { + const checkOnboardingStatus = async () => { + try { + const onboardingCompleted = await AsyncStorage.getItem('hasCompletedOnboarding'); + setHasCompletedOnboarding(onboardingCompleted === 'true'); + } catch (error) { + console.error('Error checking onboarding status:', error); + // Default to showing onboarding if we can't check + setHasCompletedOnboarding(false); + } + }; + + checkOnboardingStatus(); + }, []); // Create custom themes based on current theme const customDarkTheme = { @@ -75,6 +93,10 @@ const ThemedApp = () => { setIsAppReady(true); }; + // Don't render anything until we know the onboarding status + const shouldShowApp = isAppReady && hasCompletedOnboarding !== null; + const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding'; + return ( { style="light" /> {!isAppReady && } - {isAppReady && } + {shouldShowApp && } diff --git a/src/components/FirstTimeWelcome.tsx b/src/components/FirstTimeWelcome.tsx new file mode 100644 index 0000000..f5edee5 --- /dev/null +++ b/src/components/FirstTimeWelcome.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import Animated, { FadeInDown } from 'react-native-reanimated'; +import { useTheme } from '../contexts/ThemeContext'; +import { NavigationProp, useNavigation } from '@react-navigation/native'; +import { RootStackParamList } from '../navigation/AppNavigator'; + +const { width } = Dimensions.get('window'); + +const FirstTimeWelcome = () => { + const { currentTheme } = useTheme(); + const navigation = useNavigation>(); + + return ( + + + + + + + Welcome to Nuvio! + + + + To get started, install some addons to access content from various sources. + + + navigation.navigate('Addons')} + > + + Install Addons + + + ); +}; + +const styles = StyleSheet.create({ + container: { + margin: 16, + padding: 24, + borderRadius: 16, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + iconContainer: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 16, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + marginBottom: 8, + textAlign: 'center', + }, + description: { + fontSize: 14, + textAlign: 'center', + lineHeight: 20, + marginBottom: 20, + maxWidth: width * 0.7, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 25, + gap: 8, + }, + buttonText: { + color: 'white', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default FirstTimeWelcome; \ No newline at end of file diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index a499fcb..d09acb9 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -39,9 +39,11 @@ import PlayerSettingsScreen from '../screens/PlayerSettingsScreen'; import LogoSourceSettings from '../screens/LogoSourceSettings'; import ThemeScreen from '../screens/ThemeScreen'; import ProfilesScreen from '../screens/ProfilesScreen'; +import OnboardingScreen from '../screens/OnboardingScreen'; // Stack navigator types export type RootStackParamList = { + Onboarding: undefined; MainTabs: undefined; Home: undefined; Discover: undefined; @@ -688,7 +690,7 @@ const customFadeInterpolator = ({ current, layouts }: any) => { }; // Stack Navigator -const AppNavigator = () => { +const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => { const { currentTheme } = useTheme(); // Handle Android-specific optimizations @@ -717,6 +719,7 @@ const AppNavigator = () => { }) }}> { }), }} > + { const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]); const [catalogsLoading, setCatalogsLoading] = useState(true); const [loadedCatalogCount, setLoadedCatalogCount] = useState(0); + const [hasAddons, setHasAddons] = useState(null); const totalCatalogsRef = useRef(0); const { @@ -136,6 +139,9 @@ const HomeScreen = () => { try { const addons = await catalogService.getAllAddons(); + // Set hasAddons state based on whether we have any addons + setHasAddons(addons.length > 0); + // Load catalog settings to check which catalogs are enabled const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {}; @@ -469,6 +475,13 @@ const HomeScreen = () => { const listData: HomeScreenListItem[] = useMemo(() => { const data: HomeScreenListItem[] = []; + // If no addons are installed, just show the welcome component + if (hasAddons === false) { + data.push({ type: 'welcome', key: 'welcome' }); + return data; + } + + // Normal flow when addons are present if (showHeroSection) { data.push({ type: 'featured', key: 'featured' }); } @@ -486,7 +499,7 @@ const HomeScreen = () => { }); return data; - }, [showHeroSection, catalogs]); + }, [hasAddons, showHeroSection, catalogs]); const renderListItem = useCallback(({ item }: { item: HomeScreenListItem }) => { switch (item.type) { @@ -526,6 +539,8 @@ const HomeScreen = () => { ); + case 'welcome': + return ; default: return null; } diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx new file mode 100644 index 0000000..e81f18a --- /dev/null +++ b/src/screens/OnboardingScreen.tsx @@ -0,0 +1,373 @@ +import React, { useState, useRef } from 'react'; +import { + View, + Text, + StyleSheet, + Dimensions, + TouchableOpacity, + FlatList, + Image, + StatusBar, + Platform, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { MaterialIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + withTiming, + FadeInDown, + FadeInUp, +} from 'react-native-reanimated'; +import { useTheme } from '../contexts/ThemeContext'; +import { NavigationProp, useNavigation } from '@react-navigation/native'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const { width, height } = Dimensions.get('window'); + +interface OnboardingSlide { + id: string; + title: string; + subtitle: string; + description: string; + icon: keyof typeof MaterialIcons.glyphMap; + gradient: [string, string]; +} + +const onboardingData: OnboardingSlide[] = [ + { + id: '1', + title: 'Welcome to Nuvio', + subtitle: 'Your Ultimate Content Hub', + description: 'Discover, organize, and manage your favorite movies and TV shows from multiple sources in one beautiful app.', + icon: 'play-circle-filled', + gradient: ['#667eea', '#764ba2'], + }, + { + id: '2', + title: 'Powerful Addons', + subtitle: 'Extend Your Experience', + description: 'Install addons to access content from various platforms and services. Choose what works best for you.', + icon: 'extension', + gradient: ['#f093fb', '#f5576c'], + }, + { + id: '3', + title: 'Smart Discovery', + subtitle: 'Find What You Love', + description: 'Browse trending content, search across all your sources, and get personalized recommendations.', + icon: 'explore', + gradient: ['#4facfe', '#00f2fe'], + }, + { + id: '4', + title: 'Your Library', + subtitle: 'Track & Organize', + description: 'Save favorites, track your progress, and sync with Trakt to keep everything organized across devices.', + icon: 'library-books', + gradient: ['#43e97b', '#38f9d7'], + }, +]; + +const OnboardingScreen = () => { + const { currentTheme } = useTheme(); + const navigation = useNavigation>(); + const [currentIndex, setCurrentIndex] = useState(0); + const flatListRef = useRef(null); + const progressValue = useSharedValue(0); + + const animatedProgressStyle = useAnimatedStyle(() => ({ + width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`), + })); + + const handleNext = () => { + if (currentIndex < onboardingData.length - 1) { + const nextIndex = currentIndex + 1; + setCurrentIndex(nextIndex); + flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true }); + progressValue.value = (nextIndex + 1) / onboardingData.length; + } else { + handleGetStarted(); + } + }; + + const handleSkip = () => { + handleGetStarted(); + }; + + const handleGetStarted = async () => { + try { + await AsyncStorage.setItem('hasCompletedOnboarding', 'true'); + navigation.reset({ + index: 0, + routes: [{ name: 'MainTabs' }], + }); + } catch (error) { + console.error('Error saving onboarding status:', error); + navigation.reset({ + index: 0, + routes: [{ name: 'MainTabs' }], + }); + } + }; + + const renderSlide = ({ item, index }: { item: OnboardingSlide; index: number }) => { + const isActive = index === currentIndex; + + return ( + + + + + + + + + + {item.title} + + + {item.subtitle} + + + {item.description} + + + + ); + }; + + const renderPagination = () => ( + + {onboardingData.map((_, index) => ( + + ))} + + ); + + return ( + + + + {/* Header */} + + + + Skip + + + + {/* Progress Bar */} + + + + + + {/* Content */} + item.id} + onMomentumScrollEnd={(event) => { + const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width); + setCurrentIndex(slideIndex); + }} + style={{ flex: 1 }} + /> + + {/* Footer */} + + {renderPagination()} + + + + + {currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 20, + paddingTop: Platform.OS === 'ios' ? 10 : 20, + paddingBottom: 20, + }, + skipButton: { + padding: 10, + }, + skipText: { + fontSize: 16, + fontWeight: '500', + }, + progressContainer: { + flex: 1, + height: 4, + borderRadius: 2, + marginHorizontal: 20, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + borderRadius: 2, + }, + slide: { + width, + height: '100%', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 40, + }, + iconContainer: { + width: 160, + height: 160, + borderRadius: 80, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 60, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 10, + }, + shadowOpacity: 0.3, + shadowRadius: 20, + elevation: 15, + }, + iconWrapper: { + alignItems: 'center', + justifyContent: 'center', + }, + textContainer: { + alignItems: 'center', + paddingHorizontal: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 8, + }, + subtitle: { + fontSize: 18, + fontWeight: '600', + textAlign: 'center', + marginBottom: 16, + }, + description: { + fontSize: 16, + textAlign: 'center', + lineHeight: 24, + maxWidth: 280, + }, + footer: { + paddingHorizontal: 20, + paddingBottom: Platform.OS === 'ios' ? 40 : 20, + }, + pagination: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 40, + }, + paginationDot: { + width: 8, + height: 8, + borderRadius: 4, + marginHorizontal: 4, + }, + buttonContainer: { + alignItems: 'center', + }, + button: { + borderRadius: 30, + paddingVertical: 16, + paddingHorizontal: 32, + minWidth: 160, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + nextButton: { + // Additional styles for next button can go here + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + }, + buttonIcon: { + marginLeft: 8, + }, +}); + +export default OnboardingScreen; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 059e1f4..e6ebdec 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -607,10 +607,28 @@ const SettingsScreen: React.FC = () => { Linking.openURL('https://github.com/Stremio/stremio-expo/blob/main/PRIVACY_POLICY.md').catch(err => console.error("Couldn't load page", err))} - isLast={true} + icon="privacy-tip" + onPress={() => Linking.openURL('https://your-privacy-policy-url.com')} + renderControl={() => } + /> + navigation.navigate('Onboarding')} + renderControl={() => } + /> + { + try { + await AsyncStorage.removeItem('hasCompletedOnboarding'); + Alert.alert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); + } catch (error) { + Alert.alert('Error', 'Failed to reset onboarding.'); + } + }} + renderControl={() => } /> diff --git a/src/screens/index.ts b/src/screens/index.ts index 725e3ed..46e6bab 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,6 +1,5 @@ // Export all screens from a single file export { default as HomeScreen } from './HomeScreen'; -export { default as PlayerScreen } from './PlayerScreen'; export { default as SearchScreen } from './SearchScreen'; export { default as AddonsScreen } from './AddonsScreen'; export { default as SettingsScreen } from './SettingsScreen'; @@ -10,4 +9,5 @@ export { default as DiscoverScreen } from './DiscoverScreen'; export { default as LibraryScreen } from './LibraryScreen'; export { default as ShowRatingsScreen } from './ShowRatingsScreen'; export { default as CatalogSettingsScreen } from './CatalogSettingsScreen'; -export { default as StreamsScreen } from './StreamsScreen'; \ No newline at end of file +export { default as StreamsScreen } from './StreamsScreen'; +export { default as OnboardingScreen } from './OnboardingScreen'; \ No newline at end of file