Finish multi accounts

This commit is contained in:
Zoe Roux
2023-07-15 17:55:27 +09:00
parent 1237a9157c
commit 69760dd5f8
8 changed files with 146 additions and 123 deletions
+77 -26
View File
@@ -19,12 +19,13 @@
*/
import { z } from "zod";
import { deleteSecureItem, getSecureItem, setSecureItem } from "./secure-store";
import { deleteSecureItem, getSecureItem, setSecureItem, storage } from "./secure-store";
import { zdate } from "./utils";
import { queryFn } from "./query";
import { queryFn, setApiUrl } from "./query";
import { KyooErrors } from "./kyoo-errors";
import { Platform } from "react-native";
import { useEffect, useState } from "react";
import { createContext, useEffect, useState } from "react";
import { useMMKVListener } from "react-native-mmkv";
const TokenP = z.object({
token_type: z.literal("Bearer"),
@@ -41,25 +42,78 @@ type Result<A, B> =
export type Account = Token & { apiUrl: string; username: string };
export const useAccounts = () => {
const [accounts] = useState<Account[]>(JSON.parse(getSecureItem("accounts") ?? "[]"));
const [selected, setSelected] = useState<number>(parseInt(getSecureItem("selected") ?? "0"));
export const AccountContext = createContext<ReturnType<typeof useAccounts>>({ type: "loading" });
export const useAccounts = () => {
const [accounts, setAccounts] = useState<Account[]>(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<number | null>(
sel ? parseInt(sel) : accounts.length > 0 ? 0 : null,
);
if (selected === null && accounts.length > 0) selected = 0;
if (accounts.length === 0) selected = null;
useEffect(() => {
async function check() {
setVerified({status: "loading"});
const selAcc = accounts![selected!];
setApiUrl(selAcc.apiUrl);
const verif = await loginFunc("refresh", selAcc.refresh_token);
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",
error: verified.error,
retry: () => {
setVerified({ status: "loading" });
setRetryCount((x) => x + 1);
},
} as const;
}
return {
type: "ok",
accounts,
selected,
setSelected: (selected: number) => {
setSelected(selected);
setSecureItem("selected", selected.toString());
},
};
} as const;
};
const addAccount = (token: Token, apiUrl: string, username: string | null) => {
const addAccount = (token: Token, apiUrl: string, username: string | null) => {
const accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
const accIdx = accounts.findIndex((x) => x.refresh_token === token.refresh_token);
if (accIdx === -1) accounts.push({ ...token, username: username!, apiUrl });
else accounts[accIdx] = { ...accounts[accIdx], ...token };
if (accounts.find((x) => x.username === username && x.apiUrl === apiUrl)) return;
accounts.push({ ...token, username: username!, apiUrl });
setSecureItem("accounts", JSON.stringify(accounts));
};
const setCurrentAccountToken = (token: Token) => {
const accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
const selected = parseInt(getSecureItem("selected") ?? "0");
if (selected >= accounts.length) return;
accounts[selected] = { ...accounts[selected], ...token };
setSecureItem("accounts", JSON.stringify(accounts));
};
@@ -80,9 +134,10 @@ export const loginFunc = async (
TokenP,
);
if (typeof window !== "undefined") await setSecureItem("auth", JSON.stringify(token));
if (Platform.OS !== "web" && apiUrl)
await addAccount(token, apiUrl, typeof body !== "string" ? body.username : null);
if (typeof window !== "undefined") setSecureItem("auth", JSON.stringify(token));
if (Platform.OS !== "web" && apiUrl && typeof body !== "string")
addAccount(token, apiUrl, body.username);
else if (Platform.OS !== "web" && action === "refresh") setCurrentAccountToken(token);
return { ok: true, value: token };
} catch (e) {
console.error(action, e);
@@ -91,8 +146,7 @@ export const loginFunc = async (
};
export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [null, null]> => {
// @ts-ignore Web only.
const tokenStr = await getSecureItem("auth", cookies);
const tokenStr = getSecureItem("auth", cookies);
if (!tokenStr) return [null, null];
let token = TokenP.parse(JSON.parse(tokenStr));
@@ -107,21 +161,18 @@ export const getTokenWJ = async (cookies?: string): Promise<[string, Token] | [n
export const getToken = async (cookies?: string): Promise<string | null> =>
(await getTokenWJ(cookies))[0];
export const logout = async () => {
export const logout = () => {
if (Platform.OS !== "web") {
const tokenStr = await getSecureItem("auth");
if (!tokenStr) return;
const token = TokenP.parse(JSON.parse(tokenStr));
let accounts: Account[] = JSON.parse((await getSecureItem("accounts")) ?? "[]");
accounts = accounts.filter((x) => x.refresh_token !== token.refresh_token);
await setSecureItem("accounts", JSON.stringify(accounts));
let accounts: Account[] = JSON.parse(getSecureItem("accounts") ?? "[]");
const selected = parseInt(getSecureItem("selected") ?? "0");
accounts.splice(selected, 1);
setSecureItem("accounts", JSON.stringify(accounts));
}
await deleteSecureItem("auth");
deleteSecureItem("auth");
};
export const deleteAccount = async () => {
await queryFn({ path: ["auth", "me"], method: "DELETE" });
await logout();
logout();
};
+5 -1
View File
@@ -43,6 +43,10 @@ const kyooUrl =
export let kyooApiUrl: string | null = kyooUrl || null;
export const setApiUrl = (apiUrl: string) => {
kyooApiUrl = apiUrl;
}
export const queryFn = async <Data,>(
context:
| QueryFunctionContext
@@ -57,7 +61,7 @@ export const queryFn = async <Data,>(
token?: string | null,
): Promise<Data> => {
// @ts-ignore
let url: string | null = context.apiUrl ?? (Platform.OS !== "web" ? await getSecureItem("apiUrl") : null) ?? kyooUrl;
let url: string | null = context.apiUrl ?? kyooApiUrl;
if (!url) console.error("Kyoo's url is not defined.");
kyooApiUrl = url;
+1 -1
View File
@@ -20,7 +20,7 @@
import { MMKV } from 'react-native-mmkv'
const storage = new MMKV();
export const storage = new MMKV();
export const setSecureItem = (key: string, value: string | null) => {
if (value === null)