diff --git a/front/public/translations/en.json b/front/public/translations/en.json index 62f7899a..84fc33c2 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -246,6 +246,7 @@ "try-again": "Try again", "re-login": "Re login", "offline": "You are not connected to internet. Try again later.", + "invalid-server": "Could not reach kyoo's server.", "unauthorized": "You are missing the permissions {{permission}} to access this page.", "needVerification": "Your account needs to be verified by your server administrator before you can use it.", "needAccount": "This page can't be accessed in guest mode. You need to create an account or login.", diff --git a/front/src/primitives/links.tsx b/front/src/primitives/links.tsx index 9c606a41..8d2a2ff1 100644 --- a/front/src/primitives/links.tsx +++ b/front/src/primitives/links.tsx @@ -8,10 +8,8 @@ import { Text, type TextProps, } from "react-native"; -import { useTheme } from "yoshiki/native"; -import { cn } from "~/utils"; -import { alpha } from "./theme"; import { useResolveClassNames } from "uniwind"; +import { cn } from "~/utils"; export function useLinkTo({ href, diff --git a/front/src/providers/account-provider.tsx b/front/src/providers/account-provider.tsx index 0d4e099a..602e3cf6 100644 --- a/front/src/providers/account-provider.tsx +++ b/front/src/providers/account-provider.tsx @@ -1,5 +1,7 @@ import { useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "expo-router"; import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { Platform } from "react-native"; import { z } from "zod/v4"; import { Account, User } from "~/models"; import { RetryableError } from "~/models/retryable-error"; @@ -11,10 +13,11 @@ import { useStoreValue } from "./settings"; export const defaultApiUrl = ""; export const AccountProvider = ({ children }: { children: ReactNode }) => { + const queryClient = useQueryClient(); const accounts = useStoreValue("accounts", z.array(Account)) ?? []; const ret = useMemo(() => { - const acc = accounts.find((x) => x.selected); + const acc = accounts.find((x) => x.selected) ?? accounts[0]; return { apiUrl: acc?.apiUrl ?? defaultApiUrl, authToken: acc?.token ?? null, @@ -27,6 +30,20 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => { }; }, [accounts]); + if (Platform.OS !== "web") { + // biome-ignore lint/correctness/useHookAtTopLevel: static + const router = useRouter(); + // biome-ignore lint/correctness/useHookAtTopLevel: static + useEffect(() => { + if (!ret.apiUrl) { + setTimeout(() => { + console.log("go to login"); + router.replace("/login"); + }, 0); + } + }, [ret.apiUrl, router]); + } + // update user's data from kyoo on startup, it could have changed. const { isSuccess: userIsSuccess, @@ -62,7 +79,6 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => { updateAccount(nUser.id, nUser); }, [user, userIsSuccess, userIsPlaceholder]); - const queryClient = useQueryClient(); const selectedId = ret.selectedAccount?.id; useEffect(() => { selectedId; diff --git a/front/src/providers/index.tsx b/front/src/providers/index.tsx index d6d986e2..4aa3a76e 100644 --- a/front/src/providers/index.tsx +++ b/front/src/providers/index.tsx @@ -83,11 +83,11 @@ export const Providers = ({ children }: { children: ReactNode }) => { - - + + {children} - - + + diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx index 347a58ff..67deca0e 100644 --- a/front/src/query/query.tsx +++ b/front/src/query/query.tsx @@ -59,10 +59,9 @@ export const queryFn = async (context: { if (typeof e === "object" && e && "name" in e && e.name === "AbortError") throw { message: "Aborted", status: "aborted" } as KyooError; console.log("Fetch error", e, context.url); - throw { - message: "Could not reach Kyoo's server.", - status: "aborted", - } as KyooError; + throw new RetryableError({ + key: "offline", + }); } if (resp.status === 404) { throw { message: "Resource not found.", status: 404 } as KyooError; @@ -144,6 +143,7 @@ export type QueryIdentifier = { enabled?: boolean; options?: Partial[0]> & { apiUrl?: string; + returnError?: boolean; }; }; @@ -189,14 +189,16 @@ export const useFetch = (query: QueryIdentifier) => { enabled: query.enabled, }); - if (ret.isPaused) throw new RetryableError({ key: "offline" }); - if (ret.error && (ret.error.status === 401 || ret.error.status === 403)) { - throw new RetryableError({ - key: !selectedAccount ? "needAccount" : "unauthorized", - inner: ret.error, - }); + if (query.options?.returnError !== true) { + if (ret.isPaused) throw new RetryableError({ key: "offline" }); + if (ret.error && (ret.error.status === 401 || ret.error.status === 403)) { + throw new RetryableError({ + key: !selectedAccount ? "needAccount" : "unauthorized", + inner: ret.error, + }); + } + if (ret.error) throw ret.error; } - if (ret.error) throw ret.error; return ret; }; diff --git a/front/src/ui/login/form.tsx b/front/src/ui/login/form.tsx index f4b50de7..cefc078c 100644 --- a/front/src/ui/login/form.tsx +++ b/front/src/ui/login/form.tsx @@ -1,20 +1,25 @@ import type { ReactNode } from "react"; -import { ImageBackground, ScrollView, View } from "react-native"; -import Svg, { Path, type SvgProps } from "react-native-svg"; -import { min, px, type Stylable, useYoshiki, vh } from "yoshiki/native"; -import { ts } from "~/primitives"; +import { + ImageBackground, + ScrollView, + View, + type ViewProps, +} from "react-native"; +import { Path } from "react-native-svg"; +import { Svg } from "~/primitives"; import { defaultApiUrl } from "~/providers/account-provider"; +import { cn } from "~/utils"; -const SvgBlob = (props: SvgProps) => { - const { css, theme } = useYoshiki(); - +const SvgBlob = ({ className, ...props }: ViewProps) => { return ( - - - + + + ); @@ -23,37 +28,26 @@ const SvgBlob = (props: SvgProps) => { export const FormPage = ({ children, apiUrl, + className, ...props -}: { children: ReactNode; apiUrl?: string } & Stylable) => { - const { css } = useYoshiki(); - +}: { + children: ReactNode; + apiUrl?: string; + className?: string; +}) => { return ( theme.dark.background, - })} + className="flex-1 flex-row bg-dark" > - - + + theme.background, - borderBottomRightRadius: ts(25), - paddingBottom: ts(5), - paddingLeft: ts(3), - }, - props, + className={cn( + "max-w-xl rounded-[25rem] bg-background py-10 pl-6", + className, )} + {...props} > {children} diff --git a/front/src/ui/login/login.tsx b/front/src/ui/login/login.tsx index abcbc1a3..5ba11cda 100644 --- a/front/src/ui/login/login.tsx +++ b/front/src/ui/login/login.tsx @@ -2,8 +2,7 @@ import { useRouter } from "expo-router"; import { useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Platform } from "react-native"; -import { percent, px, useYoshiki } from "yoshiki/native"; -import { A, Button, H1, Input, P, ts } from "~/primitives"; +import { A, Button, H1, Input, P } from "~/primitives"; import { defaultApiUrl } from "~/providers/account-provider"; import { useQueryState } from "~/utils"; import { FormPage } from "./form"; @@ -18,31 +17,25 @@ export const LoginPage = () => { const [error, setError] = useState(); const { t } = useTranslation(); - const { css } = useYoshiki(); const router = useRouter(); if (Platform.OS !== "web" && !apiUrl) return ; return ( - {t("login.login")} - {/* */} - {/* {data?.passwordLoginEnabled && ( */} - {/* <> */} - {t("login.username")} + {t("login.login")} + {t("login.username")} setUsername(value)} autoCapitalize="none" /> - {t("login.password")} + {t("login.password")} setPassword(value)} /> - {error && theme.colors.red })}>{error}} + {error && {error}} { @@ -55,16 +48,8 @@ export const LoginPage = () => { if (error) return; router.replace("/"); }} - {...css({ - m: ts(1), - width: px(250), - maxWidth: percent(100), - alignSelf: "center", - mY: ts(3), - })} + className="m-2 my-6 w-60 self-center" /> - {/* > */} - {/* )} */} Don’t have an account? diff --git a/front/src/ui/login/password-input.tsx b/front/src/ui/login/password-input.tsx index d395c008..2ca17927 100644 --- a/front/src/ui/login/password-input.tsx +++ b/front/src/ui/login/password-input.tsx @@ -1,11 +1,9 @@ import VisibilityOff from "@material-symbols/svg-400/rounded/visibility_off-fill.svg"; import Visibility from "@material-symbols/svg-400/rounded/visibility-fill.svg"; import { type ComponentProps, useState } from "react"; -import { px, useYoshiki } from "yoshiki/native"; import { IconButton, Input } from "~/primitives"; export const PasswordInput = (props: ComponentProps) => { - const { css } = useYoshiki(); const [show, setVisibility] = useState(false); return ( @@ -14,9 +12,7 @@ export const PasswordInput = (props: ComponentProps) => { right={ setVisibility(!show)} - {...css({ width: px(19), height: px(19), m: 0, p: 0 })} /> } {...props} diff --git a/front/src/ui/login/register.tsx b/front/src/ui/login/register.tsx index 6aa9346c..b09f2654 100644 --- a/front/src/ui/login/register.tsx +++ b/front/src/ui/login/register.tsx @@ -2,8 +2,7 @@ import { useRouter } from "expo-router"; import { useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Platform } from "react-native"; -import { percent, px, useYoshiki } from "yoshiki/native"; -import { A, Button, H1, Input, P, ts } from "~/primitives"; +import { A, Button, H1, Input, P } from "~/primitives"; import { defaultApiUrl } from "~/providers/account-provider"; import { useQueryState } from "~/utils"; import { FormPage } from "./form"; @@ -21,50 +20,39 @@ export const RegisterPage = () => { const router = useRouter(); const { t } = useTranslation(); - const { css } = useYoshiki(); if (Platform.OS !== "web" && !apiUrl) return ; return ( - {t("login.register")} - {/* */} - {/* {data?.registrationEnabled && ( */} - {/* <> */} - {t("login.username")} + {t("login.register")} + {t("login.username")} setUsername(value)} /> - {t("login.email")} - setEmail(value)} - /> + {t("login.email")} + setEmail(value)} /> - {t("login.password")} + {t("login.password")} setPassword(value)} /> - {t("login.confirm")} + {t("login.confirm")} setConfirm(value)} /> {password !== confirm && ( - theme.colors.red })}> + {t("login.password-no-match")} )} - {error && theme.colors.red })}>{error}} + {error && {error}} { if (error) return; router.replace("/"); }} - {...css({ - m: ts(1), - width: px(250), - maxWidth: percent(100), - alignSelf: "center", - mY: ts(3), - })} + className="m-2 my-6 w-60 self-center" /> - {/* > */} - {/* )} */} Have an account already? diff --git a/front/src/ui/login/server-url.tsx b/front/src/ui/login/server-url.tsx index e9a5a247..52881f8c 100644 --- a/front/src/ui/login/server-url.tsx +++ b/front/src/ui/login/server-url.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; -import { type Theme, useYoshiki } from "yoshiki/native"; -import { Button, H1, Input, Link, P, ts } from "~/primitives"; +import { Button, H1, Input, Link, P } from "~/primitives"; import { type QueryIdentifier, useFetch } from "~/query"; export const cleanApiUrl = (apiUrl: string) => { @@ -16,73 +15,44 @@ export const ServerUrlPage = () => { const apiUrl = cleanApiUrl(_apiUrl); const { data, error } = useFetch({ ...ServerUrlPage.query, - options: { apiUrl, authToken: null }, + options: { apiUrl, authToken: null, returnError: true }, }); const { t } = useTranslation(); - const { css } = useYoshiki(); return ( - + {t("login.server")} - + {!data && ( - theme.colors.red, - alignSelf: "center", - })} - > - {error?.message ?? t("misc.loading")} + + {error + ? error.message === "offline" + ? t("errors.invalid-server") + : error.message + : t("misc.loading")} )} - - {/* */} - {/* {data && */} - {/* Object.values(data.oidc).map((x) => ( */} - {/* */} - {/* {x.logoUrl != null ? ( */} - {/* */} - {/* ) : ( */} - {/* t("login.via", { provider: x.displayName }) */} - {/* )} */} - {/* */} - {/* ))} */} - {/* */} - {/* */} - + +
{t("login.username")}
{t("login.password")}
theme.colors.red })}>{error}
{error}
Don’t have an account? diff --git a/front/src/ui/login/password-input.tsx b/front/src/ui/login/password-input.tsx index d395c008..2ca17927 100644 --- a/front/src/ui/login/password-input.tsx +++ b/front/src/ui/login/password-input.tsx @@ -1,11 +1,9 @@ import VisibilityOff from "@material-symbols/svg-400/rounded/visibility_off-fill.svg"; import Visibility from "@material-symbols/svg-400/rounded/visibility-fill.svg"; import { type ComponentProps, useState } from "react"; -import { px, useYoshiki } from "yoshiki/native"; import { IconButton, Input } from "~/primitives"; export const PasswordInput = (props: ComponentProps) => { - const { css } = useYoshiki(); const [show, setVisibility] = useState(false); return ( @@ -14,9 +12,7 @@ export const PasswordInput = (props: ComponentProps) => { right={ setVisibility(!show)} - {...css({ width: px(19), height: px(19), m: 0, p: 0 })} /> } {...props} diff --git a/front/src/ui/login/register.tsx b/front/src/ui/login/register.tsx index 6aa9346c..b09f2654 100644 --- a/front/src/ui/login/register.tsx +++ b/front/src/ui/login/register.tsx @@ -2,8 +2,7 @@ import { useRouter } from "expo-router"; import { useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Platform } from "react-native"; -import { percent, px, useYoshiki } from "yoshiki/native"; -import { A, Button, H1, Input, P, ts } from "~/primitives"; +import { A, Button, H1, Input, P } from "~/primitives"; import { defaultApiUrl } from "~/providers/account-provider"; import { useQueryState } from "~/utils"; import { FormPage } from "./form"; @@ -21,50 +20,39 @@ export const RegisterPage = () => { const router = useRouter(); const { t } = useTranslation(); - const { css } = useYoshiki(); if (Platform.OS !== "web" && !apiUrl) return ; return ( - {t("login.register")} - {/* */} - {/* {data?.registrationEnabled && ( */} - {/* <> */} - {t("login.username")} + {t("login.register")} + {t("login.username")} setUsername(value)} /> - {t("login.email")} - setEmail(value)} - /> + {t("login.email")} + setEmail(value)} /> - {t("login.password")} + {t("login.password")} setPassword(value)} /> - {t("login.confirm")} + {t("login.confirm")} setConfirm(value)} /> {password !== confirm && ( - theme.colors.red })}> + {t("login.password-no-match")} )} - {error && theme.colors.red })}>{error}} + {error && {error}} { if (error) return; router.replace("/"); }} - {...css({ - m: ts(1), - width: px(250), - maxWidth: percent(100), - alignSelf: "center", - mY: ts(3), - })} + className="m-2 my-6 w-60 self-center" /> - {/* > */} - {/* )} */} Have an account already? diff --git a/front/src/ui/login/server-url.tsx b/front/src/ui/login/server-url.tsx index e9a5a247..52881f8c 100644 --- a/front/src/ui/login/server-url.tsx +++ b/front/src/ui/login/server-url.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; -import { type Theme, useYoshiki } from "yoshiki/native"; -import { Button, H1, Input, Link, P, ts } from "~/primitives"; +import { Button, H1, Input, Link, P } from "~/primitives"; import { type QueryIdentifier, useFetch } from "~/query"; export const cleanApiUrl = (apiUrl: string) => { @@ -16,73 +15,44 @@ export const ServerUrlPage = () => { const apiUrl = cleanApiUrl(_apiUrl); const { data, error } = useFetch({ ...ServerUrlPage.query, - options: { apiUrl, authToken: null }, + options: { apiUrl, authToken: null, returnError: true }, }); const { t } = useTranslation(); - const { css } = useYoshiki(); return ( - + {t("login.server")} - + {!data && ( - theme.colors.red, - alignSelf: "center", - })} - > - {error?.message ?? t("misc.loading")} + + {error + ? error.message === "offline" + ? t("errors.invalid-server") + : error.message + : t("misc.loading")} )} - - {/* */} - {/* {data && */} - {/* Object.values(data.oidc).map((x) => ( */} - {/* */} - {/* {x.logoUrl != null ? ( */} - {/* */} - {/* ) : ( */} - {/* t("login.via", { provider: x.displayName }) */} - {/* )} */} - {/* */} - {/* ))} */} - {/* */} - {/* */} - + +
{t("login.email")}
{t("login.confirm")}
theme.colors.red })}> +
{t("login.password-no-match")}
Have an account already? diff --git a/front/src/ui/login/server-url.tsx b/front/src/ui/login/server-url.tsx index e9a5a247..52881f8c 100644 --- a/front/src/ui/login/server-url.tsx +++ b/front/src/ui/login/server-url.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; -import { type Theme, useYoshiki } from "yoshiki/native"; -import { Button, H1, Input, Link, P, ts } from "~/primitives"; +import { Button, H1, Input, Link, P } from "~/primitives"; import { type QueryIdentifier, useFetch } from "~/query"; export const cleanApiUrl = (apiUrl: string) => { @@ -16,73 +15,44 @@ export const ServerUrlPage = () => { const apiUrl = cleanApiUrl(_apiUrl); const { data, error } = useFetch({ ...ServerUrlPage.query, - options: { apiUrl, authToken: null }, + options: { apiUrl, authToken: null, returnError: true }, }); const { t } = useTranslation(); - const { css } = useYoshiki(); return ( - + {t("login.server")} - + {!data && ( - theme.colors.red, - alignSelf: "center", - })} - > - {error?.message ?? t("misc.loading")} + + {error + ? error.message === "offline" + ? t("errors.invalid-server") + : error.message + : t("misc.loading")} )} - - {/* */} - {/* {data && */} - {/* Object.values(data.oidc).map((x) => ( */} - {/* */} - {/* {x.logoUrl != null ? ( */} - {/* */} - {/* ) : ( */} - {/* t("login.via", { provider: x.displayName }) */} - {/* )} */} - {/* */} - {/* ))} */} - {/* */} - {/* */} - + +
theme.colors.red, - alignSelf: "center", - })} - > - {error?.message ?? t("misc.loading")} +
+ {error + ? error.message === "offline" + ? t("errors.invalid-server") + : error.message + : t("misc.loading")}