import React, { useContext, useEffect, useMemo, useReducer } from 'react'; import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient'; import { ApiProvider } from './ApiContext'; import { listTenants } from '../lib/apiClient'; type Tenant = Record | null; interface TenantSelection { selectionToken: string; tenants: Tenant[]; } type AppStatus = | 'logged-out' | 'authenticating' | 'authenticated' | 'selecting-tenant' | 'bootstrapping' | 'ready'; interface AppState { status: AppStatus; token: string; error: string | null; isRefreshing: boolean; tenantSelection: TenantSelection | null; tenant: Tenant; tenants: Tenant[]; } type AppAction = | { type: 'LOGIN_REQUEST' } | { type: 'LOGIN_SUCCESS'; token: string; tenant?: Tenant } | { type: 'LOGIN_FAILURE'; error?: string | null } | { type: 'TENANT_SELECTION_REQUIRED'; selectionToken: string; tenants: Tenant[] } | { type: 'CLEAR_TENANT_SELECTION' } | { type: 'LOGOUT_SUCCESS' } | { type: 'BOOTSTRAP_START' } | { type: 'BOOTSTRAP_SUCCESS' } | { type: 'BOOTSTRAP_FAILURE'; error?: string | null } | { type: 'TOKEN_REFRESH_START' } | { type: 'TOKEN_REFRESH_SUCCESS'; token: string; tenant?: Tenant } | { type: 'TOKEN_REFRESH_FAILURE'; error?: string | null } | { type: 'LOGOUT' } | { type: 'RESET_ERROR' } | { type: 'SET_TENANTS'; tenants: Tenant[] }; const storage = window.sessionStorage; const STORED_TOKEN = storage?.getItem('papercrate_token') ?? ''; let STORED_TENANT: Tenant = null; if (storage) { try { const rawTenant = storage.getItem('papercrate_tenant'); if (rawTenant) { STORED_TENANT = JSON.parse(rawTenant); } } catch (error) { console.warn('[app] Failed to parse stored tenant metadata', error); } } if (STORED_TOKEN) { setAuthToken(STORED_TOKEN); } const initialAppState: AppState = { status: STORED_TOKEN ? 'authenticated' : 'logged-out', token: STORED_TOKEN, error: null, isRefreshing: false, tenantSelection: null, tenant: STORED_TENANT, tenants: [], }; const AppStateContext = React.createContext(null); const AppDispatchContext = React.createContext | null>(null); const appStateReducer = (state: AppState, action: AppAction): AppState => { switch (action.type) { case 'LOGIN_REQUEST': return { ...state, status: 'authenticating', error: null, tenantSelection: null, tenant: null, tenants: [], }; case 'LOGIN_SUCCESS': return { ...state, status: 'authenticated', token: action.token, error: null, tenantSelection: null, tenant: action.tenant ?? null, tenants: state.tenants, }; case 'LOGIN_FAILURE': return { status: 'logged-out', token: '', error: action.error ?? null, isRefreshing: false, tenantSelection: null, tenant: null, tenants: [], }; case 'TENANT_SELECTION_REQUIRED': return { status: 'selecting-tenant', token: '', error: null, isRefreshing: false, tenantSelection: { selectionToken: action.selectionToken, tenants: action.tenants, }, tenant: null, tenants: [], }; case 'CLEAR_TENANT_SELECTION': return { status: 'logged-out', token: '', error: null, isRefreshing: false, tenantSelection: null, tenant: null, tenants: [], }; case 'LOGOUT_SUCCESS': case 'LOGOUT': return { status: 'logged-out', token: '', error: null, isRefreshing: false, tenantSelection: null, tenant: null, tenants: [], }; case 'BOOTSTRAP_START': return { ...state, status: 'bootstrapping', error: null }; case 'BOOTSTRAP_SUCCESS': return { ...state, status: 'ready', error: null }; case 'BOOTSTRAP_FAILURE': return { ...state, status: 'authenticated', error: action.error ?? null }; case 'TOKEN_REFRESH_START': return { ...state, isRefreshing: true, error: null }; case 'TOKEN_REFRESH_SUCCESS': return { ...state, token: action.token, isRefreshing: false, status: state.status === 'logged-out' ? 'authenticated' : state.status, tenantSelection: null, tenant: action.tenant ?? state.tenant ?? null, tenants: state.tenants, }; case 'TOKEN_REFRESH_FAILURE': return { status: 'logged-out', token: '', error: action.error ?? null, isRefreshing: false, tenantSelection: null, tenant: null, tenants: [], }; case 'RESET_ERROR': return { ...state, error: null }; case 'SET_TENANTS': return { ...state, tenants: Array.isArray(action.tenants) ? action.tenants : [], }; default: return state; } }; const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const [state, dispatch] = useReducer(appStateReducer, initialAppState); useEffect(() => { const token = state.token ?? ''; if (token) { setAuthToken(token); storage?.setItem('papercrate_token', token); } else { clearAuthToken(); storage?.removeItem('papercrate_token'); } }, [state.token]); useEffect(() => { if (state.tenant) { try { storage?.setItem('papercrate_tenant', JSON.stringify(state.tenant)); } catch (error) { console.warn('[app] Failed to persist tenant info', error); } } else { storage?.removeItem('papercrate_tenant'); } }, [state.tenant]); useEffect(() => { setAuthRefreshHandlers({ onRefreshSuccess: (token, payload) => { dispatch({ type: 'TOKEN_REFRESH_SUCCESS', token, tenant: payload?.tenant ?? null }); }, onRefreshFailure: (error) => { dispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null }); }, }); return () => { setAuthRefreshHandlers({}); }; }, [dispatch]); useEffect(() => { let abort = false; const loadTenants = async () => { if (state.status !== 'authenticated' || !state.token) { dispatch({ type: 'SET_TENANTS', tenants: [] }); return; } try { if (!abort) { const tenants = await listTenants(); dispatch({ type: 'SET_TENANTS', tenants, }); } } catch (error) { if (!abort) { console.warn('Failed to load tenant list', error); } } }; loadTenants(); return () => { abort = true; }; }, [state.status, state.token, dispatch]); const stateValue = useMemo(() => state, [state]); return ( {children} ); }; const useAppState = (): AppState => { const context = useContext(AppStateContext); if (!context) { throw new Error('useAppState must be used within an AppStateProvider.'); } return context; }; const useAppDispatch = (): React.Dispatch => { const context = useContext(AppDispatchContext); if (!context) { throw new Error('useAppDispatch must be used within an AppStateProvider.'); } return context; }; export { AppStateProvider, useAppState, useAppDispatch };