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 { 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>
|
||||
);
|
||||
};
|
@ -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} />;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 },
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user