Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions src/nonview/core/AccountContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ import {
toLoginRequest,
updateAccount as updateAccountStorage,
} from "./accountStorage";

// Owns the linked-account list, session list, active-account pointer, and
// derives the APIClient from whichever session is currently active. Depends
// on AuthContext for the actual credential exchange, never the reverse.
interface AccountContextValue {
accounts: LinkedAccount[];
sessions: AccountSession[];
Expand All @@ -39,13 +35,8 @@ interface AccountContextValue {
addAccount: (data: RegistrationData) => Promise<LinkedAccount>;
reauthenticate: (id: string, password: string) => Promise<void>;
updateAccount: (id: string, updates: Partial<LinkedAccount>) => LinkedAccount | null;
// Drops just this account's session (needs re-auth afterwards) without
// unlinking it — distinct from removeAccount (full unlink) and logoutAll.
signOut: (id: string) => Promise<void>;
// Minimal for now: just moves the active-account pointer. Cache
// hydration/background sync/hook points are issue #29's scope.
switchAccount: (id: string) => void;
// Real stubs — nothing calls these yet (no switcher/lifecycle UI exists).
removeAccount: (id: string) => Promise<void>;
logoutAll: () => Promise<void>;
}
Expand Down Expand Up @@ -84,9 +75,6 @@ export const AccountProvider: React.FC<AccountProviderProps> = ({ children }) =>
);
const isAuthenticated = activeSession !== null;

// Mirrors AuthContext.tsx's old tokenRef pattern: apiClient must stay a
// stable reference, so the mutable "current token" lives in a ref that's
// kept in sync with the derived activeSession.
const activeSessionRef = useRef<AccountSession | null>(activeSession);
useEffect(() => {
activeSessionRef.current = activeSession;
Expand All @@ -98,8 +86,6 @@ export const AccountProvider: React.FC<AccountProviderProps> = ({ children }) =>
baseURL: defaultBaseURL(),
getToken: () => activeSessionRef.current?.token ?? null,
onUnauthorized: () => {
// Invalidate only the active account's session — never touch
// other linked accounts.
const s = activeSessionRef.current;
if (!s) return;
removeSession(s.accountId);
Expand All @@ -109,21 +95,21 @@ export const AccountProvider: React.FC<AccountProviderProps> = ({ children }) =>
[],
);

// Hydrate on mount from localStorage (accounts/sessions) and this tab's
// sessionStorage active-account pointer. If no pointer is set yet but
// exactly one account exists, default to it — preserves today's exact
// single-account continuity until the account switcher (Parent 3) exists.
useEffect(() => {
const loadedAccounts = getAccounts();
const loadedSessions = getSessions();
setAccounts(loadedAccounts);
setSessions(loadedSessions);

let id = sessionStorage.getItem(STORAGE_ACTIVE_ACCOUNT);
const urlAccountId = new URLSearchParams(window.location.search).get("account");
const hasValidUrlAccount =
!!urlAccountId && loadedAccounts.some((a) => a.id === urlAccountId);

let id = hasValidUrlAccount ? urlAccountId : sessionStorage.getItem(STORAGE_ACTIVE_ACCOUNT);
if (!id && loadedAccounts.length === 1) {
id = loadedAccounts[0].id;
sessionStorage.setItem(STORAGE_ACTIVE_ACCOUNT, id);
}
if (id) sessionStorage.setItem(STORAGE_ACTIVE_ACCOUNT, id);
setActiveAccountId(id);
setLoading(false);
}, []);
Expand Down Expand Up @@ -160,10 +146,9 @@ export const AccountProvider: React.FC<AccountProviderProps> = ({ children }) =>
saveSession({ accountId: id, token: resp.token, expiresAt: resp.expires_at });
setAccounts(getAccounts());
setSessions(getSessions());
switchAccount(id);
return account;
},
[authContext, switchAccount],
[authContext],
);

const reauthenticate = useCallback(
Expand Down
6 changes: 6 additions & 0 deletions src/nonview/core/openAccountInNewTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Opens or focuses a browser tab when switching to a differnt account.
export function openAccountInNewTab(accountId: string): void {
const url = new URL(import.meta.env.BASE_URL, window.location.origin);
url.searchParams.set("account", accountId);
window.open(url.toString(), `quicksilver-${accountId}`);
}
65 changes: 65 additions & 0 deletions src/view/atoms/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
CircularProgress,
useMediaQuery,
useTheme,
} from "@mui/material";

interface ConfirmDialogProps {
open: boolean;
title: string;
message: React.ReactNode;
confirmLabel?: string;
cancelLabel?: string;
confirmColor?: "error" | "primary";
loading?: boolean;
onConfirm: () => void | Promise<void>;
onCancel: () => void;
}

// Reusable confirm/cancel dialog — (e.g. "Sign out of all accounts?", "Remove this account?").
const ConfirmDialog = ({
open,
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
confirmColor = "primary",
loading = false,
onConfirm,
onCancel,
}: ConfirmDialogProps) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));

return (
<Dialog open={open} onClose={onCancel} fullScreen={fullScreen} maxWidth="xs" fullWidth>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText component="div">{message}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} disabled={loading}>
{cancelLabel}
</Button>
<Button
onClick={onConfirm}
color={confirmColor}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : null}
>
{confirmLabel}
</Button>
</DialogActions>
</Dialog>
);
};

export default ConfirmDialog;
Loading