import React, { useContext, createContext, useReducer, Provider, useCallback, useEffect, useMemo, Suspense } from "react" import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { encrypt, decrypt } from './utils/encryption' import env from "../env"; import { flash } from "./toast"; import Confirmation from "./components/confirmation"; import DarkMode from "./dark-mode"; import LoadingPage from "./components/loading-page"; import AddToHomeScreen from "./add-to-homescreen"; const Login = React.lazy(() => import('./pages/login')) const SignUp = React.lazy(() => import('./pages/signup')) const Home = React.lazy(() => import('./pages/home')) const ResetPassword = React.lazy(() => import('./pages/reset-password')) const AuthorizedRoutes = React.lazy(() => import('./authorized-routes')) const jsonRegex = /^application\/json/ const textRegex = /^text\/plain/ const request = async (url: string, method: string, body: any, token: string | null, key?: string, iv?: string, stream?: boolean) => { const headers = new Headers(); headers.append('device-id', localStorage.getItem('device_id') || '') if (token) { headers.append('Authorization', `Bearer ${token}`); if (method !== 'GET') { headers.append('Content-Type', key && iv ? 'text/plain' : 'application/json'); if (key && iv) body = `~e~${encrypt(typeof body === 'object' ? JSON.stringify(body) : body, key, iv)}`; else if (typeof body === 'object') body = JSON.stringify(body); } } else if (method !== 'GET') { headers.append('Content-Type', 'application/json'); body = typeof body === 'object' ? JSON.stringify(body) : body; } return fetch(env.API_HOST + url, { method, headers, body }).then(async response => { if (!response.ok) throw new Error(response.statusText) if (textRegex.test(response.headers.get('content-type') || '')) { return response.text().then(text => { if (text.startsWith('~e~') && key && iv) { let o = null try { o = decrypt(text.slice(3), key, iv) } catch { o = null } if (o) { try { return JSON.parse(o) } catch { return o } } return text } return text }) } else if (jsonRegex.test(response.headers.get('content-type') || '')) { return response.json() } else if (response.body && stream) { return response.body } else { return response } }) } type AppState = { token: string | null, error: string | null, loading: boolean, key: string | null, iv: string | null, logData?: string, lightMode: boolean } export type AppContextType = { flash: (message: string) => void, state: AppState, setState: (newState: Partial) => void, request: (url: string, method: string, body: any, useAuth: boolean) => Promise, encrypt: (data: string | object) => string, decrypt: (data: string) => string, logout: () => void, pathname: string, queries: { [key: string]: string }, redirect: (path: string) => void } const AppContext = createContext({ state: { token: null, error: null, loading: false, key: null, iv: null } }); export const useAppContext = () => useContext(AppContext) as AppContextType; const AppProvider: Provider = AppContext.Provider export default function App() { const defaultLightMode: boolean = useMemo(() => { return !localStorage.getItem('lightMode') ? (!window.matchMedia ? true : window.matchMedia('(prefers-color-scheme: dark)').matches) : (localStorage.getItem('lightMode') === 'true' ? true : localStorage.getItem('lightMode') === 'false' ? false : !window.matchMedia ? true : window.matchMedia('(prefers-color-scheme: dark)').matches) }, []) const [state, setState] = useReducer((state: AppState, newState: AppState | ((state: AppState) => AppState)) => { if (typeof newState === 'function') return newState(state) return ({ ...state, ...newState }) }, { token: null, key: null, iv: null, error: null, loading: false, lightMode: defaultLightMode }) as any const [confirmation, setConfirmation] = React.useState<{ message: string, onConfirm: (confirmed: boolean) => void } | null>(null) const location = useLocation() const { pathname, search } = location const queries = useMemo(() => { const q = new URLSearchParams(search) const o: { [key: string]: string } = {} q.forEach((v, k) => o[k] = v) return o }, [search]) const redirect = useNavigate() const encryptData = useCallback((data: string | object) => { if (!state.key || !state.iv) throw new Error('Key or IV not set') const text = typeof data === 'object' ? JSON.stringify(data) : data return encrypt(text, state.key, state.iv) }, [state.key, state.iv]) const decryptData = useCallback((data: string) => { if (!state.key || !state.iv) throw new Error('Key or IV not set') return decrypt(data, state.key, state.iv) }, [state.key, state.iv]) const requestData = useCallback((url: string, method: string, body: any, useAuth: boolean = false, stream: boolean = false) => { if (useAuth && !state.token) throw new Error('Please login first') return request(url, method, body, useAuth ? state.token : null, state.key, state.iv, stream) }, [state.token, state.key, state.iv]) const logout = useCallback(() => { requestData('/invalidate', 'POST', {}, true).catch(() => { }) localStorage.removeItem('token') setState({ iv: null, token: null, key: null }) }, [setState, requestData]) useEffect(() => { let loggedIn = Boolean(state.token) if (!loggedIn) { const tokenInfo = localStorage.getItem('token') if (tokenInfo) { try { const { key, iv, token } = JSON.parse(tokenInfo) if (key && iv && token) { setState({ token, key, iv }) loggedIn = true } } catch { localStorage.removeItem('token') } } } else { localStorage.setItem('token', JSON.stringify({ token: state.token, key: state.key, iv: state.iv })) } if (!loggedIn && pathname !== '/login' && pathname !== '/sign-up' && pathname !== '/reset-password') { redirect('/login') } else if (loggedIn && (pathname === '/login' || pathname === '/sign-up' || pathname === '/reset-password')) { redirect('/') } }, [state.token, state.key, state.iv, pathname]) const DarkModeCSS = useMemo(() => { return !state.lightMode && }, [state.lightMode]) const ConfirmationPopup = useMemo(() => { if (!confirmation) return null return { setConfirmation(null) confirmation.onConfirm(confirmed) }} /> }, [confirmation]) return ( }> } /> } /> } /> } /> {DarkModeCSS} {ConfirmationPopup} ) }