diff --git a/assets/claude-usage-screenshot.jpg b/assets/claude-usage-screenshot.jpg index c84e577..d49cce2 100644 Binary files a/assets/claude-usage-screenshot.jpg and b/assets/claude-usage-screenshot.jpg differ diff --git a/main.js b/main.js index 40380a9..e0b06f1 100644 --- a/main.js +++ b/main.js @@ -9,6 +9,7 @@ const store = new Store({ let mainWindow = null; let loginWindow = null; +let silentLoginWindow = null; let tray = null; // Window configuration @@ -176,6 +177,149 @@ function createLoginWindow() { }); } +// Attempt silent login in a hidden browser window +async function attemptSilentLogin() { + console.log('[Main] Attempting silent login...'); + + // Notify renderer that we're trying to auto-login + if (mainWindow) { + mainWindow.webContents.send('silent-login-started'); + } + + return new Promise((resolve) => { + silentLoginWindow = new BrowserWindow({ + width: 800, + height: 700, + show: false, // Hidden window + webPreferences: { + nodeIntegration: false, + contextIsolation: true + } + }); + + silentLoginWindow.loadURL('https://claude.ai'); + + let loginCheckInterval = null; + let hasLoggedIn = false; + const SILENT_LOGIN_TIMEOUT = 15000; // 15 seconds timeout + + // Function to check login status + async function checkLoginStatus() { + if (hasLoggedIn || !silentLoginWindow) return; + + try { + const cookies = await session.defaultSession.cookies.get({ + url: 'https://claude.ai', + name: 'sessionKey' + }); + + if (cookies.length > 0) { + const sessionKey = cookies[0].value; + console.log('[Main] Silent login: Session key found, attempting to get org ID...'); + + // Fetch org ID from API + let orgId = null; + try { + const response = await axios.get('https://claude.ai/api/organizations', { + headers: { + 'Cookie': `sessionKey=${sessionKey}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + }); + + if (response.data && Array.isArray(response.data) && response.data.length > 0) { + orgId = response.data[0].uuid || response.data[0].id; + console.log('[Main] Silent login: Org ID fetched from API:', orgId); + } + } catch (err) { + console.log('[Main] Silent login: API not ready yet:', err.message); + } + + if (sessionKey && orgId) { + hasLoggedIn = true; + if (loginCheckInterval) { + clearInterval(loginCheckInterval); + loginCheckInterval = null; + } + + console.log('[Main] Silent login successful!'); + store.set('sessionKey', sessionKey); + store.set('organizationId', orgId); + + if (mainWindow) { + mainWindow.webContents.send('login-success', { sessionKey, organizationId: orgId }); + } + + silentLoginWindow.close(); + resolve(true); + } + } + } catch (error) { + console.error('[Main] Silent login check error:', error); + } + } + + // Check on page load + silentLoginWindow.webContents.on('did-finish-load', async () => { + const url = silentLoginWindow.webContents.getURL(); + console.log('[Main] Silent login page loaded:', url); + + if (url.includes('claude.ai')) { + await checkLoginStatus(); + } + }); + + // Also check on navigation + silentLoginWindow.webContents.on('did-navigate', async (event, url) => { + console.log('[Main] Silent login navigated to:', url); + if (url.includes('claude.ai')) { + await checkLoginStatus(); + } + }); + + // Poll periodically + loginCheckInterval = setInterval(async () => { + if (!hasLoggedIn && silentLoginWindow) { + await checkLoginStatus(); + } else if (loginCheckInterval) { + clearInterval(loginCheckInterval); + loginCheckInterval = null; + } + }, 1000); + + // Timeout - if silent login doesn't work, fall back to visible login + setTimeout(() => { + if (!hasLoggedIn) { + console.log('[Main] Silent login timeout, falling back to visible login...'); + if (loginCheckInterval) { + clearInterval(loginCheckInterval); + loginCheckInterval = null; + } + if (silentLoginWindow) { + silentLoginWindow.close(); + } + + // Notify renderer that silent login failed + if (mainWindow) { + mainWindow.webContents.send('silent-login-failed'); + } + + // Open visible login window + createLoginWindow(); + resolve(false); + } + }, SILENT_LOGIN_TIMEOUT); + + silentLoginWindow.on('closed', () => { + if (loginCheckInterval) { + clearInterval(loginCheckInterval); + loginCheckInterval = null; + } + silentLoginWindow = null; + }); + }); +} + function createTray() { try { tray = new Tray(path.join(__dirname, 'assets/tray-icon.png')); @@ -331,7 +475,18 @@ ipcMain.handle('fetch-usage-data', async () => { if (error.response) { console.error('[Main] Response status:', error.response.status); if (error.response.status === 401 || error.response.status === 403) { - throw new Error('Unauthorized'); + // Session expired - attempt silent re-login + console.log('[Main] Session expired, attempting silent re-login...'); + store.delete('sessionKey'); + store.delete('organizationId'); + + // Don't clear cookies - we need them for silent login to work with OAuth + // The silent login will use existing Google/OAuth session if available + + // Attempt silent login (will notify renderer appropriately) + attemptSilentLogin(); + + throw new Error('SessionExpired'); } } throw error; diff --git a/package.json b/package.json index f7c2040..1439796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-usage-widget", - "version": "1.0.0", + "version": "1.3.0", "description": "Desktop widget for Claude.ai usage monitoring", "main": "main.js", "scripts": { @@ -47,4 +47,4 @@ "!*.md" ] } -} +} \ No newline at end of file diff --git a/preload.js b/preload.js index cb54db6..63ac749 100644 --- a/preload.js +++ b/preload.js @@ -24,6 +24,15 @@ contextBridge.exposeInMainWorld('electronAPI', { onRefreshUsage: (callback) => { ipcRenderer.on('refresh-usage', () => callback()); }, + onSessionExpired: (callback) => { + ipcRenderer.on('session-expired', () => callback()); + }, + onSilentLoginStarted: (callback) => { + ipcRenderer.on('silent-login-started', () => callback()); + }, + onSilentLoginFailed: (callback) => { + ipcRenderer.on('silent-login-failed', () => callback()); + }, // API fetchUsageData: () => ipcRenderer.invoke('fetch-usage-data'), diff --git a/src/renderer/app.js b/src/renderer/app.js index 1dbcb8b..7c3c51e 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -10,6 +10,7 @@ const elements = { loadingContainer: document.getElementById('loadingContainer'), loginContainer: document.getElementById('loginContainer'), noUsageContainer: document.getElementById('noUsageContainer'), + autoLoginContainer: document.getElementById('autoLoginContainer'), mainContent: document.getElementById('mainContent'), loginBtn: document.getElementById('loginBtn'), refreshBtn: document.getElementById('refreshBtn'), @@ -103,6 +104,25 @@ function setupEventListeners() { window.electronAPI.onRefreshUsage(async () => { await fetchUsageData(); }); + + // Listen for session expiration events (403 errors) - only used as fallback + window.electronAPI.onSessionExpired(() => { + console.log('Session expired event received'); + credentials = { sessionKey: null, organizationId: null }; + showLoginRequired(); + }); + + // Listen for silent login attempts + window.electronAPI.onSilentLoginStarted(() => { + console.log('Silent login started...'); + showAutoLoginAttempt(); + }); + + // Listen for silent login failures (falls back to visible login) + window.electronAPI.onSilentLoginFailed(() => { + console.log('Silent login failed, manual login required'); + showLoginRequired(); + }); } // Fetch usage data from Claude API @@ -122,9 +142,11 @@ async function fetchUsageData() { updateUI(data); } catch (error) { console.error('Error fetching usage data:', error); - if (error.message.includes('Unauthorized')) { - await window.electronAPI.deleteCredentials(); - showLoginRequired(); + if (error.message.includes('SessionExpired') || error.message.includes('Unauthorized')) { + // Session expired - silent login attempt is in progress + // Show auto-login UI while waiting + credentials = { sessionKey: null, organizationId: null }; + showAutoLoginAttempt(); } else { showError('Failed to fetch usage data'); } @@ -139,7 +161,7 @@ function hasNoUsage(data) { const weeklyResetsAt = data.seven_day?.resets_at; return sessionUtilization === 0 && !sessionResetsAt && - weeklyUtilization === 0 && !weeklyResetsAt; + weeklyUtilization === 0 && !weeklyResetsAt; } // Update UI with usage data @@ -316,6 +338,7 @@ function showLoading() { elements.loadingContainer.style.display = 'block'; elements.loginContainer.style.display = 'none'; elements.noUsageContainer.style.display = 'none'; + elements.autoLoginContainer.style.display = 'none'; elements.mainContent.style.display = 'none'; } @@ -323,6 +346,7 @@ function showLoginRequired() { elements.loadingContainer.style.display = 'none'; elements.loginContainer.style.display = 'flex'; // Use flex to preserve centering elements.noUsageContainer.style.display = 'none'; + elements.autoLoginContainer.style.display = 'none'; elements.mainContent.style.display = 'none'; stopAutoUpdate(); } @@ -331,13 +355,24 @@ function showNoUsage() { elements.loadingContainer.style.display = 'none'; elements.loginContainer.style.display = 'none'; elements.noUsageContainer.style.display = 'flex'; + elements.autoLoginContainer.style.display = 'none'; elements.mainContent.style.display = 'none'; } +function showAutoLoginAttempt() { + elements.loadingContainer.style.display = 'none'; + elements.loginContainer.style.display = 'none'; + elements.noUsageContainer.style.display = 'none'; + elements.autoLoginContainer.style.display = 'flex'; + elements.mainContent.style.display = 'none'; + stopAutoUpdate(); +} + function showMainContent() { elements.loadingContainer.style.display = 'none'; elements.loginContainer.style.display = 'none'; elements.noUsageContainer.style.display = 'none'; + elements.autoLoginContainer.style.display = 'none'; elements.mainContent.style.display = 'block'; } diff --git a/src/renderer/index.html b/src/renderer/index.html index 35557f4..9c3dc38 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -69,6 +69,17 @@ + + +