Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions packages/web/src/api/better-auth-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,34 @@ function createBetterAuthStore() {

// Listen for tab visibility changes to refresh session
if (typeof document !== 'undefined') {
const handleVisibilityChange = () => {
const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible' && !authLoading() && isOnline()) {
// Refresh session when tab becomes visible (user might have verified email in another tab)
session().refetch?.();
try {
await session().refetch?.();

// Wait a bit for session to update
await new Promise(resolve => setTimeout(resolve, 100));

// Validate and refresh project list if user is authenticated
const currentUser = user();
if (currentUser?.id) {
// Validate project list cache against current user
projectStore.validateProjectListCache(currentUser.id);

// Refresh project list to ensure it's current
try {
await projectStore.refreshProjectList(currentUser.id);
} catch (err) {
console.warn('[auth] Failed to refresh project list after visibility change:', err);
}
} else {
// User is not authenticated, clear project list
projectStore.clearProjectList();
}
} catch (err) {
console.warn('[auth] Failed to refresh session on visibility change:', err);
}
}
};

Expand Down Expand Up @@ -652,6 +676,20 @@ function createBetterAuthStore() {
}
}

/**
* Force refresh the auth session
* Used when page is restored from bfcache to ensure session is current
* Always refreshes, even if session is already loaded
*/
async function forceRefreshSession() {
try {
await session().refetch?.();
} catch (err) {
console.warn('Force session refresh failed:', err);
throw err;
}
}

async function deleteAccount() {
try {
setAuthError(null);
Expand Down Expand Up @@ -718,6 +756,7 @@ function createBetterAuthStore() {
// Utility/compatibility methods
getCurrentUser,
refreshAccessToken,
forceRefreshSession,
sendEmailVerification,
getPendingEmail,
getAccessToken,
Expand Down
85 changes: 85 additions & 0 deletions packages/web/src/lib/bfcache-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Back-Forward Cache (bfcache) Handler
*
* Detects when a page is restored from the browser's back-forward cache
* and triggers state refresh to ensure auth session and project data are current.
*/

import { useBetterAuth } from '@api/better-auth-store.js';
import projectStore from '@/stores/projectStore.js';

/**
* Initialize bfcache restoration handler
* Should be called once on app initialization
*/
export function initBfcacheHandler() {
if (typeof window === 'undefined') return;

// Get auth instance once (it's a singleton)
const auth = useBetterAuth();

const handlePageshow = async event => {
// event.persisted === true means the page was restored from bfcache
if (!event.persisted) return;

console.log('[bfcache] Page restored from back-forward cache, refreshing state...');

// Wait for auth to finish loading if it's currently loading
// This ensures we have the current user before validating project cache
if (auth.authLoading()) {
// Wait for auth to complete (with timeout)
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Auth loading timeout'));
}, 5000);

const checkAuth = () => {
if (!auth.authLoading()) {
clearTimeout(timeout);
resolve();
} else {
setTimeout(checkAuth, 100);
}
};

checkAuth();
}).catch(err => {
console.warn('[bfcache] Auth loading timeout:', err);
});
}

// Force refresh the auth session to ensure it's current
try {
await auth.forceRefreshSession();
} catch (err) {
console.warn('[bfcache] Failed to refresh auth session:', err);
}

// Wait a bit for session to update
await new Promise(resolve => setTimeout(resolve, 100));

// Validate and refresh project list if user is authenticated
const currentUser = auth.user();
if (currentUser?.id) {
// Validate project list cache against current user
projectStore.validateProjectListCache(currentUser.id);

// Refresh project list to ensure it's current
try {
await projectStore.refreshProjectList(currentUser.id);
} catch (err) {
console.warn('[bfcache] Failed to refresh project list:', err);
}
} else {
// User is not authenticated, clear project list
projectStore.clearProjectList();
}
};

window.addEventListener('pageshow', handlePageshow);

// Return cleanup function (though typically not needed for singleton)
return () => {
window.removeEventListener('pageshow', handlePageshow);
};
}
6 changes: 6 additions & 0 deletions packages/web/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { render } from 'solid-js/web';
import './global.css';
import Routes from './Routes.jsx';
import { cleanupExpiredStates } from '@lib/formStatePersistence.js';
import { initBfcacheHandler } from '@lib/bfcache-handler.js';
import AppErrorBoundary from './components/ErrorBoundary.jsx';

// Clean up any expired form state entries from IndexedDB on app load
cleanupExpiredStates().catch(() => {
// Silent fail - cleanup is best-effort
});

// Initialize bfcache restoration handler
// This detects when Safari (and other browsers) restore pages from bfcache
// and refreshes auth session and project list to ensure state is current
initBfcacheHandler();

function Root() {
return (
<AppErrorBoundary>
Expand Down
38 changes: 38 additions & 0 deletions packages/web/src/stores/projectStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,43 @@ function createProjectStore() {
return fetchProjectList(userId, { force: true });
}

/**
* Validate cached project list against current user ID
* Clears cache if user ID doesn't match
* @param {string} currentUserId - The current authenticated user's ID
*/
function validateProjectListCache(currentUserId) {
if (!currentUserId) {
// No user ID, clear the cache
setStore('projectList', {
items: [],
loaded: false,
loading: false,
error: null,
cachedUserId: null,
});
// Clear localStorage cache
saveCachedProjectList(null, null);
return;
}

const cachedUserId = store.projectList.cachedUserId;

// If cached user ID doesn't match current user, clear the cache
if (cachedUserId && cachedUserId !== currentUserId) {
console.log('[projectStore] Cached project list belongs to different user, clearing cache');
setStore('projectList', {
items: [],
loaded: false,
loading: false,
error: null,
cachedUserId: null,
});
// Clear localStorage cache
saveCachedProjectList(null, null);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Temporarily store pending project data during creation
* This avoids passing non-serializable data through router state
Expand Down Expand Up @@ -540,6 +577,7 @@ function createProjectStore() {
// Actions - Project List (Dashboard)
fetchProjectList,
refreshProjectList,
validateProjectListCache,
addProjectToList,
updateProjectInList,
removeProjectFromList,
Expand Down