From 7776fb6d82b9b1ccdb49cdcb141df94f7d88e38b Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 17 Jun 2025 19:19:23 +0200 Subject: [PATCH] Remember last visited page in browser extension and navigate back on reopen (#928) --- .../src/entrypoints/popup/App.tsx | 75 ++++++++-------- .../popup/components/Layout/BottomNav.tsx | 10 ++- .../popup/context/NavigationContext.tsx | 85 +++++++++++++++++++ .../entrypoints/popup/pages/AuthSettings.tsx | 6 +- .../popup/pages/CredentialAddEdit.tsx | 4 +- .../src/entrypoints/popup/pages/Login.tsx | 5 +- .../src/entrypoints/popup/pages/Unlock.tsx | 5 +- 7 files changed, 145 insertions(+), 45 deletions(-) create mode 100644 apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx diff --git a/apps/browser-extension/src/entrypoints/popup/App.tsx b/apps/browser-extension/src/entrypoints/popup/App.tsx index a7807a30d..b7b737b2d 100644 --- a/apps/browser-extension/src/entrypoints/popup/App.tsx +++ b/apps/browser-extension/src/entrypoints/popup/App.tsx @@ -19,7 +19,9 @@ import Logout from '@/entrypoints/popup/pages/Logout'; import Settings from '@/entrypoints/popup/pages/Settings'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; + import '@/entrypoints/popup/style.css'; +import { NavigationProvider } from './context/NavigationContext'; /** * Route configuration. @@ -74,44 +76,45 @@ const App: React.FC = () => { return ( -
- {isLoading && ( -
- -
- )} + +
+ {isLoading && ( +
+ +
+ )} - -
+ +
-
-
- {message && ( -

{message}

- )} - - {routes.map((route) => ( - - ))} - -
-
- - -
+
+
+ {message && ( +

{message}

+ )} + + {routes.map((route) => ( + + ))} + +
+
+ +
+
); }; diff --git a/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx b/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx index 4c1d8b22e..1f79b64cc 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx @@ -18,9 +18,13 @@ const BottomNav: React.FC = () => { // Add effect to update currentTab based on route useEffect(() => { - const path = location.pathname.substring(1) as TabName; - if (['credentials', 'emails', 'settings'].includes(path)) { - setCurrentTab(path); + const path = location.pathname.substring(1); // Remove leading slash + const tabNames: TabName[] = ['credentials', 'emails', 'settings']; + + // Find the first tab name that matches the start of the path + const matchingTab = tabNames.find(tab => path === tab || path.startsWith(`${tab}/`)); + if (matchingTab) { + setCurrentTab(matchingTab); } }, [location]); diff --git a/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx new file mode 100644 index 000000000..910c4a674 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx @@ -0,0 +1,85 @@ +import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { storage } from '#imports'; + +const LAST_VISITED_PAGE_KEY = 'local:lastVisitedPage'; +const LAST_VISITED_TIME_KEY = 'local:lastVisitedTime'; +const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds + +type NavigationContextType = { + storeCurrentPage: () => Promise; + restoreLastPage: () => Promise; +}; + +const NavigationContext = createContext(undefined); + +/** + * Navigation provider component that handles storing and restoring the last visited page. + */ +export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const location = useLocation(); + const navigate = useNavigate(); + const [isInitialized, setIsInitialized] = useState(false); + + /** + * Store the current page path and timestamp in storage. + */ + const storeCurrentPage = useCallback(async (): Promise => { + await storage.setItem(LAST_VISITED_PAGE_KEY, location.pathname); + await storage.setItem(LAST_VISITED_TIME_KEY, Date.now()); + }, [location.pathname]); + + /** + * Restore the last visited page if it was visited within the memory duration. + */ + const restoreLastPage = useCallback(async (): Promise => { + const lastPage = await storage.getItem(LAST_VISITED_PAGE_KEY) as string; + const lastVisitTime = await storage.getItem(LAST_VISITED_TIME_KEY) as number; + + if (lastPage && lastVisitTime) { + const timeSinceLastVisit = Date.now() - lastVisitTime; + if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) { + navigate(lastPage); + } + } + }, [navigate]); + + // Store the current page whenever it changes + useEffect(() => { + if (isInitialized) { + storeCurrentPage(); + } + }, [location.pathname, isInitialized, storeCurrentPage]); + + // Restore the last page on initial load + useEffect(() => { + if (!isInitialized) { + restoreLastPage(); + setIsInitialized(true); + } + }, [isInitialized, restoreLastPage]); + + const contextValue = useMemo(() => ({ + storeCurrentPage, + restoreLastPage + }), [storeCurrentPage, restoreLastPage]); + + return ( + + {children} + + ); +}; + +/** + * Hook to access the navigation context. + * @returns The navigation context + */ +export const useNavigation = (): NavigationContextType => { + const context = useContext(NavigationContext); + if (context === undefined) { + throw new Error('useNavigation must be used within a NavigationProvider'); + } + return context; +}; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/AuthSettings.tsx b/apps/browser-extension/src/entrypoints/popup/pages/AuthSettings.tsx index 44340f044..78dcb15c1 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/AuthSettings.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/AuthSettings.tsx @@ -1,6 +1,8 @@ import React, { useState, useEffect } from 'react'; import * as Yup from 'yup'; +import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; + import { AppInfo } from '@/utils/AppInfo'; import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants'; @@ -55,6 +57,7 @@ const AuthSettings: React.FC = () => { const [customClientUrl, setCustomClientUrl] = useState(''); const [isGloballyEnabled, setIsGloballyEnabled] = useState(true); const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({}); + const { setIsInitialLoading } = useLoading(); useEffect(() => { /** @@ -83,10 +86,11 @@ const AuthSettings: React.FC = () => { } else { setSelectedOption(DEFAULT_OPTIONS[0].value); } + setIsInitialLoading(false); }; loadStoredSettings(); - }, []); + }, [setIsInitialLoading]); /** * Handle option change diff --git a/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx b/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx index df39c7c8c..1af637655 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/CredentialAddEdit.tsx @@ -106,7 +106,7 @@ const CredentialAddEdit: React.FC = () => { setTimeout(() => { serviceNameRef.current?.focus(); }, 100); - + setIsInitialLoading(false); return; } @@ -122,6 +122,7 @@ const CredentialAddEdit: React.FC = () => { }); setMode('manual'); + setIsInitialLoading(false); // On create mode, focus the service name field after a short delay to ensure the component is mounted } else { @@ -130,6 +131,7 @@ const CredentialAddEdit: React.FC = () => { } } catch (err) { console.error('Error loading credential:', err); + setIsInitialLoading(false); } }, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue]); diff --git a/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx b/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx index c46a23558..8ab8f3a55 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx @@ -29,7 +29,7 @@ const Login: React.FC = () => { username: '', password: '', }); - const { showLoading, hideLoading } = useLoading(); + const { showLoading, hideLoading, setIsInitialLoading } = useLoading(); const [rememberMe, setRememberMe] = useState(true); const [loginResponse, setLoginResponse] = useState(null); const [passwordHashString, setPasswordHashString] = useState(null); @@ -53,9 +53,10 @@ const Login: React.FC = () => { } setClientUrl(clientUrl); + setIsInitialLoading(false); }; loadClientUrl(); - }, []); + }, [setIsInitialLoading]); /** * Handle submit diff --git a/apps/browser-extension/src/entrypoints/popup/pages/Unlock.tsx b/apps/browser-extension/src/entrypoints/popup/pages/Unlock.tsx index 4e8dc8ac8..3109e319d 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/Unlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/Unlock.tsx @@ -29,7 +29,7 @@ const Unlock: React.FC = () => { const [password, setPassword] = useState(''); const [error, setError] = useState(null); - const { showLoading, hideLoading } = useLoading(); + const { showLoading, hideLoading, setIsInitialLoading } = useLoading(); useEffect(() => { /** @@ -41,10 +41,11 @@ const Unlock: React.FC = () => { if (statusError !== null) { await webApi.logout(statusError); } + setIsInitialLoading(false); }; checkStatus(); - }, [webApi, authContext]); + }, [webApi, authContext, setIsInitialLoading]); /** * Handle submit