/* eslint-disable @typescript-eslint/ban-types */ import { NativeStackNavigationProp, NativeStackScreenProps, createNativeStackNavigator, } from '@react-navigation/native-stack'; import { ParamListBase, useNavigation as navigationHook } from '@react-navigation/native'; import React, { ComponentProps, ComponentType, useEffect, useMemo } from 'react'; import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { RootState, useSelector } from './state/Store'; import { useDispatch } from 'react-redux'; import { Translate, translate } from './i18n/i18n'; import SearchView from './views/V2/SearchView'; import SettingsTab from './views/settings/SettingsView'; import { useQuery } from './Queries'; import API, { APIError } from './API'; import PlayView from './views/PlayView'; import { LoadingView } from './components/Loading'; import ProfileView from './views/ProfileView'; import useColorScheme from './hooks/colorScheme'; import ArtistDetailsView from './views/ArtistDetailsView'; import { Button, Center, VStack } from 'native-base'; import { unsetAccessToken } from './state/UserSlice'; import TextButton from './components/TextButton'; import ErrorView from './views/ErrorView'; import GenreDetailsView from './views/GenreDetailsView'; import GoogleView from './views/GoogleView'; import VerifiedView from './views/VerifiedView'; import SigninView from './views/SigninView'; import SignupView from './views/SignupView'; import PasswordResetView from './views/PasswordResetView'; import ForgotPasswordView from './views/ForgotPasswordView'; import DiscoveryView from './views/V2/DiscoveryView'; import MusicView from './views/MusicView'; import Leaderboardiew from './views/LeaderboardView'; import { LinearGradient } from 'expo-linear-gradient'; import { createCustomNavigator } from './utils/navigator'; import { Cup, Discover, Music, SearchNormal1, Setting2, User } from 'iconsax-react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const Stack = createNativeStackNavigator(); const Tab = createCustomNavigator(); const Tabs = () => { return ( {Object.entries(tabRoutes).map(([name, route], routeIndex) => ( ))} ); }; // Util function to hide route props in URL const removeMe = () => ''; const tabRoutes = { Home: { component: DiscoveryView, options: { headerShown: false, tabBarIcon: Discover }, link: '/', }, User: { component: ProfileView, options: { headerShown: false, tabBarIcon: User }, link: '/user', }, Music: { component: MusicView, options: { headerShown: false, tabBarIcon: Music }, link: '/music', }, Search: { component: SearchView, options: { headerShown: false, tabBarIcon: SearchNormal1 }, link: '/search/:query?', }, Leaderboard: { component: Leaderboardiew, options: { title: translate('leaderboardTitle'), headerShown: false, tabBarIcon: Cup }, link: '/leaderboard', }, Settings: { component: SettingsTab, options: { headerShown: false, tabBarIcon: Setting2, subMenu: true }, link: '/settings/:screen?', stringify: { screen: removeMe, }, }, }; const protectedRoutes = { Tabs: { component: Tabs, options: { headerShown: false, path: '' }, link: '', childRoutes: tabRoutes, }, Play: { component: PlayView, options: { headerShown: false, title: translate('play') }, link: '/play/:songId', }, Artist: { component: ArtistDetailsView, options: { title: translate('artistFilter') }, link: '/artist/:artistId', }, Genre: { component: GenreDetailsView, options: { title: translate('genreFilter') }, link: '/genre/:genreId', }, Error: { component: ErrorView, options: { title: translate('error'), headerLeft: null }, link: undefined, }, Verified: { component: VerifiedView, options: { title: 'Verify email', headerShown: false }, link: '/verify', }, }; const publicRoutes = { Login: { component: SigninView, options: { title: translate('signInBtn'), headerShown: false }, link: '/login', }, Signup: { component: SignupView, options: { title: translate('signUpBtn'), headerShown: false }, link: '/signup', }, Google: { component: GoogleView, options: { title: 'Google signin', headerShown: false }, link: '/logged/google', }, PasswordReset: { component: PasswordResetView, options: { title: 'Password reset form', headerShown: false }, link: '/password_reset', }, ForgotPassword: { component: ForgotPasswordView, options: { title: 'Password reset form', headerShown: false }, link: '/forgot_password', }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Route = { component: ComponentType; options: object; link?: string; }; // if the component has no props, ComponentProps return unknown so we remove those type RemoveNonObjects = [T] extends [{}] ? T : undefined; type RouteParams> = { [RouteName in keyof Routes]: RemoveNonObjects>; }; type PrivateRoutesParams = RouteParams; type PublicRoutesParams = RouteParams; type TabsRoutesParams = RouteParams; type AppRouteParams = Omit & { Tabs: { screen: keyof TabsRoutesParams }; } & PublicRoutesParams & TabsRoutesParams & { Oops: undefined }; const RouteToScreen = (Component: Route['component']) => function Route(props: NativeStackScreenProps) { const colorScheme = useColorScheme(); const insets = useSafeAreaInsets(); return ( ); }; const routesToScreens = (routes: Partial>) => Object.entries(routes).map(([name, route], routeIndex) => ( )); type RouteDescription = Record< string, { link?: string; stringify?: Record string>; childRoutes?: RouteDescription } >; const routesToLinkingConfig = (routes: RouteDescription) => { // Too lazy to (find the) type // eslint-disable-next-line @typescript-eslint/no-explicit-any const pagesToRoute = {} as Record; Object.keys(routes).forEach((route) => { const index = route as keyof AppRouteParams; if (routes[index]?.link !== undefined) { pagesToRoute[index] = { path: routes[index]!.link!, stringify: routes[index]!.stringify, screens: routes[index]!.childRoutes ? routesToLinkingConfig(routes[index]!.childRoutes!).config.screens : undefined, }; } }); return { prefixes: [], config: { screens: pagesToRoute }, }; }; const ProfileErrorView = (props: { onTryAgain: () => void }) => { const dispatch = useDispatch(); const navigation = useNavigation(); return (
{ dispatch(unsetAccessToken()); navigation.navigate('Login'); }} colorScheme="error" variant="outline" translate={{ translationKey: 'signOutBtn' }} />
); }; export const Router = () => { const dispatch = useDispatch(); const accessToken = useSelector((state: RootState) => state.user.accessToken); const userProfile = useQuery(API.getUserInfo, { retry: 1, refetchOnWindowFocus: false, onError: (err) => { if (err instanceof APIError && err.status === 401) { dispatch(unsetAccessToken()); } }, }); const colorScheme = useColorScheme(); const authStatus = useMemo(() => { if (userProfile.isError && accessToken && !userProfile.isLoading) { return 'error'; } if (userProfile.isLoading && !userProfile.data) { return 'loading'; } if (userProfile.isSuccess && accessToken) { return 'authed'; } return 'noAuth'; }, [userProfile, accessToken]); useEffect(() => { if (accessToken) { userProfile.refetch(); } }, [accessToken]); if (authStatus == 'loading') { // We dont want this to be a screen, as this lead to a navigator without the requested route, and fallback. return ; } const routes = authStatus == 'authed' ? { ...protectedRoutes } : publicRoutes; return ( } theme={colorScheme == 'light' ? DefaultTheme : DarkTheme} > {authStatus == 'error' ? ( <> ( userProfile.refetch()} /> ))} /> {routesToScreens(publicRoutes)} ) : ( routesToScreens(routes) )} ); }; export const useNavigation = () => navigationHook>();