From 633e50050520a38995bb280d205a44b3a76c315a Mon Sep 17 00:00:00 2001 From: saknarajapakshe Date: Wed, 15 Jul 2026 09:15:03 +0530 Subject: [PATCH] feat: add ConfirmDialog component and openAccountInNewTab utility for account switching --- src/nonview/core/AccountContext.tsx | 29 +++-------- src/nonview/core/openAccountInNewTab.ts | 6 +++ src/view/atoms/ConfirmDialog.tsx | 65 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 src/nonview/core/openAccountInNewTab.ts create mode 100644 src/view/atoms/ConfirmDialog.tsx diff --git a/src/nonview/core/AccountContext.tsx b/src/nonview/core/AccountContext.tsx index 008c328..d964c22 100644 --- a/src/nonview/core/AccountContext.tsx +++ b/src/nonview/core/AccountContext.tsx @@ -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[]; @@ -39,13 +35,8 @@ interface AccountContextValue { addAccount: (data: RegistrationData) => Promise; reauthenticate: (id: string, password: string) => Promise; updateAccount: (id: string, updates: Partial) => 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; - // 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; logoutAll: () => Promise; } @@ -84,9 +75,6 @@ export const AccountProvider: React.FC = ({ 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(activeSession); useEffect(() => { activeSessionRef.current = activeSession; @@ -98,8 +86,6 @@ export const AccountProvider: React.FC = ({ 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); @@ -109,21 +95,21 @@ export const AccountProvider: React.FC = ({ 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); }, []); @@ -160,10 +146,9 @@ export const AccountProvider: React.FC = ({ 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( diff --git a/src/nonview/core/openAccountInNewTab.ts b/src/nonview/core/openAccountInNewTab.ts new file mode 100644 index 0000000..6046b8f --- /dev/null +++ b/src/nonview/core/openAccountInNewTab.ts @@ -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}`); +} diff --git a/src/view/atoms/ConfirmDialog.tsx b/src/view/atoms/ConfirmDialog.tsx new file mode 100644 index 0000000..083c210 --- /dev/null +++ b/src/view/atoms/ConfirmDialog.tsx @@ -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; + 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 ( + + {title} + + {message} + + + + + + + ); +}; + +export default ConfirmDialog;