Replace custom AuthContext with react-oidc-context

Deletes the hand-rolled AuthContext/UserManager setup and replaces it
with AuthProvider from react-oidc-context. onSigninCallback clears the
OIDC code params from the URL (unless an error is present). TokenSync
bridges the library token into the existing api/client setTokenProvider
pattern. AuthGuard updated to use auth.isLoading/isAuthenticated/
signinRedirect from the library. CallbackPage simplified to a passive
error renderer — react-oidc-context processes the OIDC exchange itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Spencer Twaddle
2026-05-02 16:38:12 -05:00
parent 65701cbdb8
commit b33ff5079c
6 changed files with 41 additions and 86 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="UserContentModel"> <component name="UserContentModel">
<attachedFolders /> <attachedFolders>
<Path>.plans</Path>
</attachedFolders>
<explicitIncludes /> <explicitIncludes />
<explicitExcludes /> <explicitExcludes />
</component> </component>
+10 -10
View File
@@ -1,10 +1,10 @@
import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './auth/AuthContext'; import { AuthProvider } from 'react-oidc-context';
import { authConfig } from './auth/authConfig';
import { TokenSync } from './auth/TokenSync';
import { AuthGuard } from './auth/AuthGuard'; import { AuthGuard } from './auth/AuthGuard';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
import { ToastProvider } from './components/Toast'; import { ToastProvider } from './components/Toast';
import { setTokenProvider } from './api/client';
import { CallbackPage } from './pages/CallbackPage'; import { CallbackPage } from './pages/CallbackPage';
import { BudgetsPage } from './pages/BudgetsPage'; import { BudgetsPage } from './pages/BudgetsPage';
import { IncomePage } from './pages/IncomePage'; import { IncomePage } from './pages/IncomePage';
@@ -12,18 +12,18 @@ import { OutgoPage } from './pages/OutgoPage';
import { SummaryPage } from './pages/SummaryPage'; import { SummaryPage } from './pages/SummaryPage';
import { SettingsPage } from './pages/SettingsPage'; import { SettingsPage } from './pages/SettingsPage';
function TokenWirer() { const onSigninCallback = () => {
const { getToken } = useAuth(); if (!window.location.search.includes('error')) {
useEffect(() => { setTokenProvider(getToken); }, [getToken]); window.history.replaceState({}, '', '/');
return null; }
} };
function App() { function App() {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<ToastProvider> <ToastProvider>
<AuthProvider> <AuthProvider {...authConfig} onSigninCallback={onSigninCallback}>
<TokenWirer /> <TokenSync />
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<Navigate to="/budgets" replace />} /> <Route path="/" element={<Navigate to="/budgets" replace />} />
@@ -1,58 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import { UserManager } from 'oidc-client-ts';
import type { User } from 'oidc-client-ts';
import { authConfig } from './authConfig';
interface AuthContextValue {
user: User | null;
isLoading: boolean;
login: () => void;
logout: () => void;
getToken: () => string | null;
}
const AuthContext = createContext<AuthContextValue | null>(null);
const userManager = new UserManager(authConfig);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
userManager.getUser().then(u => {
setUser(u);
setIsLoading(false);
});
const onUserLoaded = (u: User) => setUser(u);
const onUserUnloaded = () => setUser(null);
userManager.events.addUserLoaded(onUserLoaded);
userManager.events.addUserUnloaded(onUserUnloaded);
return () => {
userManager.events.removeUserLoaded(onUserLoaded);
userManager.events.removeUserUnloaded(onUserUnloaded);
};
}, []);
const login = () => userManager.signinRedirect();
const logout = () => userManager.signoutRedirect();
const getToken = () => user?.access_token ?? null;
return (
<AuthContext.Provider value={{ user, isLoading, login, logout, getToken }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
export { userManager };
+5 -5
View File
@@ -1,13 +1,13 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useAuth } from './AuthContext'; import { useAuth } from 'react-oidc-context';
export function AuthGuard({ children }: { children: ReactNode }) { export function AuthGuard({ children }: { children: ReactNode }) {
const { user, isLoading, login } = useAuth(); const auth = useAuth();
if (isLoading) return <div>Loading...</div>; if (auth.isLoading) return <div>Loading...</div>;
if (!user) { if (!auth.isAuthenticated) {
login(); auth.signinRedirect();
return null; return null;
} }
+11
View File
@@ -0,0 +1,11 @@
import { useEffect } from 'react';
import { useAuth } from 'react-oidc-context';
import { setTokenProvider } from '../api/client';
export function TokenSync() {
const auth = useAuth();
useEffect(() => {
setTokenProvider(() => auth.user?.access_token ?? null);
}, [auth.user]);
return null;
}
+12 -12
View File
@@ -1,18 +1,18 @@
import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { userManager } from '../auth/AuthContext';
export function CallbackPage() { export function CallbackPage() {
const navigate = useNavigate(); const [params] = useSearchParams();
const error = params.get('error');
const errorDescription = params.get('error_description');
useEffect(() => { if (error) {
userManager.signinRedirectCallback() return (
.then(() => navigate('/budgets')) <div>
.catch(err => { <h2>Sign-in failed</h2>
console.error('OIDC callback error', err); <p>{errorDescription ?? error}</p>
navigate('/'); </div>
}); );
}, [navigate]); }
return <div>Signing in...</div>; return <div>Signing in...</div>;
} }