mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05:00 
			
		
		
		
	Rework account internals storage
This commit is contained in:
		
							parent
							
								
									0a8553653c
								
							
						
					
					
						commit
						ea7cb8b4d1
					
				@ -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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										58
									
								
								front/src/providers/account-store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								front/src/providers/account-store.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
};
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user