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"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<attachedFolders>
<Path>.plans</Path>
</attachedFolders>
<explicitIncludes />
<explicitExcludes />
</component>
+9 -9
View File
@@ -1,10 +1,10 @@
import { useEffect } from 'react';
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 { ErrorBoundary } from './components/ErrorBoundary';
import { ToastProvider } from './components/Toast';
import { setTokenProvider } from './api/client';
import { CallbackPage } from './pages/CallbackPage';
import { BudgetsPage } from './pages/BudgetsPage';
import { IncomePage } from './pages/IncomePage';
@@ -12,18 +12,18 @@ import { OutgoPage } from './pages/OutgoPage';
import { SummaryPage } from './pages/SummaryPage';
import { SettingsPage } from './pages/SettingsPage';
function TokenWirer() {
const { getToken } = useAuth();
useEffect(() => { setTokenProvider(getToken); }, [getToken]);
return null;
const onSigninCallback = () => {
if (!window.location.search.includes('error')) {
window.history.replaceState({}, '', '/');
}
};
function App() {
return (
<ErrorBoundary>
<ToastProvider>
<AuthProvider>
<TokenWirer />
<AuthProvider {...authConfig} onSigninCallback={onSigninCallback}>
<TokenSync />
<BrowserRouter>
<Routes>
<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 { useAuth } from './AuthContext';
import { useAuth } from 'react-oidc-context';
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) {
login();
if (!auth.isAuthenticated) {
auth.signinRedirect();
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 { useNavigate } from 'react-router-dom';
import { userManager } from '../auth/AuthContext';
import { useSearchParams } from 'react-router-dom';
export function CallbackPage() {
const navigate = useNavigate();
const [params] = useSearchParams();
const error = params.get('error');
const errorDescription = params.get('error_description');
useEffect(() => {
userManager.signinRedirectCallback()
.then(() => navigate('/budgets'))
.catch(err => {
console.error('OIDC callback error', err);
navigate('/');
});
}, [navigate]);
if (error) {
return (
<div>
<h2>Sign-in failed</h2>
<p>{errorDescription ?? error}</p>
</div>
);
}
return <div>Signing in...</div>;
}