import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { LoadingScreen } from "@/components/LoadingScreen"; import { authStoragePrefix, buildAccountId, loadAccounts, loadActiveAccountId, loadDraftInstanceUrl, saveAccounts, saveActiveAccountId, saveDraftInstanceUrl, type SavedAccount, } from "@/lib/accounts"; import { setRuntimeApiUrl, getApiUrl, DEFAULT_API_URL } from "@/lib/config"; import { normalizeInstanceUrl, saveStoredInstanceUrl } from "@/lib/instance-url"; type AccountsContextValue = { accounts: SavedAccount[]; activeAccount: SavedAccount | null; activeAccountId: string | null; apiUrl: string; authStoragePrefix: string; setInstanceUrl: (url: string) => Promise; switchAccount: (accountId: string) => Promise; registerAccount: (input: { instanceUrl: string; userId: string; email: string; name: string; }) => Promise; removeAccount: (accountId: string) => Promise; clearActiveAccount: () => Promise; }; const AccountsContext = createContext(null); export function AccountsProvider({ children }: { children: ReactNode }) { const [ready, setReady] = useState(false); const [accounts, setAccounts] = useState([]); const [activeAccountId, setActiveAccountId] = useState(null); const [apiUrl, setApiUrl] = useState(getApiUrl); useEffect(() => { Promise.all([loadAccounts(), loadActiveAccountId(), loadDraftInstanceUrl()]) .then(([storedAccounts, activeId, draftUrl]) => { setAccounts(storedAccounts); const active = storedAccounts.find((account) => account.id === activeId) ?? null; if (active) { setActiveAccountId(active.id); setRuntimeApiUrl(active.instanceUrl); } else if (draftUrl) { setRuntimeApiUrl(draftUrl); } else { setRuntimeApiUrl(DEFAULT_API_URL); } setApiUrl(getApiUrl()); }) .finally(() => setReady(true)); }, []); const activeAccount = useMemo( () => accounts.find((account) => account.id === activeAccountId) ?? null, [accounts, activeAccountId], ); const setInstanceUrl = useCallback( async (url: string) => { const normalized = normalizeInstanceUrl(url); if (!normalized) { throw new Error("Enter a valid server URL (e.g. beenvoice.app or localhost:3000)"); } if (activeAccount) { const nextAccounts = accounts.map((account) => account.id === activeAccount.id ? { ...account, instanceUrl: normalized } : account, ); setAccounts(nextAccounts); await saveAccounts(nextAccounts); } else { await saveDraftInstanceUrl(normalized); } await saveStoredInstanceUrl(normalized); setRuntimeApiUrl(normalized); setApiUrl(normalized); return normalized; }, [activeAccount, accounts], ); const switchAccount = useCallback( async (accountId: string) => { const account = accounts.find((entry) => entry.id === accountId); if (!account) return; const nextAccounts = accounts.map((entry) => entry.id === accountId ? { ...entry, lastUsedAt: Date.now() } : entry, ); setAccounts(nextAccounts); await saveAccounts(nextAccounts); await saveActiveAccountId(accountId); setActiveAccountId(accountId); setRuntimeApiUrl(account.instanceUrl); setApiUrl(account.instanceUrl); }, [accounts], ); const registerAccount = useCallback( async (input: { instanceUrl: string; userId: string; email: string; name: string }) => { const id = buildAccountId(input.instanceUrl, input.userId); const existing = accounts.find((account) => account.id === id); const account: SavedAccount = { id, instanceUrl: input.instanceUrl, userId: input.userId, email: input.email, name: input.name, lastUsedAt: Date.now(), }; const nextAccounts = existing ? accounts.map((entry) => (entry.id === id ? account : entry)) : [account, ...accounts.filter((entry) => entry.id !== id)]; setAccounts(nextAccounts); await saveAccounts(nextAccounts); await saveActiveAccountId(id); await saveDraftInstanceUrl(null); setActiveAccountId(id); setRuntimeApiUrl(input.instanceUrl); setApiUrl(input.instanceUrl); await saveStoredInstanceUrl(input.instanceUrl); return account; }, [accounts], ); const removeAccount = useCallback( async (accountId: string) => { const nextAccounts = accounts.filter((account) => account.id !== accountId); setAccounts(nextAccounts); await saveAccounts(nextAccounts); if (activeAccountId === accountId) { const fallback = nextAccounts[0] ?? null; await saveActiveAccountId(fallback?.id ?? null); setActiveAccountId(fallback?.id ?? null); if (fallback) { setRuntimeApiUrl(fallback.instanceUrl); setApiUrl(fallback.instanceUrl); } } }, [accounts, activeAccountId], ); const clearActiveAccount = useCallback(async () => { await saveActiveAccountId(null); setActiveAccountId(null); }, []); const value = useMemo( () => ({ accounts, activeAccount, activeAccountId, apiUrl, authStoragePrefix: activeAccount ? authStoragePrefix(activeAccount.id) : "beenvoice:guest", setInstanceUrl, switchAccount, registerAccount, removeAccount, clearActiveAccount, }), [ accounts, activeAccount, activeAccountId, apiUrl, setInstanceUrl, switchAccount, registerAccount, removeAccount, clearActiveAccount, ], ); if (!ready) { return ; } return {children}; } export function useAccounts() { const ctx = useContext(AccountsContext); if (!ctx) throw new Error("useAccounts must be used within AccountsProvider"); return ctx; }