diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx
index b4f27db8..19ada4e9 100644
--- a/front/apps/mobile/app/_layout.tsx
+++ b/front/apps/mobile/app/_layout.tsx
@@ -21,7 +21,7 @@
import { PortalProvider } from "@gorhom/portal";
import { ThemeSelector } from "@kyoo/primitives";
import { NavbarRight, NavbarTitle } from "@kyoo/ui";
-import { AccountContext, createQueryClient, useAccounts } from "@kyoo/models";
+import { AccountProvider, createQueryClient, useAccounts } from "@kyoo/models";
import { QueryClientProvider } from "@tanstack/react-query";
import i18next from "i18next";
import { Stack } from "expo-router";
@@ -121,8 +121,8 @@ export default function Root() {
if (!isReady) return null;
rendered = true;
return (
-
-
+
+
}
-
-
+
+
);
}
diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx
index 774d75a9..f41c10dc 100755
--- a/front/apps/web/src/pages/_app.tsx
+++ b/front/apps/web/src/pages/_app.tsx
@@ -24,13 +24,13 @@ import { HydrationBoundary, QueryClientProvider } from "@tanstack/react-query";
import { HiddenIfNoJs, SkeletonCss, ThemeSelector } from "@kyoo/primitives";
import { WebTooltip } from "@kyoo/primitives/src/tooltip.web";
import {
+ AccountProvider,
createQueryClient,
fetchQuery,
getTokenWJ,
QueryIdentifier,
QueryPage,
} from "@kyoo/models";
-import { setSecureItem } from "@kyoo/models/src/secure-store.web";
import { useState } from "react";
import NextApp, { AppContext, type AppProps } from "next/app";
import { Poppins } from "next/font/google";
@@ -40,6 +40,11 @@ import Head from "next/head";
import { withTranslations } from "../i18n";
import arrayShuffle from "array-shuffle";
import { Tooltip } from "react-tooltip";
+import {
+ getCurrentAccount,
+ readAccountCookie,
+ updateAccount,
+} from "@kyoo/models/src/account-internal";
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
@@ -114,7 +119,10 @@ const App = ({ Component, pageProps }: AppProps) => {
useMobileHover();
// Set the auth from the server (if the token was refreshed during SSR).
- if (typeof window !== "undefined" && token) setSecureItem("auth", JSON.stringify(token));
+ if (typeof window !== "undefined" && token) {
+ const account = getCurrentAccount();
+ if (account) updateAccount(account.id, { ...account, token });
+ }
return (
@@ -124,25 +132,27 @@ const App = ({ Component, pageProps }: AppProps) => {
-
-
-
-
- }
- randomItems={[]}
- {...layoutProps}
- />
-
-
-
+
+
+
+
+
+ }
+ randomItems={[]}
+ {...layoutProps}
+ />
+
+
+
+
>
@@ -150,6 +160,7 @@ const App = ({ Component, pageProps }: AppProps) => {
};
App.getInitialProps = async (ctx: AppContext) => {
+ const account = readAccountCookie(ctx.ctx.req?.headers.cookie);
const appProps = await NextApp.getInitialProps(ctx);
const Component = ctx.Component as QueryPage;
@@ -167,7 +178,7 @@ App.getInitialProps = async (ctx: AppContext) => {
...(getUrl ? getUrl(ctx.router.query as any, items) : []),
...(getLayoutUrl ? getLayoutUrl(ctx.router.query as any, items) : []),
];
- const [authToken, token] = await getTokenWJ(ctx.ctx.req?.headers.cookie);
+ const [authToken, token] = await getTokenWJ(account ?? null);
appProps.pageProps.queryState = await fetchQuery(urls, authToken);
appProps.pageProps.token = token;
diff --git a/front/packages/models/src/accounts.tsx b/front/packages/models/src/accounts.tsx
index 072a8d64..430bd4a3 100644
--- a/front/packages/models/src/accounts.tsx
+++ b/front/packages/models/src/accounts.tsx
@@ -18,74 +18,84 @@
* along with Kyoo. If not, see .
*/
-import { getSecureItem, setSecureItem, storage } from "./secure-store";
-import { setApiUrl } from "./query";
-import { createContext, useEffect, useState } from "react";
-import { useMMKVListener } from "react-native-mmkv";
-import { Account, loginFunc } from "./login";
+import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
+import { User, UserP } from "./resources";
+import { z } from "zod";
+import { zdate } from "./utils";
+import { removeAccounts, setAccountCookie, updateAccount } from "./account-internal";
+import { useMMKVString } from "react-native-mmkv";
+import { Platform } from "react-native";
+import { queryFn, useFetch } from "./query";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
-export const AccountContext = createContext>({ type: "loading" });
+export const TokenP = z.object({
+ token_type: z.literal("Bearer"),
+ access_token: z.string(),
+ refresh_token: z.string(),
+ expire_in: z.string(),
+ expire_at: zdate(),
+});
+export type Token = z.infer;
+
+export const AccountP = UserP.and(
+ z.object({
+ token: TokenP,
+ apiUrl: z.string(),
+ selected: z.boolean(),
+ }),
+);
+export type Account = z.infer;
+
+const AccountContext = createContext<(Account & { select: () => void; remove: () => void })[]>([]);
+
+export const AccountProvider = ({ children }: { children: ReactNode }) => {
+ const [accStr] = useMMKVString("accounts");
+ const acc = z.array(AccountP).parse(accStr);
+ const accounts = useMemo(
+ () =>
+ acc.map((account) => ({
+ ...account,
+ select: () => updateAccount(account.id, { ...account, selected: true }),
+ remove: () => removeAccounts((x) => x.id == x.id),
+ })),
+ [acc],
+ );
+
+ // update user's data from kyoo un startup, it could have changed.
+ const selected = useMemo(() => accounts.find((x) => x.selected), [accounts]);
+ const user = useFetch({
+ path: ["auth", "me"],
+ parser: UserP,
+ placeholderData: selected,
+ enabled: !!selected,
+ timeout: 5_000,
+ });
+ useEffect(() => {
+ if (!selected || !user.isSuccess || user.isPlaceholderData) return;
+ const nUser = { ...selected, ...user.data };
+ if (!Object.is(selected, nUser)) updateAccount(nUser.id, nUser);
+ }, [selected, user]);
+
+ const queryClient = useQueryClient();
+ const oldSelectedId = useRef(selected?.id);
+ useEffect(() => {
+ // if the user change account (or connect/disconnect), reset query cache.
+ if (selected?.id !== oldSelectedId.current)
+ queryClient.invalidateQueries();
+ oldSelectedId.current = selected?.id;
+
+ // update cookies for ssr (needs to contains token, theme, language...)
+ if (Platform.OS === "web") setAccountCookie(selected);
+ }, [selected, queryClient]);
+
+ return {children};
+};
+
+export const useAccount = () => {
+ const acc = useContext(AccountContext);
+ return acc.find((x) => x.selected);
+};
export const useAccounts = () => {
- const [accounts, setAccounts] = useState(
- JSON.parse(getSecureItem("accounts") ?? "[]"),
- );
- const [verified, setVerified] = useState<{
- status: "ok" | "error" | "loading" | "unverified";
- error?: string;
- }>({ status: "loading" });
- const [retryCount, setRetryCount] = useState(0);
-
- const sel = getSecureItem("selected");
- let [selected, _setSelected] = useState(
- sel ? parseInt(sel) : accounts.length > 0 ? 0 : null,
- );
- if (selected === null && accounts.length > 0) selected = 0;
- if (accounts.length === 0) selected = null;
-
- const setSelected = (selected: number) => {
- _setSelected(selected);
- setSecureItem("selected", selected.toString());
- };
-
- useEffect(() => {
- async function check() {
- setVerified({ status: "loading" });
- const selAcc = accounts![selected!];
- setApiUrl(selAcc.apiUrl);
- const verif = await loginFunc("refresh", selAcc.refresh_token, undefined, 5_000);
- setVerified(verif.ok ? { status: "ok" } : { status: "error", error: verif.error });
- }
-
- if (accounts.length && selected !== null) check();
- else setVerified({ status: "unverified" });
- // Use the length of the array and not the array directly because we don't care if the refresh token changes.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [accounts.length, selected, retryCount]);
-
- useMMKVListener((key) => {
- if (key === "accounts") setAccounts(JSON.parse(getSecureItem("accounts") ?? "[]"));
- }, storage);
-
- if (verified.status === "loading") return { type: "loading" } as const;
- if (accounts.length && verified.status === "unverified") return { type: "loading" } as const;
- if (verified.status === "error") {
- return {
- type: "error",
- accounts,
- selected,
- error: verified.error,
- setSelected,
- retry: () => {
- setVerified({ status: "loading" });
- setRetryCount((x) => x + 1);
- },
- } as const;
- }
- return {
- type: "ok",
- accounts,
- selected,
- setSelected,
- } as const;
+ return useContext(AccountContext);
};
diff --git a/front/packages/models/src/accounts.web.ts b/front/packages/models/src/accounts.web.ts
deleted file mode 100644
index d40e258e..00000000
--- a/front/packages/models/src/accounts.web.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Kyoo - A portable and vast media library solution.
- * Copyright (c) Kyoo.
- *
- * See AUTHORS.md and LICENSE file in the project root for full license information.
- *
- * Kyoo is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * any later version.
- *
- * Kyoo is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Kyoo. If not, see .
- */
-
-import { createContext } from "react";
-
-export const useAccounts = () => {};
-
-export const AccountContext = createContext({});