From d38a46de22d6be1d3d65f85a4952c726f8c9fd3f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 6 Feb 2025 18:08:28 +0100 Subject: [PATCH] Rework account internals storage --- front/packages/models/src/account-internal.ts | 68 ------------------- front/src/providers/account-provider.tsx | 28 ++++---- front/src/providers/account-store.ts | 58 ++++++++++++++++ front/src/providers/index.tsx | 5 +- front/src/providers/settings.ts | 22 ++++-- 5 files changed, 92 insertions(+), 89 deletions(-) create mode 100644 front/src/providers/account-store.ts diff --git a/front/packages/models/src/account-internal.ts b/front/packages/models/src/account-internal.ts index 28c80260..609d690d 100644 --- a/front/packages/models/src/account-internal.ts +++ b/front/packages/models/src/account-internal.ts @@ -17,71 +17,3 @@ * You should have received a copy of the GNU General Public License * along with Kyoo. If not, see . */ - -import { Platform } from "react-native"; -import { type ZodTypeAny, z } from "zod"; -import { type Account, AccountP } from "./accounts"; - -const readAccounts = () => { - const acc = storage.getString("accounts"); - if (!acc) return []; - return z.array(AccountP).parse(JSON.parse(acc)); -}; - -const writeAccounts = (accounts: Account[]) => { - storage.set("accounts", JSON.stringify(accounts)); - if (Platform.OS === "web") { - const selected = accounts.find((x) => x.selected); - if (!selected) return; - setCookie("account", selected); - // cookie used for images and videos since we can't add Authorization headers in img or video tags. - setCookie("X-Bearer", selected?.token.access_token); - } -}; - -export const getCurrentAccount = () => { - const accounts = readAccounts(); - return accounts.find((x) => x.selected); -}; - -export const addAccount = (account: Account) => { - const accounts = readAccounts(); - - // Prevent the user from adding the same account twice. - if (accounts.find((x) => x.id === account.id)) { - updateAccount(account.id, account); - return; - } - - for (const acc of accounts) acc.selected = false; - accounts.push(account); - writeAccounts(accounts); -}; - -export const removeAccounts = (filter: (acc: Account) => boolean) => { - let accounts = readAccounts(); - accounts = accounts.filter((x) => !filter(x)); - if (!accounts.find((x) => x.selected) && accounts.length > 0) { - accounts[0].selected = true; - } - writeAccounts(accounts); -}; - -export const updateAccount = (id: string, account: Account) => { - const accounts = readAccounts(); - const idx = accounts.findIndex((x) => x.id === id); - if (idx === -1) return; - - const selected = account.selected; - if (selected) { - for (const acc of accounts) acc.selected = false; - // if account was already on the accounts list, we keep it selected. - account.selected = selected; - } else if (accounts[idx].selected) { - // we just unselected the current account, focus another one. - if (accounts.length > 0) accounts[0].selected = true; - } - - accounts[idx] = account; - writeAccounts(accounts); -}; diff --git a/front/src/providers/account-provider.tsx b/front/src/providers/account-provider.tsx index 7cb51142..44f033d4 100644 --- a/front/src/providers/account-provider.tsx +++ b/front/src/providers/account-provider.tsx @@ -1,8 +1,11 @@ import { type ReactNode, createContext, useEffect, useMemo } from "react"; import { Platform } from "react-native"; -import { type Account, type Token, type User, UserP } from "~/models"; +import { z } from "zod"; +import { type Account, AccountP, type Token, type User, UserP } from "~/models"; import { useFetch } from "~/query"; +import { removeAccounts, updateAccount } from "./account-store"; import { useSetError } from "./error-provider"; +import { useStoreValue } from "./settings"; const AccountContext = createContext<{ apiUrl: string; @@ -31,22 +34,19 @@ export const AccountProvider = ({ } const setError = useSetError(); - - const [accStr] = useMMKVString("accounts"); - const accounts = accStr ? z.array(AccountP).parse(JSON.parse(accStr)) : null; + const accounts = useStoreValue("accounts", z.array(AccountP)) ?? []; const ret = useMemo(() => { const acc = accounts.find((x) => x.selected); return { - apiUrl: acc.apiUrl, - authToken: acc.token, + apiUrl: Platform.OS === "web" ? "/api" : acc?.apiUrl, + authToken: acc?.token, selectedAccount: acc, - accounts: - accounts?.map((account) => ({ - ...account, - select: () => updateAccount(account.id, { ...account, selected: true }), - remove: () => removeAccounts((x) => x.id === account.id), - })) ?? [], + accounts: accounts.map((account) => ({ + ...account, + select: () => updateAccount(account.id, { ...account, selected: true }), + remove: () => removeAccounts((x) => x.id === account.id), + })), }; }, [accounts]); @@ -62,10 +62,10 @@ export const AccountProvider = ({ path: ["auth", "me"], parser: UserP, placeholderData: ret.selectedAccount, - enabled: ret.selectedAccount, + enabled: !!ret.selectedAccount, options: { apiUrl: ret.apiUrl, - authToken: ret.authToken, + authToken: ret.authToken?.access_token, }, }); // Use a ref here because we don't want the effect to trigger when the selected diff --git a/front/src/providers/account-store.ts b/front/src/providers/account-store.ts new file mode 100644 index 00000000..32a02224 --- /dev/null +++ b/front/src/providers/account-store.ts @@ -0,0 +1,58 @@ +import { Platform } from "react-native"; +import { z } from "zod"; +import { type Account, AccountP } from "~/models"; +import { readValue, setCookie, storeValue } from "./settings"; + +const writeAccounts = (accounts: Account[]) => { + storeValue("accounts", accounts); + if (Platform.OS === "web") { + const selected = accounts.find((x) => x.selected); + if (!selected) return; + setCookie("account", selected); + // cookie used for images and videos since we can't add Authorization headers in img or video tags. + setCookie("X-Bearer", selected?.token.access_token); + } +}; + +export const addAccount = (account: Account) => { + const accounts = readValue("accounts", z.array(AccountP)) ?? []; + + // Prevent the user from adding the same account twice. + if (accounts.find((x) => x.id === account.id)) { + updateAccount(account.id, account); + return; + } + + for (const acc of accounts) acc.selected = false; + account.selected = true; + accounts.push(account); + writeAccounts(accounts); +}; + +export const removeAccounts = (filter: (acc: Account) => boolean) => { + let accounts = readValue("accounts", z.array(AccountP)) ?? []; + accounts = accounts.filter((x) => !filter(x)); + if (!accounts.find((x) => x.selected) && accounts.length > 0) { + accounts[0].selected = true; + } + writeAccounts(accounts); +}; + +export const updateAccount = (id: string, account: Account) => { + const accounts = readValue("accounts", z.array(AccountP)) ?? []; + const idx = accounts.findIndex((x) => x.id === id); + if (idx === -1) return; + + const selected = account.selected; + if (selected) { + for (const acc of accounts) acc.selected = false; + // if account was already on the accounts list, we keep it selected. + account.selected = selected; + } else if (accounts[idx].selected) { + // we just unselected the current account, focus another one. + if (accounts.length > 0) accounts[0].selected = true; + } + + accounts[idx] = account; + writeAccounts(accounts); +}; diff --git a/front/src/providers/index.tsx b/front/src/providers/index.tsx index ff22dc68..0263fc63 100644 --- a/front/src/providers/index.tsx +++ b/front/src/providers/index.tsx @@ -4,6 +4,7 @@ import { type ReactNode, useState } from "react"; import { ThemeSelector } from "~/primitives/theme"; import { createQueryClient } from "~/query"; import { ErrorConsumer, ErrorProvider } from "./error-provider"; +import { AccountProvider } from "./account-provider"; const QueryProvider = ({ children }: { children: ReactNode }) => { const [queryClient] = useState(() => createQueryClient()); @@ -26,7 +27,9 @@ export const Providers = ({ children }: { children: ReactNode }) => { - {children} + + {children} + diff --git a/front/src/providers/settings.ts b/front/src/providers/settings.ts index 183929de..bf93b4fe 100644 --- a/front/src/providers/settings.ts +++ b/front/src/providers/settings.ts @@ -1,7 +1,7 @@ import { MMKV, useMMKVString } from "react-native-mmkv"; -import type { ZodTypeAny } from "zod"; +import type { ZodTypeAny, z } from "zod"; -export const storage = new MMKV(); +const storage = new MMKV(); function toBase64(utf8: string) { if (typeof window !== "undefined") return window.btoa(utf8); @@ -35,11 +35,21 @@ export const readCookie = ( const ret = ca.find((x) => x.trimStart().startsWith(name)); if (ret === undefined) return undefined; const str = fromBase64(ret.substring(name.length)); - return parser ? parser.parse(JSON.parse(str)) : str; + return parser ? (parser.parse(JSON.parse(str)) as z.infer) : str; }; -export const useStoreValue = (key: string, parser?: T) => { +export const useStoreValue = (key: string, parser: T) => { const [val] = useMMKVString(key); - if (!val) return val; - return parser ? parser.parse(JSON.parse(val)) : val; + if (val === undefined) return val; + return parser.parse(JSON.parse(val)) as z.infer; +}; + +export const storeValue = (key: string, value: unknown) => { + storage.set(key, JSON.stringify(value)); +}; + +export const readValue = (key: string, parser: T) => { + const val = storage.getString(key); + if (val === undefined) return val; + return parser.parse(JSON.parse(val)) as z.infer; };