Rework account internals storage

This commit is contained in:
Zoe Roux 2025-02-06 18:08:28 +01:00
parent 25b5f0036d
commit d38a46de22
No known key found for this signature in database
5 changed files with 92 additions and 89 deletions

View File

@ -17,71 +17,3 @@
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
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);
};

View File

@ -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

View File

@ -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);
};

View File

@ -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 }) => {
<QueryProvider>
<ThemeProvider>
<ErrorProvider>
<ErrorConsumer scope="root">{children}</ErrorConsumer>
<AccountProvider>
<ErrorConsumer scope="root">{children}</ErrorConsumer>
</AccountProvider>
</ErrorProvider>
</ThemeProvider>
</QueryProvider>

View File

@ -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 = <T extends ZodTypeAny>(
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<T>) : str;
};
export const useStoreValue = <T extends ZodTypeAny>(key: string, parser?: T) => {
export const useStoreValue = <T extends ZodTypeAny>(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<T>;
};
export const storeValue = (key: string, value: unknown) => {
storage.set(key, JSON.stringify(value));
};
export const readValue = <T extends ZodTypeAny>(key: string, parser: T) => {
const val = storage.getString(key);
if (val === undefined) return val;
return parser.parse(JSON.parse(val)) as z.infer<T>;
};