Rework error provider

This commit is contained in:
Zoe Roux 2025-02-03 00:55:13 +01:00
parent 8710d49ed1
commit 5637ed0676
No known key found for this signature in database
7 changed files with 118 additions and 70 deletions

View File

@ -0,0 +1,43 @@
import { type ReactNode, createContext, useContext, useState } from "react";
import type { KyooError } from "~/models";
import { ErrorView, errorHandlers } from "~/ui/errors";
type Error = {
key: string;
error?: KyooError;
retry?: () => void;
};
const ErrorContext = createContext<{
error: Error | null;
setError: (error: Error | null) => void;
}>({ error: null, setError: () => {} });
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
const [error, setError] = useState<Error | null>(null);
return (
<ErrorContext.Provider
value={{
error,
setError,
}}
>
{children}
</ErrorContext.Provider>
);
};
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
const { error } = useContext(ErrorContext);
if (!error) return children;
const handler = errorHandlers[error.key] ?? { view: ErrorView };
if (handler.forbid && handler.forbid !== scope) return children;
const Handler = handler.view;
return <Handler {...(error as any)} />;
};
export const useSetError = () => {
const { setError } = useContext(ErrorContext);
return setError;
};

View File

@ -1,8 +1,9 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { type ReactNode, useState } from "react";
import { createQueryClient } from "~/query";
// import { useUserTheme } from "@kyoo/models";
import { ThemeSelector } from "~/primitives/theme";
import { createQueryClient } from "~/query";
import { ErrorConsumer, ErrorProvider } from "./error-provider";
const QueryProvider = ({ children }: { children: ReactNode }) => {
const [queryClient] = useState(() => createQueryClient());
@ -23,7 +24,11 @@ const ThemeProvider = ({ children }: { children: ReactNode }) => {
export const Providers = ({ children }: { children: ReactNode }) => {
return (
<QueryProvider>
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
<ErrorProvider>
<ErrorConsumer scope="root">{children}</ErrorConsumer>
</ErrorProvider>
</ThemeProvider>
</QueryProvider>
);
};

View File

@ -1,5 +1,6 @@
import type { ReactElement } from "react";
import { ErrorView, OfflineView } from "~/ui/errors";
import { useSetError } from "~/providers/error-provider";
import { ErrorView } from "~/ui/errors";
import { type QueryIdentifier, useFetch } from "./query";
export const Fetch = <Data,>({
@ -12,9 +13,17 @@ export const Fetch = <Data,>({
Loader: () => ReactElement;
}): JSX.Element | null => {
const { data, isPaused, error } = useFetch(query);
const setError = useSetError();
if (error) return <ErrorView error={error} />;
if (isPaused) return <OfflineView />;
if (error) {
if (error.status === 401 || error.status === 403) {
setError({ key: "unauthorized", error });
}
return <ErrorView error={error} />;
}
if (isPaused) {
setError({ key: "offline" });
}
if (!data) return <Loader />;
return <Render {...data} />;
};

View File

@ -1,59 +1,20 @@
import { ConnectionErrorContext, useAccount } from "@kyoo/models";
import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives";
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useRouter } from "solito/router";
import { useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../../../packages/ui/src/layout";
import type { KyooError } from "~/models";
import { Button, H1, Link, P, ts } from "~/primitives";
export const ConnectionError = () => {
export const ConnectionError = ({ error, retry }: { error: KyooError; retry: () => void }) => {
const { css } = useYoshiki();
const { t } = useTranslation();
const router = useRouter();
const { error, retry } = useContext(ConnectionErrorContext);
const account = useAccount();
if (error && (error.status === 401 || error.status === 403)) {
if (!account) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needAccount")}</P>
<Button
as={Link}
href={"/register"}
text={t("login.register")}
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
/>
</View>
);
}
if (!account.isVerified) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needVerification")}</P>
</View>
);
}
}
return (
<View {...css({ padding: ts(2) })}>
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
<P>{error?.errors[0] ?? t("errors.unknown")}</P>
<P>{error?.message ?? t("errors.unknown")}</P>
<P>{t("errors.connection-tips")}</P>
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
<Button
onPress={() => router.push("/login")}
text={t("errors.re-login")}
{...css({ m: ts(1) })}
/>
<Button as={Link} href="/login" text={t("errors.re-login")} {...css({ m: ts(1) })} />
</View>
);
};
ConnectionError.getLayout = DefaultLayout;

View File

@ -1,24 +1,15 @@
import { useContext, useLayoutEffect } from "react";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { ConnectionErrorContext, type KyooErrors } from "~/models";
import type { KyooError } from "~/models";
import { P } from "~/primitives";
export const ErrorView = ({
error,
noBubble = false,
}: {
error: KyooErrors;
noBubble?: boolean;
error: KyooError;
}) => {
const { css } = useYoshiki();
const { setError } = useContext(ConnectionErrorContext);
useLayoutEffect(() => {
// if this is a permission error, make it go up the tree to have a whole page login screen.
if (!noBubble && (error.status === 401 || error.status === 403)) setError(error);
}, [error, noBubble, setError]);
console.log(error);
return (
<View
{...css({
@ -29,11 +20,7 @@ export const ErrorView = ({
alignItems: "center",
})}
>
{error.errors.map((x, i) => (
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
{x}
</P>
))}
<P {...css({ color: (theme) => theme.colors.white })}>{error.message}</P>
</View>
);
};

View File

@ -1,6 +1,21 @@
import type { FC } from "react";
import type { KyooError } from "~/models";
import { ConnectionError } from "./connection";
import { OfflineView } from "./offline";
import { SetupPage } from "./setup";
import { Unauthorized } from "./unauthorized";
export * from "./error";
export * from "./unauthorized";
export * from "./connection";
export * from "./setup";
export * from "./empty";
export * from "./offline";
export type ErrorHandler = {
view: FC<{ error: KyooError; retry: () => void }>;
forbid?: string;
};
export const errorHandlers: Record<string, ErrorHandler> = {
unauthorized: { view: Unauthorized, forbid: "app" },
setup: { view: SetupPage, forbid: "setup" },
connection: { view: ConnectionError },
offline: { view: OfflineView },
};

View File

@ -1,11 +1,39 @@
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { P } from "~/primitives";
import { Button, Icon, Link, P, ts } from "~/primitives";
export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation();
const { css } = useYoshiki();
const account = useAccount();
if (!account) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needAccount")}</P>
<Button
as={Link}
href={"/register"}
text={t("login.register")}
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
/>
</View>
);
}
if (!account.isVerified) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needVerification")}</P>
</View>
);
}
return (
<View