feat: refine expo admin mobile flows

This commit is contained in:
xuhongbin
2026-03-08 20:53:15 +08:00
parent c70ca1641a
commit 434bbf258a
21 changed files with 3128 additions and 6381 deletions

View File

@@ -1,4 +1,5 @@
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
const { proxy } = require('valtio');
const BASE_URL_KEY = 'sub2api_base_url';
@@ -67,39 +68,51 @@ export function getDefaultAdminConfig() {
}
async function getItem(key: string) {
if (typeof window !== 'undefined') {
if (typeof localStorage === 'undefined') {
return null;
try {
if (Platform.OS === 'web') {
if (typeof localStorage === 'undefined') {
return null;
}
return localStorage.getItem(key);
}
return localStorage.getItem(key);
return await SecureStore.getItemAsync(key);
} catch {
return null;
}
return SecureStore.getItemAsync(key);
}
async function setItem(key: string, value: string) {
if (typeof window !== 'undefined') {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(key, value);
try {
if (Platform.OS === 'web') {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(key, value);
}
return;
}
await SecureStore.setItemAsync(key, value);
} catch {
return;
}
await SecureStore.setItemAsync(key, value);
}
async function deleteItem(key: string) {
if (typeof window !== 'undefined') {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(key);
try {
if (Platform.OS === 'web') {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(key);
}
return;
}
await SecureStore.deleteItemAsync(key);
} catch {
return;
}
await SecureStore.deleteItemAsync(key);
}
export const adminConfigState = proxy({
@@ -112,98 +125,104 @@ export const adminConfigState = proxy({
export async function hydrateAdminConfig() {
const defaults = getDefaultAdminConfig();
const [baseUrl, adminApiKey, rawAccounts, activeAccountId] = await Promise.all([
getItem(BASE_URL_KEY),
getItem(ADMIN_KEY_KEY),
getItem(ACCOUNTS_KEY),
getItem(ACTIVE_ACCOUNT_ID_KEY),
]);
let accounts: AdminAccountProfile[] = [];
try {
const [baseUrl, adminApiKey, rawAccounts, activeAccountId] = await Promise.all([
getItem(BASE_URL_KEY),
getItem(ADMIN_KEY_KEY),
getItem(ACCOUNTS_KEY),
getItem(ACTIVE_ACCOUNT_ID_KEY),
]);
if (rawAccounts) {
try {
const parsed = JSON.parse(rawAccounts) as AdminAccountProfile[];
accounts = Array.isArray(parsed) ? parsed.map((account) => normalizeAccount(account)) : [];
} catch {
accounts = [];
let accounts: AdminAccountProfile[] = [];
if (rawAccounts) {
try {
const parsed = JSON.parse(rawAccounts) as AdminAccountProfile[];
accounts = Array.isArray(parsed) ? parsed.map((account) => normalizeAccount(account)) : [];
} catch {
accounts = [];
}
}
if (accounts.length === 0 && baseUrl) {
const legacyConfig = normalizeConfig({
baseUrl,
adminApiKey: adminApiKey ?? defaults.adminApiKey,
});
accounts = [
{
id: createAccountId(),
label: getAccountLabel(legacyConfig.baseUrl),
...legacyConfig,
updatedAt: new Date().toISOString(),
enabled: true,
},
];
}
const sortedAccounts = sortAccounts(accounts);
const activeAccount = getNextActiveAccount(sortedAccounts, activeAccountId ?? undefined);
const nextActiveAccountId = activeAccount?.id || '';
adminConfigState.accounts = sortedAccounts;
adminConfigState.activeAccountId = nextActiveAccountId;
adminConfigState.baseUrl = activeAccount?.baseUrl ?? defaults.baseUrl;
adminConfigState.adminApiKey = activeAccount?.adminApiKey ?? defaults.adminApiKey;
await Promise.all([
setItem(ACCOUNTS_KEY, JSON.stringify(sortedAccounts)),
nextActiveAccountId ? setItem(ACTIVE_ACCOUNT_ID_KEY, nextActiveAccountId) : deleteItem(ACTIVE_ACCOUNT_ID_KEY),
setItem(BASE_URL_KEY, activeAccount?.baseUrl ?? defaults.baseUrl),
setItem(ADMIN_KEY_KEY, activeAccount?.adminApiKey ?? defaults.adminApiKey),
]);
} finally {
adminConfigState.hydrated = true;
}
if (accounts.length === 0 && baseUrl) {
const legacyConfig = normalizeConfig({
baseUrl,
adminApiKey: adminApiKey ?? defaults.adminApiKey,
});
accounts = [
{
id: createAccountId(),
label: getAccountLabel(legacyConfig.baseUrl),
...legacyConfig,
updatedAt: new Date().toISOString(),
enabled: true,
},
];
}
const sortedAccounts = sortAccounts(accounts);
const activeAccount = getNextActiveAccount(sortedAccounts, activeAccountId ?? undefined);
const nextActiveAccountId = activeAccount?.id || '';
adminConfigState.accounts = sortedAccounts;
adminConfigState.activeAccountId = nextActiveAccountId;
adminConfigState.baseUrl = activeAccount?.baseUrl ?? defaults.baseUrl;
adminConfigState.adminApiKey = activeAccount?.adminApiKey ?? defaults.adminApiKey;
adminConfigState.hydrated = true;
await Promise.all([
setItem(ACCOUNTS_KEY, JSON.stringify(sortedAccounts)),
nextActiveAccountId ? setItem(ACTIVE_ACCOUNT_ID_KEY, nextActiveAccountId) : deleteItem(ACTIVE_ACCOUNT_ID_KEY),
setItem(BASE_URL_KEY, activeAccount?.baseUrl ?? defaults.baseUrl),
setItem(ADMIN_KEY_KEY, activeAccount?.adminApiKey ?? defaults.adminApiKey),
]);
}
export async function saveAdminConfig(input: { baseUrl: string; adminApiKey: string }) {
adminConfigState.saving = true;
const normalized = normalizeConfig(input);
const nextUpdatedAt = new Date().toISOString();
const existingAccount = adminConfigState.accounts.find(
(account: AdminAccountProfile) => account.baseUrl === normalized.baseUrl && account.adminApiKey === normalized.adminApiKey
);
const nextAccount: AdminAccountProfile = existingAccount
? {
...existingAccount,
label: getAccountLabel(normalized.baseUrl),
updatedAt: nextUpdatedAt,
}
: {
id: createAccountId(),
label: getAccountLabel(normalized.baseUrl),
...normalized,
updatedAt: nextUpdatedAt,
enabled: true,
};
const nextAccounts = sortAccounts([
nextAccount,
...adminConfigState.accounts.filter((account: AdminAccountProfile) => account.id !== nextAccount.id),
]);
try {
const normalized = normalizeConfig(input);
const nextUpdatedAt = new Date().toISOString();
const existingAccount = adminConfigState.accounts.find(
(account: AdminAccountProfile) => account.baseUrl === normalized.baseUrl && account.adminApiKey === normalized.adminApiKey
);
const nextAccount: AdminAccountProfile = existingAccount
? {
...existingAccount,
label: getAccountLabel(normalized.baseUrl),
updatedAt: nextUpdatedAt,
}
: {
id: createAccountId(),
label: getAccountLabel(normalized.baseUrl),
...normalized,
updatedAt: nextUpdatedAt,
enabled: true,
};
const nextAccounts = sortAccounts([
nextAccount,
...adminConfigState.accounts.filter((account: AdminAccountProfile) => account.id !== nextAccount.id),
]);
await Promise.all([
setItem(BASE_URL_KEY, normalized.baseUrl),
setItem(ADMIN_KEY_KEY, normalized.adminApiKey),
setItem(ACCOUNTS_KEY, JSON.stringify(nextAccounts)),
setItem(ACTIVE_ACCOUNT_ID_KEY, nextAccount.id),
]);
await Promise.all([
setItem(BASE_URL_KEY, normalized.baseUrl),
setItem(ADMIN_KEY_KEY, normalized.adminApiKey),
setItem(ACCOUNTS_KEY, JSON.stringify(nextAccounts)),
setItem(ACTIVE_ACCOUNT_ID_KEY, nextAccount.id),
]);
adminConfigState.accounts = nextAccounts;
adminConfigState.activeAccountId = nextAccount.id;
adminConfigState.baseUrl = normalized.baseUrl;
adminConfigState.adminApiKey = normalized.adminApiKey;
adminConfigState.saving = false;
adminConfigState.accounts = nextAccounts;
adminConfigState.activeAccountId = nextAccount.id;
adminConfigState.baseUrl = normalized.baseUrl;
adminConfigState.adminApiKey = normalized.adminApiKey;
} finally {
adminConfigState.saving = false;
}
}
export async function switchAdminAccount(accountId: string) {