mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Rework error provider
This commit is contained in:
parent
8710d49ed1
commit
5637ed0676
43
front/src/providers/error-provider.tsx
Normal file
43
front/src/providers/error-provider.tsx
Normal 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;
|
||||||
|
};
|
@ -1,8 +1,9 @@
|
|||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { createQueryClient } from "~/query";
|
|
||||||
// import { useUserTheme } from "@kyoo/models";
|
// import { useUserTheme } from "@kyoo/models";
|
||||||
import { ThemeSelector } from "~/primitives/theme";
|
import { ThemeSelector } from "~/primitives/theme";
|
||||||
|
import { createQueryClient } from "~/query";
|
||||||
|
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
||||||
|
|
||||||
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
@ -23,7 +24,11 @@ const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
export const Providers = ({ children }: { children: ReactNode }) => {
|
export const Providers = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<ErrorProvider>
|
||||||
|
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
||||||
|
</ErrorProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import type { ReactElement } from "react";
|
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";
|
import { type QueryIdentifier, useFetch } from "./query";
|
||||||
|
|
||||||
export const Fetch = <Data,>({
|
export const Fetch = <Data,>({
|
||||||
@ -12,9 +13,17 @@ export const Fetch = <Data,>({
|
|||||||
Loader: () => ReactElement;
|
Loader: () => ReactElement;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { data, isPaused, error } = useFetch(query);
|
const { data, isPaused, error } = useFetch(query);
|
||||||
|
const setError = useSetError();
|
||||||
|
|
||||||
if (error) return <ErrorView error={error} />;
|
if (error) {
|
||||||
if (isPaused) return <OfflineView />;
|
if (error.status === 401 || error.status === 403) {
|
||||||
|
setError({ key: "unauthorized", error });
|
||||||
|
}
|
||||||
|
return <ErrorView error={error} />;
|
||||||
|
}
|
||||||
|
if (isPaused) {
|
||||||
|
setError({ key: "offline" });
|
||||||
|
}
|
||||||
if (!data) return <Loader />;
|
if (!data) return <Loader />;
|
||||||
return <Render {...data} />;
|
return <Render {...data} />;
|
||||||
};
|
};
|
||||||
|
@ -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 { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useRouter } from "solito/router";
|
|
||||||
import { useYoshiki } from "yoshiki/native";
|
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 { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
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 (
|
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?.errors[0] ?? t("errors.unknown")}</P>
|
<P>{error?.message ?? t("errors.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 as={Link} href="/login" text={t("errors.re-login")} {...css({ m: ts(1) })} />
|
||||||
onPress={() => router.push("/login")}
|
|
||||||
text={t("errors.re-login")}
|
|
||||||
{...css({ m: ts(1) })}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ConnectionError.getLayout = DefaultLayout;
|
|
||||||
|
@ -1,24 +1,15 @@
|
|||||||
import { useContext, useLayoutEffect } from "react";
|
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { ConnectionErrorContext, type KyooErrors } from "~/models";
|
import type { KyooError } from "~/models";
|
||||||
import { P } from "~/primitives";
|
import { P } from "~/primitives";
|
||||||
|
|
||||||
export const ErrorView = ({
|
export const ErrorView = ({
|
||||||
error,
|
error,
|
||||||
noBubble = false,
|
|
||||||
}: {
|
}: {
|
||||||
error: KyooErrors;
|
error: KyooError;
|
||||||
noBubble?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { css } = useYoshiki();
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
{...css({
|
{...css({
|
||||||
@ -29,11 +20,7 @@ export const ErrorView = ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{error.errors.map((x, i) => (
|
<P {...css({ color: (theme) => theme.colors.white })}>{error.message}</P>
|
||||||
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
|
|
||||||
{x}
|
|
||||||
</P>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 "./error";
|
||||||
export * from "./unauthorized";
|
|
||||||
export * from "./connection";
|
|
||||||
export * from "./setup";
|
|
||||||
export * from "./empty";
|
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 },
|
||||||
|
};
|
||||||
|
@ -1,11 +1,39 @@
|
|||||||
|
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { P } from "~/primitives";
|
import { Button, Icon, Link, P, ts } from "~/primitives";
|
||||||
|
|
||||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { css } = useYoshiki();
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
|
Loading…
x
Reference in New Issue
Block a user