diff --git a/browser-extensions/chrome/src/app/App.tsx b/browser-extensions/chrome/src/app/App.tsx index eb08e4997..df9527635 100644 --- a/browser-extensions/chrome/src/app/App.tsx +++ b/browser-extensions/chrome/src/app/App.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; -import { useDb } from './context/DbContext'; import { useMinDurationLoading } from './hooks/useMinDurationLoading'; import Header from './components/Layout/Header'; import BottomNav from './components/Layout/BottomNav'; @@ -15,6 +14,7 @@ import CredentialDetails from './pages/CredentialDetails'; import EmailDetails from './pages/EmailDetails'; import Settings from './pages/Settings'; import GlobalStateChangeHandler from './components/GlobalStateChangeHandler'; +import { useLoading } from './context/LoadingContext'; /** * Route configuration. @@ -31,9 +31,9 @@ type RouteConfig = { */ const App: React.FC = () => { const authContext = useAuth(); - const dbContext = useDb(); - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const { isInitialLoading } = useLoading(); const [isLoading, setIsLoading] = useMinDurationLoading(true, 150); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const [message, setMessage] = useState(null); // Add these route configurations @@ -47,14 +47,11 @@ const App: React.FC = () => { { path: '/settings', element: , showBackButton: true, title: 'Settings' }, ]; - /** - * Set loading state to false when auth and db are initialized. - */ useEffect(() => { - if (authContext.isInitialized && dbContext.dbInitialized) { + if (!isInitialLoading) { setIsLoading(false); } - }, [authContext.isInitialized, dbContext.dbInitialized, setIsLoading]); + }, [isInitialLoading, setIsLoading]); /** * Print global message if it exists. @@ -66,21 +63,15 @@ const App: React.FC = () => { } }, [authContext, authContext.globalMessage]); - if (isLoading) { - return ( -
-
-
- -
-
-
- ); - } - return (
+ {isLoading && ( +
+ +
+ )} +
setIsUserMenuOpen(!isUserMenuOpen)} diff --git a/browser-extensions/chrome/src/app/context/LoadingContext.tsx b/browser-extensions/chrome/src/app/context/LoadingContext.tsx index a1acbe504..215fc30c7 100644 --- a/browser-extensions/chrome/src/app/context/LoadingContext.tsx +++ b/browser-extensions/chrome/src/app/context/LoadingContext.tsx @@ -5,6 +5,8 @@ type LoadingContextType = { isLoading: boolean; showLoading: () => void; hideLoading: () => void; + isInitialLoading: boolean; + setIsInitialLoading: (isInitialLoading: boolean) => void; } /** @@ -16,6 +18,16 @@ const LoadingContext = createContext(undefined); * Loading provider */ export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + /** + * Initial loading state for when extension is first loaded. This initial loading state is + * hidden by the component that is rendered when the extension is first loaded to prevent + * multiple loading spinners from being shown. + */ + const [isInitialLoading, setIsInitialLoading] = useState(true); + + /** + * Loading state that can be used by other components during normal operation. + */ const [isLoading, setIsLoading] = useState(false); /** @@ -29,7 +41,7 @@ export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ child const hideLoading = (): void => setIsLoading(false); return ( - + {children} diff --git a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx index f47b3cd73..9280167d9 100644 --- a/browser-extensions/chrome/src/app/pages/CredentialsList.tsx +++ b/browser-extensions/chrome/src/app/pages/CredentialsList.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useDb } from '../context/DbContext'; import { Credential } from '../../shared/types/Credential'; import { Buffer } from 'buffer'; @@ -9,6 +9,8 @@ import { VaultResponse } from '../../shared/types/webapi/VaultResponse'; import ReloadButton from '../components/ReloadButton'; import { useAuth } from '../context/AuthContext'; import { StatusResponse } from '../../shared/types/webapi/StatusResponse'; +import LoadingSpinner from '../components/LoadingSpinner'; +import { useMinDurationLoading } from '../hooks/useMinDurationLoading'; /** * Credentials list page. @@ -18,42 +20,18 @@ const CredentialsList: React.FC = () => { const webApi = useWebApi(); const [credentials, setCredentials] = useState([]); const navigate = useNavigate(); - const { showLoading, hideLoading } = useLoading(); + const { showLoading, hideLoading, setIsInitialLoading } = useLoading(); const authContext = useAuth(); - useEffect(() => { - /** - * Check if the extension is (still) supported by the API and if the local vault is up to date. - */ - const checkStatus = async (): Promise => { - if (!dbContext?.sqliteClient) return; - - const statusResponse = await webApi.get('Auth/status') as StatusResponse; - if (!statusResponse.supported) { - authContext.logout('This version of the AliasVault browser extension is outdated. Please update to the latest version.'); - return; - } - - if (statusResponse.vaultRevision > dbContext.vaultRevision) { - await onRefresh(); - } - - // Load credentials - try { - const results = dbContext.sqliteClient.getAllCredentials(); - setCredentials(results); - } catch (err) { - console.error('Error loading credentials:', err); - } - }; - - checkStatus(); - }); + /** + * Loading state with minimum duration for more fluid UX. + */ + const [isLoading, setIsLoading] = useMinDurationLoading(true, 150); /** * Retrieve latest vault and refresh the page. */ - const onRefresh = async () : Promise => { + const onRefresh = useCallback(async () : Promise => { if (!dbContext?.sqliteClient) return; try { @@ -77,20 +55,64 @@ const CredentialsList: React.FC = () => { try { const results = dbContext.sqliteClient.getAllCredentials(); setCredentials(results); + setIsLoading(false); + setIsInitialLoading(false); } catch (err) { console.error('Error loading credentials:', err); } } catch (err) { console.error('Refresh error:', err); } - }; + }, [dbContext, webApi, authContext, hideLoading, setIsInitialLoading, setIsLoading]); + useEffect(() => { + /** + * Check if the extension is (still) supported by the API and if the local vault is up to date. + */ + const checkStatus = async (): Promise => { + if (!dbContext?.sqliteClient) return; + + const statusResponse = await webApi.get('Auth/status') as StatusResponse; + if (!statusResponse.supported) { + authContext.logout('This version of the AliasVault browser extension is outdated. Please update to the latest version.'); + return; + } + + if (statusResponse.vaultRevision > dbContext.vaultRevision) { + await onRefresh(); + } + + // Load credentials + try { + const results = dbContext.sqliteClient.getAllCredentials(); + setCredentials(results); + setIsLoading(false); + setIsInitialLoading(false); + } catch (err) { + console.error('Error loading credentials:', err); + } + }; + + checkStatus(); + }, [authContext, dbContext?.sqliteClient, dbContext?.vaultRevision, onRefresh, webApi, setIsInitialLoading, setIsLoading]); + + /** + * Manually refresh the credentials list. + */ const onManualRefresh = async (): Promise => { showLoading(); await onRefresh(); hideLoading(); }; + if (isLoading) { + return ( +
+ +
+ ); + } + return (
diff --git a/browser-extensions/chrome/src/app/pages/Home.tsx b/browser-extensions/chrome/src/app/pages/Home.tsx index 008d5f108..aef4fddc6 100644 --- a/browser-extensions/chrome/src/app/pages/Home.tsx +++ b/browser-extensions/chrome/src/app/pages/Home.tsx @@ -5,6 +5,7 @@ import Login from './Login'; import UnlockSuccess from './UnlockSuccess'; import { useNavigate } from 'react-router-dom'; import { useDb } from '../context/DbContext'; +import { useLoading } from '../context/LoadingContext'; /** * Home page that shows the correct page based on the user's authentication state. @@ -14,6 +15,7 @@ const Home: React.FC = () => { const authContext = useAuth(); const dbContext = useDb(); const navigate = useNavigate(); + const { setIsInitialLoading } = useLoading(); const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false); const needsUnlock = (!authContext.isLoggedIn && authContext.isInitialized) || (!dbContext.dbAvailable && dbContext.dbInitialized); @@ -28,6 +30,9 @@ const Home: React.FC = () => { } }, [isLoggedIn, needsUnlock, isInlineUnlockMode, navigate]); + // Set initial loading state to false once the page is loaded until here. + setIsInitialLoading(false); + if (!isLoggedIn) { return ; }