mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add error consumer for unauthorized
This commit is contained in:
parent
b4144e61d3
commit
6cfeba6c62
10
front/app/(app)/_layout.tsx
Normal file
10
front/app/(app)/_layout.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Slot } from "one";
|
||||
import { ErrorConsumer } from "~/providers/error-provider";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<ErrorConsumer scope="app">
|
||||
<Slot />
|
||||
</ErrorConsumer>
|
||||
);
|
||||
}
|
2
front/routes.d.ts
vendored
2
front/routes.d.ts
vendored
@ -6,7 +6,7 @@ import type { OneRouter } from 'one'
|
||||
declare module 'one' {
|
||||
export namespace OneRouter {
|
||||
export interface __routes<T extends string = string> extends Record<string, unknown> {
|
||||
StaticRoutes: `/` | `/_sitemap`
|
||||
StaticRoutes: `/` | `/(app)` | `/_sitemap`
|
||||
DynamicRoutes: never
|
||||
DynamicRouteTemplate: never
|
||||
IsTyped: true
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type ReactNode, createContext, useEffect, useMemo, useRef } from "react";
|
||||
import { type ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { z } from "zod";
|
||||
import { type Account, AccountP, type Token, UserP } from "~/models";
|
||||
@ -8,14 +8,12 @@ import { removeAccounts, updateAccount } from "./account-store";
|
||||
import { useSetError } from "./error-provider";
|
||||
import { useStoreValue } from "./settings";
|
||||
|
||||
export const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
|
||||
|
||||
export const AccountContext = createContext<{
|
||||
apiUrl: string;
|
||||
authToken: Token | null;
|
||||
selectedAccount: Account | null;
|
||||
accounts: (Account & { select: () => void; remove: () => void })[];
|
||||
}>({ apiUrl: ssrApiUrl, authToken: null, selectedAccount: null, accounts: [] });
|
||||
}>({ apiUrl: "/api", authToken: null, selectedAccount: null, accounts: [] });
|
||||
|
||||
export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [setError, clearError] = useSetError("account");
|
||||
@ -26,7 +24,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
return {
|
||||
apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl!,
|
||||
authToken: acc?.token ?? null,
|
||||
selectedAccount: acc,
|
||||
selectedAccount: acc ?? null,
|
||||
accounts: accounts.map((account) => ({
|
||||
...account,
|
||||
select: () => updateAccount(account.id, { ...account, selected: true }),
|
||||
@ -51,6 +49,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
authToken: ret.authToken?.access_token,
|
||||
},
|
||||
});
|
||||
console.log(user);
|
||||
// Use a ref here because we don't want the effect to trigger when the selected
|
||||
// value has changed, only when the fetch result changed
|
||||
// If we trigger the effect when the selected value change, we enter an infinite render loop
|
||||
@ -59,7 +58,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
useEffect(() => {
|
||||
if (!selectedRef.current || !userIsSuccess || userIsPlaceholder) return;
|
||||
// The id is different when user is stale data, we need to wait for the use effect to invalidate the query.
|
||||
if (user.id !== selectedRef.current.id) return;
|
||||
if (user?.id !== selectedRef.current.id) return;
|
||||
const nUser = { ...selectedRef.current, ...user };
|
||||
updateAccount(nUser.id, nUser);
|
||||
}, [user, userIsSuccess, userIsPlaceholder]);
|
||||
@ -83,3 +82,8 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
return <AccountContext.Provider value={ret}>{children}</AccountContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAccount = () => {
|
||||
const { selectedAccount } = useContext(AccountContext);
|
||||
return selectedAccount;
|
||||
};
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { type ReactNode, createContext, useContext, useState } from "react";
|
||||
import {
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { KyooError } from "~/models";
|
||||
import { ErrorView, errorHandlers } from "~/ui/errors";
|
||||
|
||||
@ -8,41 +16,49 @@ type Error = {
|
||||
retry?: () => void;
|
||||
};
|
||||
|
||||
const ErrorContext = createContext<{
|
||||
error: Error | null;
|
||||
const ErrorContext = createContext<Error | null>(null);
|
||||
const ErrorSetterContext = createContext<{
|
||||
setError: (error: Error | null) => void;
|
||||
}>({ error: null, setError: () => {} });
|
||||
clearError: (key: string) => void;
|
||||
}>(null!);
|
||||
|
||||
export const ErrorProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const currentKey = useRef(error?.key);
|
||||
currentKey.current = error?.key;
|
||||
const clearError = useCallback((key: string) => {
|
||||
if (key === currentKey.current) setError(null);
|
||||
}, []);
|
||||
const provider = useMemo(
|
||||
() => ({
|
||||
setError,
|
||||
clearError,
|
||||
}),
|
||||
[clearError],
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorContext.Provider
|
||||
value={{
|
||||
error,
|
||||
setError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ErrorContext.Provider>
|
||||
<ErrorSetterContext.Provider value={provider}>
|
||||
<ErrorContext.Provider value={error}>{children}</ErrorContext.Provider>
|
||||
</ErrorSetterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorConsumer = ({ children, scope }: { children: ReactNode; scope: string }) => {
|
||||
const { error } = useContext(ErrorContext);
|
||||
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)} />;
|
||||
const { key, ...val } = error;
|
||||
return <Handler {...(val as any)} />;
|
||||
};
|
||||
export const useSetError = (key: string) => {
|
||||
const { error, setError } = useContext(ErrorContext);
|
||||
const set = ({ key: nKey, ...obj }: Error & { key?: Error["key"] }) =>
|
||||
const { setError, clearError } = useContext(ErrorSetterContext);
|
||||
const set = ({ key: nKey, ...obj }: Omit<Error, "key"> & { key?: Error["key"] }) =>
|
||||
setError({ key: nKey ?? key, ...obj });
|
||||
const clearError = () => {
|
||||
if (error?.key === key) setError(null);
|
||||
};
|
||||
return [set, clearError] as const;
|
||||
const clear = () => clearError(key);
|
||||
return [set, clear] as const;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { useLayoutEffect, type ReactElement } from "react";
|
||||
import { useSetError } from "~/providers/error-provider";
|
||||
import { ErrorView } from "~/ui/errors";
|
||||
import { type QueryIdentifier, useFetch } from "./query";
|
||||
@ -15,15 +15,18 @@ export const Fetch = <Data,>({
|
||||
const { data, isPaused, error } = useFetch(query);
|
||||
const [setError] = useSetError("fetch");
|
||||
|
||||
if (error) {
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
useLayoutEffect(() => {
|
||||
if (isPaused) {
|
||||
setError({ key: "offline" });
|
||||
}
|
||||
if (error && (error.status === 401 || error.status === 403)) {
|
||||
setError({ key: "unauthorized", error });
|
||||
}
|
||||
}, [error, isPaused]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
if (isPaused) {
|
||||
setError({ key: "offline" });
|
||||
}
|
||||
if (!data) return <Loader />;
|
||||
return <Render {...data} />;
|
||||
};
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { QueryClient, dehydrate, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { setServerData } from "one";
|
||||
import { useContext } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import type { z } from "zod";
|
||||
import { type KyooError, type Page, Paged } from "~/models";
|
||||
import { AccountContext, ssrApiUrl } from "~/providers/account-provider";
|
||||
import { AccountContext } from "~/providers/account-provider";
|
||||
|
||||
const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api";
|
||||
|
||||
const cleanSlash = (str: string | null, keepFirst = false) => {
|
||||
if (!str) return null;
|
||||
@ -21,6 +24,8 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
||||
parser?: Parser;
|
||||
signal: AbortSignal;
|
||||
}): Promise<z.infer<Parser>> => {
|
||||
if (Platform.OS === "web" && typeof window === "undefined" && context.url.startsWith("/api"))
|
||||
context.url = `${ssrApiUrl}/${context.url.substring(4)}`;
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(context.url, {
|
||||
@ -55,11 +60,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
|
||||
data = { message: error } as KyooError;
|
||||
}
|
||||
data.status = resp.status;
|
||||
console.trace(
|
||||
`Invalid response (${context.method ?? "GET"} ${context.url}):`,
|
||||
data,
|
||||
resp.status,
|
||||
);
|
||||
console.log(`Invalid response (${context.method ?? "GET"} ${context.url}):`, data, resp.status);
|
||||
throw data as KyooError;
|
||||
}
|
||||
|
||||
@ -199,39 +200,41 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
|
||||
const authToken = undefined;
|
||||
|
||||
await Promise.all(
|
||||
queries.map((query) => {
|
||||
const key = toQueryKey({
|
||||
apiUrl: ssrApiUrl,
|
||||
path: query.path,
|
||||
params: query.params,
|
||||
});
|
||||
queries
|
||||
.filter((x) => x.enabled !== false)
|
||||
.map((query) => {
|
||||
const key = toQueryKey({
|
||||
apiUrl: ssrApiUrl,
|
||||
path: query.path,
|
||||
params: query.params,
|
||||
});
|
||||
|
||||
if (query.infinite) {
|
||||
return client.prefetchInfiniteQuery({
|
||||
if (query.infinite) {
|
||||
return client.prefetchInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: (ctx) =>
|
||||
queryFn({
|
||||
url: key.join("/").replace("/?", "?"),
|
||||
parser: Paged(query.parser),
|
||||
signal: ctx.signal,
|
||||
authToken: authToken?.access_token ?? null,
|
||||
...query.options,
|
||||
}),
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
return client.prefetchQuery({
|
||||
queryKey: key,
|
||||
queryFn: (ctx) =>
|
||||
queryFn({
|
||||
url: key.join("/").replace("/?", "?"),
|
||||
parser: Paged(query.parser),
|
||||
parser: query.parser,
|
||||
signal: ctx.signal,
|
||||
authToken: authToken?.access_token ?? null,
|
||||
...query.options,
|
||||
}),
|
||||
initialPageParam: undefined,
|
||||
});
|
||||
}
|
||||
return client.prefetchQuery({
|
||||
queryKey: key,
|
||||
queryFn: (ctx) =>
|
||||
queryFn({
|
||||
url: key.join("/").replace("/?", "?"),
|
||||
parser: query.parser,
|
||||
signal: ctx.signal,
|
||||
authToken: authToken?.access_token ?? null,
|
||||
...query.options,
|
||||
}),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
);
|
||||
setServerData("queryState", dehydrate(client));
|
||||
return client;
|
||||
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Button, Icon, Link, P, ts } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-provider";
|
||||
|
||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
Loading…
x
Reference in New Issue
Block a user