import React, { useContext, useEffect, useMemo, useReducer } from 'react'; import api from '../lib/api'; const storage = typeof window !== 'undefined' ? window.sessionStorage : undefined; const STORED_TOKEN = storage?.getItem('papercrate_token') ?? ''; let STORED_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) { api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`; } const initialAppState = { 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); const appStateReducer = (state, action) => { 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': 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 'LOGOUT': return { status: 'logged-out', token: '', 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 = ({ children }) => { const [state, dispatch] = useReducer(appStateReducer, initialAppState); useEffect(() => { const token = state.token ?? ''; if (token) { api.defaults.headers.common.Authorization = `Bearer ${token}`; storage?.setItem('papercrate_token', token); } else { delete api.defaults.headers.common.Authorization; 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(() => { let abort = false; const loadTenants = async () => { if (state.status !== 'authenticated' || !state.token) { dispatch({ type: 'SET_TENANTS', tenants: [] }); return; } try { const { data } = await api.get('/auth/tenants'); if (!abort) { dispatch({ type: 'SET_TENANTS', tenants: Array.isArray(data?.tenants) ? data.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 = () => { const context = useContext(AppStateContext); if (!context) { throw new Error('useAppState must be used within an AppStateProvider.'); } return context; }; const useAppDispatch = () => { const context = useContext(AppDispatchContext); if (!context) { throw new Error('useAppDispatch must be used within an AppStateProvider.'); } return context; }; export { api, AppStateProvider, useAppState, useAppDispatch };