Add auth guard and connection check on mobile

This commit is contained in:
Zoe Roux 2023-12-01 20:02:44 +01:00
parent 14319a5c89
commit 4c955e4115
3 changed files with 51 additions and 34 deletions

View File

@ -21,7 +21,7 @@
import { PortalProvider } from "@gorhom/portal"; import { PortalProvider } from "@gorhom/portal";
import { ThemeSelector } from "@kyoo/primitives"; import { ThemeSelector } from "@kyoo/primitives";
import { NavbarRight, NavbarTitle } from "@kyoo/ui"; import { NavbarRight, NavbarTitle } from "@kyoo/ui";
import { AccountProvider, createQueryClient, useAccounts } from "@kyoo/models"; import { AccountProvider, createQueryClient, useAccount, useAccounts } from "@kyoo/models";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import i18next from "i18next"; import i18next from "i18next";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
@ -92,37 +92,34 @@ const ThemedStack = ({ onLayout }: { onLayout?: () => void }) => {
); );
}; };
const AuthGuard = ({ selected }: { selected: number | null }) => { const AuthGuard = () => {
const router = useRouter(); const router = useRouter();
// TODO: support guest accounts on mobile too.
const account = useAccount();
useEffect(() => { useEffect(() => {
if (selected === null) if (account === null)
router.replace("/login", undefined, { router.replace("/login", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false }, experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
}); });
}, [selected, router]); }, [account, router]);
return null; return null;
}; };
let rendered: boolean = false;
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
export default function Root() { export default function Root() {
const [queryClient] = useState(() => createQueryClient()); const [queryClient] = useState(() => createQueryClient());
const theme = useColorScheme(); const theme = useColorScheme();
const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black }); const [fontsLoaded] = useFonts({ Poppins_300Light, Poppins_400Regular, Poppins_900Black });
const info = useAccounts();
const isReady = fontsLoaded && (rendered || info.type !== "loading");
useEffect(() => { useEffect(() => {
if (isReady) SplashScreen.hideAsync(); if (fontsLoaded) SplashScreen.hideAsync();
}, [isReady]); }, [fontsLoaded]);
if (!isReady) return null; if (!fontsLoaded) return null;
rendered = true;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AccountProvider>
<ThemeSelector <ThemeSelector
theme={theme ?? "light"} theme={theme ?? "light"}
font={{ font={{
@ -133,12 +130,15 @@ export default function Root() {
}} }}
> >
<PortalProvider> <PortalProvider>
{info.type === "loading" ? <CircularProgress /> : <ThemedStack />} <AccountProvider>
{info.type === "error" && <ConnectionError />} <>
{info.type === "ok" && <AuthGuard selected={info.selected} />} <ThemedStack />
<ConnectionError />
<AuthGuard />
</>
</AccountProvider>
</PortalProvider> </PortalProvider>
</ThemeSelector> </ThemeSelector>
</AccountProvider>
</QueryClientProvider> </QueryClientProvider>
); );
} }

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { AccountContext } from "@kyoo/models"; import { ConnectionErrorContext } from "@kyoo/models";
import { Button, H1, P, ts } from "@kyoo/primitives"; import { Button, H1, P, ts } from "@kyoo/primitives";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useContext } from "react"; import { useContext } from "react";
@ -30,12 +30,12 @@ const ConnectionError = () => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { error, retry } = useContext(AccountContext); const { error, retry } = useContext(ConnectionErrorContext);
return ( return (
<View {...css({ padding: ts(2) })}> <View {...css({ padding: ts(2) })}>
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1> <H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
<P>{error ?? t("error.unknown")}</P> <P>{error?.errors[0] ?? t("error.unknown")}</P>
<P>{t("errors.connection-tips")}</P> <P>{t("errors.connection-tips")}</P>
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} /> <Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
<Button <Button

View File

@ -27,6 +27,7 @@ import { useMMKVString } from "react-native-mmkv";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { queryFn, useFetch } from "./query"; import { queryFn, useFetch } from "./query";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { KyooErrors } from "./kyoo-errors";
export const TokenP = z.object({ export const TokenP = z.object({
token_type: z.literal("Bearer"), token_type: z.literal("Bearer"),
@ -47,6 +48,10 @@ export const AccountP = UserP.and(
export type Account = z.infer<typeof AccountP>; export type Account = z.infer<typeof AccountP>;
const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]); const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]);
export const ConnectionErrorContext = createContext<{
error: KyooErrors | null;
retry?: () => void;
}>({ error: null });
export const AccountProvider = ({ children }: { children: ReactNode }) => { export const AccountProvider = ({ children }: { children: ReactNode }) => {
const [accStr] = useMMKVString("accounts"); const [accStr] = useMMKVString("accounts");
@ -80,15 +85,27 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
const oldSelectedId = useRef<string | undefined>(selected?.id); const oldSelectedId = useRef<string | undefined>(selected?.id);
useEffect(() => { useEffect(() => {
// if the user change account (or connect/disconnect), reset query cache. // if the user change account (or connect/disconnect), reset query cache.
if (selected?.id !== oldSelectedId.current) if (selected?.id !== oldSelectedId.current) queryClient.invalidateQueries();
queryClient.invalidateQueries();
oldSelectedId.current = selected?.id; oldSelectedId.current = selected?.id;
// update cookies for ssr (needs to contains token, theme, language...) // update cookies for ssr (needs to contains token, theme, language...)
if (Platform.OS === "web") setAccountCookie(selected); if (Platform.OS === "web") setAccountCookie(selected);
}, [selected, queryClient]); }, [selected, queryClient]);
return <AccountContext.Provider value={accounts}>{children}</AccountContext.Provider>; return (
<AccountContext.Provider value={accounts}>
<ConnectionErrorContext.Provider
value={{
error: user.error,
retry: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
},
}}
>
{children}
</ConnectionErrorContext.Provider>
</AccountContext.Provider>
);
}; };
export const useAccount = () => { export const useAccount = () => {