diff --git a/apps/browser-extension/src/entrypoints/popup/App.tsx b/apps/browser-extension/src/entrypoints/popup/App.tsx
index 1e0f6096f..3353b8d36 100644
--- a/apps/browser-extension/src/entrypoints/popup/App.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/App.tsx
@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
-import GlobalStateChangeHandler from '@/entrypoints/popup/components/GlobalStateChangeHandler';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
import Header from '@/entrypoints/popup/components/Layout/Header';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
@@ -15,9 +14,10 @@ import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
-import Home from '@/entrypoints/popup/pages/Home';
+import Index from '@/entrypoints/popup/pages/Index';
import Login from '@/entrypoints/popup/pages/Login';
import Logout from '@/entrypoints/popup/pages/Logout';
+import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
import Settings from '@/entrypoints/popup/pages/Settings';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
@@ -49,7 +49,8 @@ const App: React.FC = () => {
// Add these route configurations
const routes: RouteConfig[] = [
- { path: '/', element: , showBackButton: false },
+ { path: '/', element: , showBackButton: false },
+ { path: '/reinitialize', element: , showBackButton: false },
{ path: '/login', element: , showBackButton: false },
{ path: '/unlock', element: , showBackButton: false },
{ path: '/unlock-success', element: window.location.search = ''} />, showBackButton: false },
@@ -92,7 +93,6 @@ const App: React.FC = () => {
)}
-
{
- const authContext = useAuth();
- const navigate = useNavigate();
- const lastLoginState = useRef(authContext.isLoggedIn);
- const initialRender = useRef(true);
-
- /**
- * Listen for auth logged in changes and redirect to home page if logged in state changes to handle logins and logouts.
- */
- useEffect(() => {
- // Only navigate when auth state is different from the last state we acted on.
- if (lastLoginState.current !== authContext.isLoggedIn) {
- lastLoginState.current = authContext.isLoggedIn;
-
- /**
- * Skip the first auth state change to avoid redirecting when popup opens for the first time
- * which already causes the auth state to change from false to true.
- */
- if (initialRender.current) {
- initialRender.current = false;
- return;
- }
-
- // Redirect to home page if logged in state changes.
- navigate('/');
- }
- }, [authContext.isLoggedIn]); // eslint-disable-line react-hooks/exhaustive-deps
-
- return null;
-};
-
-export default GlobalStateChangeHandler;
\ No newline at end of file
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 7eb53a882..f7a953779 100644
--- a/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/components/Layout/BottomNav.tsx
@@ -31,9 +31,8 @@ const BottomNav: React.FC = () => {
navigate(`/${tab}`);
};
-
// Auth pages that don't show bottom navigation but still show header
- const authPages = ['/login', '/unlock', '/unlock-success', '/upgrade'];
+ const authPages = ['/login', '/auth-settings', '/unlock', '/unlock-success', '/upgrade'];
const isAuthPage = authPages.includes(location.pathname);
if (isAuthPage) {
diff --git a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx
index 2f904ad76..8c31ad89e 100644
--- a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx
@@ -40,17 +40,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
* @returns object containing whether the user is logged in.
*/
const initializeAuth = useCallback(async () : Promise<{ isLoggedIn: boolean }> => {
+ let isLoggedIn = false;
+
const accessToken = await storage.getItem('local:accessToken') as string;
const refreshToken = await storage.getItem('local:refreshToken') as string;
const username = await storage.getItem('local:username') as string;
if (accessToken && refreshToken && username) {
setUsername(username);
setIsLoggedIn(true);
+ isLoggedIn = true;
}
setIsInitialized(true);
return { isLoggedIn };
- }, [setUsername, setIsLoggedIn, isLoggedIn]);
+ }, [setUsername, setIsLoggedIn]);
/**
* Check for tokens in browser local storage on initial load when this context is mounted.
@@ -94,6 +97,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
setUsername(null);
+ console.log('setting isLoggedIn to false');
setIsLoggedIn(false);
}, [dbContext]);
diff --git a/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx
index b59b20644..4eb5fa164 100644
--- a/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx
@@ -1,17 +1,14 @@
-import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
-import { sendMessage } from 'webext-bridge/popup';
+import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
+import { useLocation } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
-import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { storage } from '#imports';
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
-const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
type NavigationHistoryEntry = {
pathname: string;
@@ -21,7 +18,6 @@ type NavigationHistoryEntry = {
type NavigationContextType = {
storeCurrentPage: () => Promise;
- restoreLastPage: () => Promise;
isFullyInitialized: boolean;
requiresAuth: boolean;
};
@@ -29,29 +25,25 @@ type NavigationContextType = {
const NavigationContext = createContext(undefined);
/**
- * Navigation provider component that handles storing and restoring the last visited page,
- * as well as managing initialization and auth state redirects.
+ * Navigation provider component that handles storing the last visited page.
*/
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
- const navigate = useNavigate();
- const [isInitialized, setIsInitialized] = useState(false);
- const { setIsInitialLoading } = useLoading();
// Auth and DB state
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
- const { dbInitialized, dbAvailable } = useDb();
+ const { dbInitialized, dbAvailable, upgradeRequired } = useDb();
// Derived state
const isFullyInitialized = authInitialized && dbInitialized;
- const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable);
+ const requiresAuth = isFullyInitialized && (!isLoggedIn || (!dbAvailable && !upgradeRequired));
/**
* Store the current page path, timestamp, and navigation history in storage.
*/
const storeCurrentPage = useCallback(async (): Promise => {
// Pages that are not allowed to be stored as these are auth conditional pages.
- const notAllowedPaths = ['/', '/login', '/unlock', '/unlock-success', '/auth-settings'];
+ const notAllowedPaths = ['/', '/reinitialize', '/login', '/unlock', '/unlock-success', '/auth-settings', '/upgrade'];
// Only store the page if we're fully initialized and don't need auth
if (isFullyInitialized && !requiresAuth && !notAllowedPaths.includes(location.pathname)) {
@@ -77,101 +69,18 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
}
}, [location, isFullyInitialized, requiresAuth]);
- /**
- * Restore the last visited page and navigation history if it was visited within the memory duration.
- */
- const restoreLastPage = useCallback(async (): Promise => {
- // Only restore if we're fully initialized and don't need auth
- if (!isFullyInitialized || requiresAuth) {
- return;
- }
-
- const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
- storage.getItem(LAST_VISITED_PAGE_KEY) as Promise,
- storage.getItem(LAST_VISITED_TIME_KEY) as Promise,
- storage.getItem(NAVIGATION_HISTORY_KEY) as Promise,
- ]);
-
- if (lastPage && lastVisitTime) {
- const timeSinceLastVisit = Date.now() - lastVisitTime;
- if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
- // Restore the navigation history
- if (savedHistory?.length) {
- // First navigate to credentials page as the base
- navigate('/credentials', { replace: true });
-
- // Then restore the history stack
- for (const entry of savedHistory) {
- navigate(entry.pathname + entry.search + entry.hash);
- }
- return;
- }
-
- // Fallback to simple navigation if no history
- navigate('/credentials', { replace: true });
- navigate(lastPage, { replace: true });
- return;
- }
- }
-
- // Duration has expired, clear all stored navigation data
- await Promise.all([
- storage.removeItem(LAST_VISITED_PAGE_KEY),
- storage.removeItem(LAST_VISITED_TIME_KEY),
- storage.removeItem(NAVIGATION_HISTORY_KEY),
- sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
- ]);
-
- // Navigate to the credentials page as default entry page.
- navigate('/credentials', { replace: true });
- }, [navigate, isFullyInitialized, requiresAuth]);
-
- // Handle initialization and auth state changes
- useEffect(() => {
- // Check for inline unlock mode
- const urlParams = new URLSearchParams(window.location.search);
- const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
-
- if (isFullyInitialized) {
- setIsInitialLoading(false);
-
- if (requiresAuth) {
- const allowedPaths = ['/login', '/unlock', '/unlock-success', '/auth-settings'];
- if (allowedPaths.includes(location.pathname)) {
- // Do not override the navigation if the current path is in the allowed paths.
- return;
- }
-
- // Determine which auth page to show
- if (!isLoggedIn) {
- navigate('/login', { replace: true });
- } else if (!dbAvailable) {
- navigate('/unlock', { replace: true });
- }
- } else if (inlineUnlock) {
- navigate('/unlock-success', { replace: true });
- } else if (!isInitialized) {
- // First initialization, try to restore last page or go to credentials
- restoreLastPage().then(() => {
- setIsInitialized(true);
- });
- }
- }
- }, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, isInitialized, navigate, restoreLastPage, setIsInitialLoading, location.pathname]);
-
// Store the current page whenever it changes
useEffect(() => {
- if (isInitialized) {
+ if (isFullyInitialized) {
storeCurrentPage();
}
- }, [location.pathname, location.search, location.hash, isInitialized, storeCurrentPage]);
+ }, [location.pathname, location.search, location.hash, isFullyInitialized, storeCurrentPage]);
const contextValue = useMemo(() => ({
storeCurrentPage,
- restoreLastPage,
isFullyInitialized,
requiresAuth
- }), [storeCurrentPage, restoreLastPage, isFullyInitialized, requiresAuth]);
+ }), [storeCurrentPage, isFullyInitialized, requiresAuth]);
return (
diff --git a/apps/browser-extension/src/entrypoints/popup/pages/Home.tsx b/apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
similarity index 86%
rename from apps/browser-extension/src/entrypoints/popup/pages/Home.tsx
rename to apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
index b500c123b..d015b6fe8 100644
--- a/apps/browser-extension/src/entrypoints/popup/pages/Home.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
@@ -14,7 +14,7 @@ const Home: React.FC = () => {
return null;
}
- return ;
+ return ;
};
export default Home;
\ No newline at end of file
diff --git a/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx b/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx
index bcfebb609..d42de9e57 100644
--- a/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx
+++ b/apps/browser-extension/src/entrypoints/popup/pages/Login.tsx
@@ -1,6 +1,7 @@
import { Buffer } from 'buffer';
import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import Button from '@/entrypoints/popup/components/Button';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
@@ -27,6 +28,7 @@ import { storage } from '#imports';
* Login page
*/
const Login: React.FC = () => {
+ const navigate = useNavigate();
const authContext = useAuth();
const dbContext = useDb();
const { setHeaderButtons } = useHeaderButtons();
@@ -155,11 +157,21 @@ const Login: React.FC = () => {
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
// Initialize the SQLite context with the new vault data.
- await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
+ const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
// Set logged in status to true which refreshes the app.
await authContext.login();
+ // If there are pending migrations, redirect to the upgrade page.
+ if (await sqliteClient.hasPendingMigrations()) {
+ navigate('/upgrade', { replace: true });
+ hideLoading();
+ return;
+ }
+
+ // Navigate to credentials page after successful login
+ navigate('/credentials', { replace: true });
+
// Show app.
hideLoading();
} catch (err) {
@@ -222,11 +234,20 @@ const Login: React.FC = () => {
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
// Initialize the SQLite context with the new vault data.
- await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
+ const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
// Set logged in status to true which refreshes the app.
await authContext.login();
+ // If there are pending migrations, redirect to the upgrade page.
+ if (await sqliteClient.hasPendingMigrations()) {
+ navigate('/upgrade', { replace: true });
+ return;
+ }
+
+ // Navigate to credentials page after successful login
+ navigate('/credentials', { replace: true });
+
// Reset 2FA state and login response as it's no longer needed
setTwoFactorRequired(false);
setTwoFactorCode('');
diff --git a/apps/browser-extension/src/entrypoints/popup/pages/Reinitialize.tsx b/apps/browser-extension/src/entrypoints/popup/pages/Reinitialize.tsx
new file mode 100644
index 000000000..92beded0f
--- /dev/null
+++ b/apps/browser-extension/src/entrypoints/popup/pages/Reinitialize.tsx
@@ -0,0 +1,161 @@
+import React, { useCallback, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { sendMessage } from 'webext-bridge/popup';
+
+import { useAuth } from '@/entrypoints/popup/context/AuthContext';
+import { useDb } from '@/entrypoints/popup/context/DbContext';
+import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
+import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
+
+import { storage } from '#imports';
+
+const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
+const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
+const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
+const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
+
+type NavigationHistoryEntry = {
+ pathname: string;
+ search: string;
+ hash: string;
+};
+
+/**
+ * Initialize component that handles initial application setup, authentication checks,
+ * vault synchronization, and state restoration.
+ */
+const Reinitialize: React.FC = () => {
+ const navigate = useNavigate();
+ const { setIsInitialLoading } = useLoading();
+ const { syncVault } = useVaultSync();
+ const hasInitialized = useRef(false);
+
+ // Auth and DB state
+ const { isInitialized: authInitialized, isLoggedIn } = useAuth();
+ const { dbInitialized, dbAvailable, upgradeRequired } = useDb();
+
+ // Derived state
+ const isFullyInitialized = authInitialized && dbInitialized;
+ const requiresAuth = isFullyInitialized && (!isLoggedIn || (!dbAvailable && !upgradeRequired));
+ const requiresUpgrade = isFullyInitialized && isLoggedIn && upgradeRequired;
+
+ /**
+ * Restore the last visited page and navigation history if it was visited within the memory duration.
+ */
+ const restoreLastPage = useCallback(async (): Promise => {
+ const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
+ storage.getItem(LAST_VISITED_PAGE_KEY) as Promise,
+ storage.getItem(LAST_VISITED_TIME_KEY) as Promise,
+ storage.getItem(NAVIGATION_HISTORY_KEY) as Promise,
+ ]);
+
+ if (lastPage && lastVisitTime) {
+ const timeSinceLastVisit = Date.now() - lastVisitTime;
+ if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
+ // Restore the navigation history
+ if (savedHistory?.length) {
+ // First navigate to credentials page as the base
+ navigate('/credentials', { replace: true });
+
+ // Then restore the history stack
+ for (const entry of savedHistory) {
+ navigate(entry.pathname + entry.search + entry.hash);
+ }
+ return;
+ }
+
+ // Fallback to simple navigation if no history
+ navigate('/credentials', { replace: true });
+ navigate(lastPage, { replace: true });
+ return;
+ }
+ }
+
+ // Duration has expired, clear all stored navigation data
+ await Promise.all([
+ storage.removeItem(LAST_VISITED_PAGE_KEY),
+ storage.removeItem(LAST_VISITED_TIME_KEY),
+ storage.removeItem(NAVIGATION_HISTORY_KEY),
+ sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
+ ]);
+
+ // Navigate to the credentials page as default entry page
+ navigate('/credentials', { replace: true });
+ }, [navigate]);
+
+ useEffect(() => {
+ // Check for inline unlock mode
+ const urlParams = new URLSearchParams(window.location.search);
+ const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
+
+ if (isFullyInitialized) {
+ // Prevent multiple vault syncs (only run sync once)
+ const shouldRunSync = !hasInitialized.current;
+
+ if (requiresUpgrade) {
+ // Upgrade is required, navigate to upgrade page
+ navigate('/upgrade', { replace: true });
+ // Keep loading active until upgrade navigation completes
+ setTimeout(() => {
+ setIsInitialLoading(false);
+ }, 50);
+ } else if (requiresAuth) {
+ setIsInitialLoading(false);
+
+ // Determine which auth page to show
+ if (!isLoggedIn) {
+ navigate('/login', { replace: true });
+ } else if (!dbAvailable) {
+ navigate('/unlock', { replace: true });
+ }
+ } else if (inlineUnlock) {
+ setIsInitialLoading(false);
+ navigate('/unlock-success', { replace: true });
+ } else if (shouldRunSync) {
+ // Only perform vault sync once during initialization
+ hasInitialized.current = true;
+
+ // Perform vault sync and restore state
+ syncVault({
+ initialSync: true,
+ /**
+ * Handle successful vault sync.
+ */
+ onSuccess: async () => {
+ // After successful sync, try to restore last page or go to credentials
+ await restoreLastPage();
+ setIsInitialLoading(false);
+ },
+ /**
+ * Handle vault sync error.
+ * @param error Error message
+ */
+ onError: (error) => {
+ console.error('Vault sync error during initialization:', error);
+ // Even if sync fails, continue with initialization
+ restoreLastPage().then(() => {
+ setIsInitialLoading(false);
+ });
+ },
+ /**
+ * Handle upgrade required.
+ */
+ onUpgradeRequired: () => {
+ // This shouldn't happen anymore since we check in DbContext
+ navigate('/upgrade', { replace: true });
+ setIsInitialLoading(false);
+ }
+ });
+ } else {
+ // User is logged in and db is available, navigate to appropriate page
+ setIsInitialLoading(false);
+ restoreLastPage();
+ }
+ }
+ }, [isFullyInitialized, requiresAuth, requiresUpgrade, isLoggedIn, dbAvailable, upgradeRequired, navigate, setIsInitialLoading, syncVault, restoreLastPage]);
+
+ // This component doesn't render anything visible - it just handles initialization
+ return null;
+};
+
+export default Reinitialize;
diff --git a/apps/browser-extension/src/utils/AppInfo.ts b/apps/browser-extension/src/utils/AppInfo.ts
index 6581c8cad..6f09bddd7 100644
--- a/apps/browser-extension/src/utils/AppInfo.ts
+++ b/apps/browser-extension/src/utils/AppInfo.ts
@@ -14,7 +14,6 @@ export class AppInfo {
*/
public static readonly MIN_SERVER_VERSION = '0.12.0-dev';
-
/**
* The client name to use in the X-AliasVault-Client header.
* Detects the specific browser being used.
@@ -57,7 +56,6 @@ export class AppInfo {
*/
private constructor() {}
-
/**
* Checks if a given server version is supported
* @param serverVersion The version to check
diff --git a/apps/browser-extension/src/utils/WebApiService.ts b/apps/browser-extension/src/utils/WebApiService.ts
index 5f1b9f39a..b87a00af5 100644
--- a/apps/browser-extension/src/utils/WebApiService.ts
+++ b/apps/browser-extension/src/utils/WebApiService.ts
@@ -33,7 +33,6 @@ export class WebApiService {
return apiUrl.replace(/\/$/, '') + '/v1/';
}
-
/**
* Check if the current server is self-hosted.
*/
diff --git a/apps/mobile-app/app/index.tsx b/apps/mobile-app/app/index.tsx
index 0238fa4ec..adab62aad 100644
--- a/apps/mobile-app/app/index.tsx
+++ b/apps/mobile-app/app/index.tsx
@@ -5,5 +5,5 @@ import { Redirect } from 'expo-router';
* redirect to the login screen if the user is not logged in or to the main tabs screen if the user is logged in.
*/
export default function AppIndex() : React.ReactNode {
- return
+ return
}
\ No newline at end of file