diff --git a/crypto_vault_extension/background.js b/crypto_vault_extension/background.js index 6796f38..a891c44 100644 --- a/crypto_vault_extension/background.js +++ b/crypto_vault_extension/background.js @@ -29,7 +29,21 @@ function startSessionTimeout() { if (vault && currentSession) { // Lock the session vault.lock_session(); + + // Keep the session info for SigSocket connection but mark it as timed out + const keyspace = currentSession.keyspace; await sessionManager.clear(); + + // Maintain SigSocket connection for the locked keyspace to receive pending requests + if (sigSocketService && keyspace) { + try { + // Keep SigSocket connected to receive requests even when locked + console.log(`๐Ÿ”’ Session timed out but maintaining SigSocket connection for: ${keyspace}`); + } catch (error) { + console.warn('Failed to maintain SigSocket connection after timeout:', error); + } + } + // Notify popup if it's open if (popupPort) { popupPort.postMessage({ @@ -130,12 +144,48 @@ async function restoreSession() { if (isUnlocked) { // Restart keep-alive for restored session startKeepAlive(); + + // Connect to SigSocket for the restored session + if (sigSocketService) { + try { + const connected = await sigSocketService.connectToServer(session.keyspace); + if (connected) { + console.log(`๐Ÿ”— SigSocket reconnected for restored workspace: ${session.keyspace}`); + } + } catch (error) { + // Don't show as warning if it's just "no workspace" - this is expected on fresh start + if (error.message && error.message.includes('Workspace not found')) { + console.log(`โ„น๏ธ SigSocket connection skipped for restored session: No workspace available yet`); + } else { + console.warn('Failed to reconnect SigSocket for restored session:', error); + } + } + } + return session; } else { - await sessionManager.clear(); + // Session exists but is locked - still try to connect SigSocket to receive pending requests + if (sigSocketService && session.keyspace) { + try { + const connected = await sigSocketService.connectToServer(session.keyspace); + if (connected) { + console.log(`๐Ÿ”— SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`); + } + } catch (error) { + // Don't show as warning if it's just "no workspace" - this is expected on fresh start + if (error.message && error.message.includes('Workspace not found')) { + console.log(`โ„น๏ธ SigSocket connection skipped for locked session: No workspace available yet`); + } else { + console.warn('Failed to connect SigSocket for locked session:', error); + } + } + } + + // Don't clear the session - keep it for SigSocket connection + // await sessionManager.clear(); } } - return null; + return session; // Return session even if locked, so we know which keyspace to use } // Import WASM module functions and SigSocket service @@ -187,14 +237,31 @@ const messageHandlers = { // Smart auto-connect to SigSocket when session is initialized if (sigSocketService) { try { + console.log(`๐Ÿ”— Initializing SigSocket connection for workspace: ${request.keyspace}`); + // This will reuse existing connection if same workspace, or switch if different const connected = await sigSocketService.connectToServer(request.keyspace); if (connected) { - console.log(`๐Ÿ”— SigSocket ready for workspace: ${request.keyspace}`); + console.log(`โœ… SigSocket ready for workspace: ${request.keyspace}`); + } else { + console.warn(`โš ๏ธ SigSocket connection failed for workspace: ${request.keyspace}`); } } catch (error) { console.warn('Failed to auto-connect to SigSocket:', error); + + // If connection fails, try once more after a short delay + setTimeout(async () => { + try { + console.log(`๐Ÿ”„ Retrying SigSocket connection for workspace: ${request.keyspace}`); + await sigSocketService.connectToServer(request.keyspace); + } catch (retryError) { + console.warn('SigSocket retry connection also failed:', retryError); + } + }, 2000); } + + // Notify SigSocket service that keyspace is now unlocked + await sigSocketService.onKeypaceUnlocked(); } return { success: true }; @@ -288,6 +355,19 @@ const messageHandlers = { return { success: true }; }, + updateSigSocketUrl: async (request) => { + if (sigSocketService) { + // Update the server URL in the SigSocket service + sigSocketService.defaultServerUrl = request.serverUrl; + + // Save to storage (already done in popup, but ensure consistency) + await chrome.storage.local.set({ sigSocketUrl: request.serverUrl }); + + console.log(`๐Ÿ”— SigSocket server URL updated to: ${request.serverUrl}`); + } + return { success: true }; + }, + // SigSocket handlers connectSigSocket: async (request) => { if (!sigSocketService) { @@ -313,6 +393,15 @@ const messageHandlers = { return { success: true, status }; }, + getSigSocketStatusWithTest: async () => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + // Use the enhanced connection testing method + const status = await sigSocketService.getStatusWithConnectionTest(); + return { success: true, status }; + }, + getPendingSignRequests: async () => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; @@ -393,6 +482,25 @@ chrome.runtime.onConnect.addListener((port) => { startKeepAlive(); } + // Handle messages from popup + port.onMessage.addListener(async (message) => { + if (message.type === 'REQUEST_IMMEDIATE_STATUS') { + // Immediately send current SigSocket status to popup + if (sigSocketService) { + try { + const status = await sigSocketService.getStatus(); + port.postMessage({ + type: 'CONNECTION_STATUS_CHANGED', + status: status + }); + console.log('๐Ÿ“ก Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected'); + } catch (error) { + console.warn('Failed to send immediate status:', error); + } + } + } + }); + port.onDisconnect.addListener(() => { // Popup closed, clear reference and stop keep-alive popupPort = null; @@ -404,4 +512,159 @@ chrome.runtime.onConnect.addListener((port) => { } }); } -}); \ No newline at end of file +}); + +// Handle notification clicks to open extension (notifications are now clickable without buttons) +chrome.notifications.onClicked.addListener(async (notificationId) => { + console.log(`๐Ÿ”” Notification clicked: ${notificationId}`); + + // Check if this is a SigSocket notification + if (notificationId.startsWith('sigsocket-request-')) { + console.log('๐Ÿ”” SigSocket notification clicked, opening extension...'); + try { + await openExtensionPopup(); + // Clear the notification after successfully opening + chrome.notifications.clear(notificationId); + console.log('โœ… Notification cleared after opening extension'); + } catch (error) { + console.error('โŒ Failed to handle notification click:', error); + } + } else { + console.log('๐Ÿ”” Non-SigSocket notification clicked, ignoring'); + } +}); + +// Note: Notification button handler removed - notifications are now clickable without buttons + +// Function to open extension popup with best UX +async function openExtensionPopup() { + try { + console.log('๐Ÿ”” Opening extension popup from notification...'); + + // First, check if there's already a popup window open + const windows = await chrome.windows.getAll({ populate: true }); + const existingPopup = windows.find(window => + window.type === 'popup' && + window.tabs?.some(tab => tab.url?.includes('popup.html')) + ); + + if (existingPopup) { + // Focus existing popup and send focus message + await chrome.windows.update(existingPopup.id, { focused: true }); + console.log('โœ… Focused existing popup window'); + + // Send message to focus on SigSocket section + if (popupPort) { + popupPort.postMessage({ + type: 'FOCUS_SIGSOCKET', + fromNotification: true + }); + } + return; + } + + // Best UX: Try to use the normal popup experience + // The action API gives the same popup as clicking the extension icon + try { + if (chrome.action && chrome.action.openPopup) { + await chrome.action.openPopup(); + console.log('โœ… Extension popup opened via action API (best UX - normal popup)'); + + // Send focus message after popup opens + setTimeout(() => { + if (popupPort) { + popupPort.postMessage({ + type: 'FOCUS_SIGSOCKET', + fromNotification: true + }); + } + }, 200); + + return; + } + } catch (actionError) { + // The action API fails when there's no active browser window + // This is common when all browser windows are closed but extension is still running + console.log('โš ๏ธ Action API failed (likely no active window):', actionError.message); + + // Check if we have any normal browser windows + const allWindows = await chrome.windows.getAll(); + const normalWindows = allWindows.filter(w => w.type === 'normal'); + + if (normalWindows.length > 0) { + // We have browser windows, try to focus one and retry action API + try { + const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0]; + await chrome.windows.update(targetWindow.id, { focused: true }); + + // Small delay and retry + await new Promise(resolve => setTimeout(resolve, 100)); + await chrome.action.openPopup(); + console.log('โœ… Extension popup opened via action API after focusing window'); + + setTimeout(() => { + if (popupPort) { + popupPort.postMessage({ + type: 'FOCUS_SIGSOCKET', + fromNotification: true + }); + } + }, 200); + + return; + } catch (retryError) { + console.log('โš ๏ธ Action API retry also failed:', retryError.message); + } + } + } + + // If action API fails completely, we need to create a window + // But let's make it as close to the normal popup experience as possible + console.log('โš ๏ธ Creating popup window as fallback (action API unavailable)'); + + const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); + + // Position the popup where the extension icon would normally show its popup + // Try to position it in the top-right area like a normal extension popup + let left = screen.width - 420; // 400px width + 20px margin + let top = 80; // Below browser toolbar area + + try { + // If we have a browser window, position relative to it + const allWindows = await chrome.windows.getAll(); + const normalWindows = allWindows.filter(w => w.type === 'normal'); + + if (normalWindows.length > 0) { + const referenceWindow = normalWindows[0]; + left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420; + top = (referenceWindow.top || 0) + 80; + } + } catch (positionError) { + console.log('โš ๏ธ Could not get window position, using screen-based positioning'); + } + + const newWindow = await chrome.windows.create({ + url: popupUrl, + type: 'popup', + width: 400, + height: 600, + left: Math.max(0, left), + top: Math.max(0, top), + focused: true + }); + + console.log(`โœ… Extension popup window created: ${newWindow.id}`); + + } catch (error) { + console.error('โŒ Failed to open extension popup:', error); + + // Final fallback: open in new tab (least ideal but still functional) + try { + const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); + await chrome.tabs.create({ url: popupUrl, active: true }); + console.log('โœ… Opened extension in new tab as final fallback'); + } catch (tabError) { + console.error('โŒ All popup opening methods failed:', tabError); + } + } +} \ No newline at end of file diff --git a/crypto_vault_extension/background/sigsocket.js b/crypto_vault_extension/background/sigsocket.js index b2ec336..13abe14 100644 --- a/crypto_vault_extension/background/sigsocket.js +++ b/crypto_vault_extension/background/sigsocket.js @@ -25,6 +25,10 @@ class SigSocketService { // UI communication this.popupPort = null; + + // Status monitoring + this.statusMonitorInterval = null; + this.lastKnownConnectionState = false; } /** @@ -44,72 +48,221 @@ class SigSocketService { console.warn('Failed to load SigSocket URL from storage:', error); } + // Restore any persisted pending requests + await this.restorePendingRequests(); + console.log('๐Ÿ”Œ SigSocket service initialized with WASM APIs'); } + /** + * Restore pending requests from persistent storage + * Only restore requests that match the current workspace + */ + async restorePendingRequests() { + try { + const result = await chrome.storage.local.get(['sigSocketPendingRequests']); + if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) { + console.log(`๐Ÿ”„ Found ${result.sigSocketPendingRequests.length} stored requests`); + + // Filter requests for current workspace only + const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request => + request.target_public_key === this.connectedPublicKey + ); + + console.log(`๐Ÿ”„ Restoring ${currentWorkspaceRequests.length} requests for current workspace`); + + // Add each workspace-specific request back to WASM storage + for (const request of currentWorkspaceRequests) { + try { + await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request)); + console.log(`โœ… Restored request: ${request.id || request.request?.id}`); + } catch (error) { + console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error); + } + } + + // Update badge after restoration + this.updateBadge(); + } + } catch (error) { + console.warn('Failed to restore pending requests:', error); + } + } + + /** + * Persist pending requests to storage with workspace isolation + */ + async persistPendingRequests() { + try { + const requests = await this.getFilteredRequests(); + + // Get existing storage to merge with other workspaces + const result = await chrome.storage.local.get(['sigSocketPendingRequests']); + const existingRequests = result.sigSocketPendingRequests || []; + + // Remove old requests for current workspace + const otherWorkspaceRequests = existingRequests.filter(request => + request.target_public_key !== this.connectedPublicKey + ); + + // Combine with current workspace requests + const allRequests = [...otherWorkspaceRequests, ...requests]; + + await chrome.storage.local.set({ sigSocketPendingRequests: allRequests }); + console.log(`๐Ÿ’พ Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`); + } catch (error) { + console.warn('Failed to persist pending requests:', error); + } + } + /** * Connect to SigSocket server using WASM APIs * WASM handles all connection logic (reuse, switching, etc.) * @param {string} workspaceId - The workspace/keyspace identifier + * @param {number} retryCount - Number of retry attempts (default: 3) * @returns {Promise} - True if connected successfully */ - async connectToServer(workspaceId) { - try { - if (!this.wasmModule?.SigSocketManager) { - throw new Error('WASM SigSocketManager not available'); + async connectToServer(workspaceId, retryCount = 3) { + for (let attempt = 1; attempt <= retryCount; attempt++) { + try { + if (!this.wasmModule?.SigSocketManager) { + throw new Error('WASM SigSocketManager not available'); + } + + console.log(`๐Ÿ”— Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`); + + // Clean workspace switching + if (this.currentWorkspace && this.currentWorkspace !== workspaceId) { + console.log(`๐Ÿ”„ Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`); + await this.cleanWorkspaceSwitch(workspaceId); + // Small delay to ensure clean state transition + await new Promise(resolve => setTimeout(resolve, 300)); + } + + // Let WASM handle all connection logic (reuse, switching, etc.) + const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events( + workspaceId, + this.defaultServerUrl, + (event) => this.handleSigSocketEvent(event) + ); + + // Parse connection info + const info = JSON.parse(connectionInfo); + this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response + this.connectedPublicKey = info.public_key; + this.isConnected = info.is_connected; + + console.log(`โœ… SigSocket connection result:`, { + workspace: this.currentWorkspace, + publicKey: this.connectedPublicKey?.substring(0, 16) + '...', + connected: this.isConnected, + serverUrl: this.defaultServerUrl + }); + + // Validate that we have a public key if connected + if (this.isConnected && !this.connectedPublicKey) { + console.warn('โš ๏ธ Connected but no public key received - this may cause request issues'); + } + + // Update badge to show current state + this.updateBadge(); + + if (this.isConnected) { + // Clean flow: Connect -> Restore workspace requests -> Update UI + console.log(`๐Ÿ”— Connected to workspace: ${workspaceId}, restoring pending requests...`); + + // 1. Restore requests for this specific workspace + await this.restorePendingRequests(); + + // 2. Update badge with current count + this.updateBadge(); + + console.log(`โœ… Workspace ${workspaceId} ready with restored requests`); + return true; + } + + // If not connected but no error, try again + if (attempt < retryCount) { + console.log(`โณ Connection not established, retrying in 1 second...`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + } catch (error) { + // Check if this is an expected "no workspace" error during startup + const isExpectedStartupError = error.message && + (error.message.includes('Workspace not found') || + error.message.includes('no keypairs available')); + + if (isExpectedStartupError && attempt === 1) { + console.log(`โณ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`); + } + + // Check if this is a public key related error + if (error.message && error.message.includes('public key')) { + console.error(`๐Ÿ”‘ Public key error detected: ${error.message}`); + // For public key errors, don't retry immediately - might need workspace change + if (attempt === 1) { + console.log(`๐Ÿ”„ Public key error on first attempt, trying to disconnect and reconnect...`); + await this.disconnect(); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + if (attempt < retryCount) { + if (!isExpectedStartupError) { + console.log(`โณ Retrying connection in 2 seconds...`); + } + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + // Final attempt failed + this.isConnected = false; + this.currentWorkspace = null; + this.connectedPublicKey = null; + + if (isExpectedStartupError) { + console.log(`โ„น๏ธ SigSocket connection failed: No active workspace. Will connect when user logs in.`); + } + } } - - console.log(`๐Ÿ”— Requesting SigSocket connection for workspace: ${workspaceId}`); - - // Let WASM handle all connection logic (reuse, switching, etc.) - const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events( - workspaceId, - this.defaultServerUrl, - (event) => this.handleSigSocketEvent(event) - ); - - // Parse connection info - const info = JSON.parse(connectionInfo); - this.currentWorkspace = info.workspace; - this.connectedPublicKey = info.public_key; - this.isConnected = info.is_connected; - - console.log(`โœ… SigSocket connection result:`, { - workspace: this.currentWorkspace, - publicKey: this.connectedPublicKey?.substring(0, 16) + '...', - connected: this.isConnected - }); - - // Update badge to show current state - this.updateBadge(); - - return this.isConnected; - - } catch (error) { - console.error('โŒ SigSocket connection failed:', error); - this.isConnected = false; - this.currentWorkspace = null; - this.connectedPublicKey = null; - return false; } + + return false; } + + /** * Handle events from the WASM SigSocket client * This is called automatically when requests arrive * @param {Object} event - Event from WASM layer */ - handleSigSocketEvent(event) { + async handleSigSocketEvent(event) { console.log('๐Ÿ“จ Received SigSocket event:', event); if (event.type === 'sign_request') { - console.log(`๐Ÿ” New sign request: ${event.request_id}`); + console.log(`๐Ÿ” New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`); - // The request is automatically stored by WASM - // We just handle UI updates - this.showSignRequestNotification(); - this.updateBadge(); - this.notifyPopupOfNewRequest(); + // Clean flow: Request arrives -> Store -> Persist -> Update UI + try { + // 1. Request is automatically stored in WASM (already done by WASM layer) + + // 2. Persist to storage with workspace isolation + await this.persistPendingRequests(); + + // 3. Update badge count + this.updateBadge(); + + // 4. Show notification + this.showSignRequestNotification(); + + // 5. Notify popup if connected + this.notifyPopupOfNewRequest(); + + console.log(`โœ… Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`); + + } catch (error) { + console.error(`โŒ Failed to process request ${event.request_id}:`, error); + } } } @@ -124,6 +277,19 @@ class SigSocketService { throw new Error('WASM SigSocketManager not available'); } + // Check if we're connected before attempting approval + if (!this.isConnected) { + console.warn(`โš ๏ธ Not connected to SigSocket server, cannot approve request: ${requestId}`); + throw new Error('Not connected to SigSocket server'); + } + + // Verify we can approve this request + const canApprove = await this.canApproveRequest(requestId); + if (!canApprove) { + console.warn(`โš ๏ธ Cannot approve request ${requestId} - keyspace may be locked or request not found`); + throw new Error('Cannot approve request - keyspace may be locked or request not found'); + } + console.log(`โœ… Approving request: ${requestId}`); // WASM handles all validation, signing, and server communication @@ -131,14 +297,37 @@ class SigSocketService { console.log(`๐ŸŽ‰ Request approved successfully: ${requestId}`); - // Update UI + // Clean flow: Approve -> Remove from storage -> Update UI + // 1. Remove from persistent storage (WASM already removed it) + await this.persistPendingRequests(); + + // 2. Update badge count this.updateBadge(); + + // 3. Notify popup of updated state this.notifyPopupOfRequestUpdate(); + console.log(`โœ… Request ${requestId} approved and cleaned up`); return true; } catch (error) { console.error(`โŒ Failed to approve request ${requestId}:`, error); + + // Check if this is a connection-related error + if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) { + console.error(`๐Ÿ”‘ Connection/public key error during approval. Current state:`, { + connected: this.isConnected, + workspace: this.currentWorkspace, + publicKey: this.connectedPublicKey?.substring(0, 16) + '...' + }); + + // Try to reconnect for next time + if (this.currentWorkspace) { + console.log(`๐Ÿ”„ Attempting to reconnect to workspace: ${this.currentWorkspace}`); + setTimeout(() => this.connectToServer(this.currentWorkspace), 1000); + } + } + return false; } } @@ -162,10 +351,17 @@ class SigSocketService { console.log(`โœ… Request rejected successfully: ${requestId}`); - // Update UI + // Clean flow: Reject -> Remove from storage -> Update UI + // 1. Remove from persistent storage (WASM already removed it) + await this.persistPendingRequests(); + + // 2. Update badge count this.updateBadge(); + + // 3. Notify popup of updated state this.notifyPopupOfRequestUpdate(); + console.log(`โœ… Request ${requestId} rejected and cleaned up`); return true; } catch (error) { @@ -224,18 +420,54 @@ class SigSocketService { } /** - * Show notification for new sign request + * Show clickable notification for new sign request + * Call this AFTER the request has been stored and persisted */ - showSignRequestNotification() { + async showSignRequestNotification() { try { if (chrome.notifications && chrome.notifications.create) { - chrome.notifications.create({ + // Small delay to ensure request is fully stored + await new Promise(resolve => setTimeout(resolve, 100)); + + console.log(`๐Ÿ“ข Preparing notification for new signature request`); + + // Check if keyspace is currently unlocked to customize message + let message = 'New signature request received. Click to review and approve.'; + let title = 'SigSocket Sign Request'; + + // Try to determine if keyspace is locked + try { + const requests = await this.getPendingRequests(); + const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; + if (!canApprove) { + message = 'New signature request received. Click to unlock keyspace and approve.'; + title = 'SigSocket Request'; + } + } catch (error) { + // If we can't check, use generic message + message = 'New signature request received. Click to open extension.'; + } + + // Create clickable notification with unique ID + const notificationId = `sigsocket-request-${Date.now()}`; + + const notificationOptions = { type: 'basic', iconUrl: 'icons/icon48.png', - title: 'SigSocket Sign Request', - message: 'New signature request received. Click to review.' + title: title, + message: message, + requireInteraction: true // Keep notification visible until user interacts + }; + + console.log(`๐Ÿ“ข Creating notification: ${notificationId}`, notificationOptions); + + chrome.notifications.create(notificationId, notificationOptions, (createdId) => { + if (chrome.runtime.lastError) { + console.error('โŒ Failed to create notification:', chrome.runtime.lastError); + } else { + console.log(`โœ… Notification created successfully: ${createdId}`); + } }); - console.log('๐Ÿ“ข Notification shown for sign request'); } else { console.log('๐Ÿ“ข Notifications not available, skipping notification'); } @@ -322,6 +554,10 @@ class SigSocketService { this.isConnected = false; this.currentWorkspace = null; this.connectedPublicKey = null; + this.lastKnownConnectionState = false; + + // Stop status monitoring + this.stopStatusMonitoring(); this.updateBadge(); @@ -333,7 +569,50 @@ class SigSocketService { } /** - * Get connection status from WASM + * Clear persisted pending requests from storage + */ + async clearPersistedRequests() { + try { + await chrome.storage.local.remove(['sigSocketPendingRequests']); + console.log('๐Ÿ—‘๏ธ Cleared persisted pending requests from storage'); + } catch (error) { + console.warn('Failed to clear persisted requests:', error); + } + } + + /** + * Clean workspace switch - clear current workspace requests only + */ + async cleanWorkspaceSwitch(newWorkspace) { + try { + console.log(`๐Ÿ”„ Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`); + + // 1. Persist current workspace requests before switching + if (this.currentWorkspace && this.isConnected) { + await this.persistPendingRequests(); + console.log(`๐Ÿ’พ Saved requests for workspace: ${this.currentWorkspace}`); + } + + // 2. Clear WASM state (will be restored for new workspace) + if (this.wasmModule?.SigSocketManager) { + await this.wasmModule.SigSocketManager.clear_pending_requests(); + console.log('๐Ÿงน Cleared WASM request state'); + } + + // 3. Reset local state + this.currentWorkspace = null; + this.connectedPublicKey = null; + this.isConnected = false; + + console.log('โœ… Workspace switch cleanup completed'); + + } catch (error) { + console.error('โŒ Failed to clean workspace switch:', error); + } + } + + /** + * Get connection status with real connection verification * @returns {Promise} - Connection status information */ async getStatus() { @@ -348,21 +627,63 @@ class SigSocketService { }; } - // Let WASM provide the authoritative status + // Get WASM status first const statusJson = await this.wasmModule.SigSocketManager.get_connection_status(); const status = JSON.parse(statusJson); - const requests = await this.getPendingRequests(); - return { - isConnected: status.is_connected, - workspace: status.workspace, - publicKey: status.public_key, + // Verify connection by trying to get requests (this will fail if not connected) + let actuallyConnected = false; + let requests = []; + + try { + requests = await this.getPendingRequests(); + // If we can get requests and WASM says connected, we're probably connected + actuallyConnected = status.is_connected && Array.isArray(requests); + } catch (error) { + // If getting requests fails, we're definitely not connected + console.warn('Connection verification failed:', error); + actuallyConnected = false; + } + + // Update our internal state + this.isConnected = actuallyConnected; + + if (status.connected_public_key && actuallyConnected) { + this.connectedPublicKey = status.connected_public_key; + } else { + this.connectedPublicKey = null; + } + + // If we're disconnected, clear our workspace + if (!actuallyConnected) { + this.currentWorkspace = null; + } + + const statusResult = { + isConnected: actuallyConnected, + workspace: this.currentWorkspace, + publicKey: status.connected_public_key, pendingRequestCount: requests.length, - serverUrl: this.defaultServerUrl + serverUrl: this.defaultServerUrl, + // Clean flow status indicators + cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key }; + console.log('๐Ÿ“Š Clean flow status:', { + connected: statusResult.isConnected, + workspace: statusResult.workspace, + requestCount: statusResult.pendingRequestCount, + flowReady: statusResult.cleanFlowReady + }); + + return statusResult; + } catch (error) { console.error('Failed to get status:', error); + // Clear state on error + this.isConnected = false; + this.currentWorkspace = null; + this.connectedPublicKey = null; return { isConnected: false, workspace: null, @@ -375,33 +696,178 @@ class SigSocketService { /** * Set the popup port for communication - * @param {chrome.runtime.Port} port - The popup port + * @param {chrome.runtime.Port|null} port - The popup port or null to disconnect */ setPopupPort(port) { this.popupPort = port; - console.log('๐Ÿ“ฑ Popup connected to SigSocket service'); + + if (port) { + console.log('๐Ÿ“ฑ Popup connected to SigSocket service'); + + // Immediately check connection status when popup opens + this.checkConnectionStatusNow(); + + // Start monitoring connection status when popup connects + this.startStatusMonitoring(); + } else { + console.log('๐Ÿ“ฑ Popup disconnected from SigSocket service'); + // Stop monitoring when popup disconnects + this.stopStatusMonitoring(); + } } /** - * Called when keyspace is unlocked - notify popup of current state + * Immediately check and update connection status + */ + async checkConnectionStatusNow() { + try { + // Force a fresh connection check + const currentStatus = await this.getStatusWithConnectionTest(); + this.lastKnownConnectionState = currentStatus.isConnected; + + // Notify popup of current status + this.notifyPopupOfStatusChange(currentStatus); + + console.log(`๐Ÿ” Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`); + } catch (error) { + console.warn('Failed to check connection status immediately:', error); + } + } + + /** + * Get status with additional connection testing + */ + async getStatusWithConnectionTest() { + const status = await this.getStatus(); + + // If WASM claims we're connected, do an additional verification + if (status.isConnected) { + try { + // Try to get connection status again - if this fails, we're not really connected + const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status(); + const verifyStatus = JSON.parse(verifyJson); + + if (!verifyStatus.is_connected) { + console.log('๐Ÿ” Connection verification failed - marking as disconnected'); + status.isConnected = false; + this.isConnected = false; + this.currentWorkspace = null; + } + } catch (error) { + console.log('๐Ÿ” Connection test failed - marking as disconnected:', error.message); + status.isConnected = false; + this.isConnected = false; + this.currentWorkspace = null; + } + } + + return status; + } + + /** + * Start periodic status monitoring to detect connection changes + */ + startStatusMonitoring() { + // Clear any existing monitoring + if (this.statusMonitorInterval) { + clearInterval(this.statusMonitorInterval); + } + + // Check status every 2 seconds when popup is open (more responsive) + this.statusMonitorInterval = setInterval(async () => { + if (this.popupPort) { + try { + const currentStatus = await this.getStatusWithConnectionTest(); + + // Check if connection status changed + if (currentStatus.isConnected !== this.lastKnownConnectionState) { + console.log(`๐Ÿ”„ Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`); + this.lastKnownConnectionState = currentStatus.isConnected; + + // Notify popup of status change + this.notifyPopupOfStatusChange(currentStatus); + } + } catch (error) { + console.warn('Status monitoring error:', error); + // On error, assume disconnected + if (this.lastKnownConnectionState !== false) { + console.log('๐Ÿ”„ Status monitoring error - marking as disconnected'); + this.lastKnownConnectionState = false; + this.notifyPopupOfStatusChange({ + isConnected: false, + workspace: null, + publicKey: null, + pendingRequestCount: 0, + serverUrl: this.defaultServerUrl + }); + } + } + } else { + // Stop monitoring when popup is closed + this.stopStatusMonitoring(); + } + }, 2000); // 2 seconds for better responsiveness + } + + /** + * Stop status monitoring + */ + stopStatusMonitoring() { + if (this.statusMonitorInterval) { + clearInterval(this.statusMonitorInterval); + this.statusMonitorInterval = null; + } + } + + /** + * Notify popup of connection status change + * @param {Object} status - Current connection status + */ + notifyPopupOfStatusChange(status) { + if (this.popupPort) { + this.popupPort.postMessage({ + type: 'CONNECTION_STATUS_CHANGED', + status: status + }); + console.log(`๐Ÿ“ก Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`); + } + } + + /** + * Called when keyspace is unlocked - clean approach to show pending requests */ async onKeypaceUnlocked() { - if (!this.popupPort) return; - try { + console.log('๐Ÿ”“ Keyspace unlocked - preparing to show pending requests'); + + // 1. Restore any persisted requests for this workspace + await this.restorePendingRequests(); + + // 2. Get current requests (includes restored + any new ones) const requests = await this.getPendingRequests(); - const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; - this.popupPort.postMessage({ - type: 'KEYSPACE_UNLOCKED', - canApprove, - pendingRequests: requests - }); + // 3. Check if we can approve requests (keyspace should be unlocked now) + const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true; - console.log(`๐Ÿ”“ Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`); + // 4. Update badge with current count + this.updateBadge(); + + // 5. Notify popup if connected + if (this.popupPort) { + this.popupPort.postMessage({ + type: 'KEYSPACE_UNLOCKED', + canApprove, + pendingRequests: requests + }); + } + + console.log(`๐Ÿ”“ Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`); + + return requests; } catch (error) { console.error('Failed to handle keyspace unlock:', error); + return []; } } } diff --git a/crypto_vault_extension/popup.html b/crypto_vault_extension/popup.html index 71c4784..1f6c077 100644 --- a/crypto_vault_extension/popup.html +++ b/crypto_vault_extension/popup.html @@ -7,28 +7,17 @@
- diff --git a/crypto_vault_extension/popup.js b/crypto_vault_extension/popup.js index dd8bb65..4f14892 100644 --- a/crypto_vault_extension/popup.js +++ b/crypto_vault_extension/popup.js @@ -32,6 +32,11 @@ function showToast(message, type = 'info') { // Enhanced loading states for buttons function setButtonLoading(button, loading = true) { + // Handle null/undefined button gracefully + if (!button) { + return; + } + if (loading) { button.dataset.originalText = button.textContent; button.classList.add('loading'); @@ -126,9 +131,18 @@ const elements = { // Header elements lockBtn: document.getElementById('lockBtn'), themeToggle: document.getElementById('themeToggle'), - settingsToggle: document.getElementById('settingsToggle'), - settingsDropdown: document.getElementById('settingsDropdown'), + settingsBtn: document.getElementById('settingsBtn'), + headerTitle: document.getElementById('headerTitle'), + + // Section elements + authSection: document.getElementById('authSection'), + vaultSection: document.getElementById('vaultSection'), + settingsSection: document.getElementById('settingsSection'), + + // Settings page elements timeoutInput: document.getElementById('timeoutInput'), + serverUrlInput: document.getElementById('serverUrlInput'), + saveServerUrlBtn: document.getElementById('saveServerUrlBtn'), // Keypair management elements toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), @@ -219,6 +233,53 @@ async function saveTimeoutSetting(timeout) { await sendMessage('updateTimeout', { timeout }); } +// Server URL settings +async function loadServerUrlSetting() { + try { + const result = await chrome.storage.local.get(['sigSocketUrl']); + const serverUrl = result.sigSocketUrl || 'ws://localhost:8080/ws'; + if (elements.serverUrlInput) { + elements.serverUrlInput.value = serverUrl; + } + } catch (error) { + console.warn('Failed to load server URL setting:', error); + } +} + +async function saveServerUrlSetting() { + try { + const serverUrl = elements.serverUrlInput?.value?.trim(); + if (!serverUrl) { + showToast('Please enter a valid server URL', 'error'); + return; + } + + // Basic URL validation + if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) { + showToast('Server URL must start with ws:// or wss://', 'error'); + return; + } + + // Save to storage + await chrome.storage.local.set({ sigSocketUrl: serverUrl }); + + // Notify background script to update server URL + const response = await sendMessage('updateSigSocketUrl', { serverUrl }); + + if (response?.success) { + showToast('Server URL saved successfully', 'success'); + + // Refresh connection status + await loadSigSocketState(); + } else { + showToast('Failed to update server URL', 'error'); + } + } catch (error) { + console.error('Failed to save server URL:', error); + showToast('Failed to save server URL', 'error'); + } +} + async function resetSessionTimeout() { if (currentKeyspace) { await sendMessage('resetTimeout'); @@ -241,11 +302,63 @@ function toggleTheme() { updateThemeIcon(newTheme); } -// Settings dropdown management -function toggleSettingsDropdown() { - const dropdown = elements.settingsDropdown; - if (dropdown) { - dropdown.classList.toggle('hidden'); +// Settings page navigation +async function showSettingsPage() { + // Hide all sections + document.querySelectorAll('.section').forEach(section => { + section.classList.add('hidden'); + }); + + // Show settings section + elements.settingsSection?.classList.remove('hidden'); + + // Ensure we have current status before updating settings display + await loadSigSocketState(); +} + +async function hideSettingsPage() { + // Hide settings section + elements.settingsSection?.classList.add('hidden'); + + // Check current session state to determine what to show + try { + const response = await sendMessage('getStatus'); + + if (response && response.success && response.status && response.session) { + // Active session exists - show vault section + currentKeyspace = response.session.keyspace; + if (elements.keyspaceInput) { + elements.keyspaceInput.value = currentKeyspace; + } + setStatus(currentKeyspace, true); + elements.vaultSection?.classList.remove('hidden'); + updateSettingsVisibility(); // Update settings visibility + + // Load vault content + await loadKeypairs(); + + // Use retry mechanism for existing sessions that might have stale connections + await loadSigSocketStateWithRetry(); + } else { + // No active session - show auth section + currentKeyspace = null; + setStatus('', false); + elements.authSection?.classList.remove('hidden'); + updateSettingsVisibility(); // Update settings visibility + + // For no session, use regular loading + await loadSigSocketState(); + } + } catch (error) { + console.warn('Failed to check session state:', error); + // Fallback to auth section on error + currentKeyspace = null; + setStatus('', false); + elements.authSection?.classList.remove('hidden'); + updateSettingsVisibility(); // Update settings visibility + + // Still try to load SigSocket state + await loadSigSocketState(); } } @@ -287,6 +400,19 @@ function updateThemeIcon(theme) { } } +// Update settings button visibility based on keyspace state +function updateSettingsVisibility() { + if (elements.settingsBtn) { + if (currentKeyspace) { + // Show settings when keyspace is unlocked + elements.settingsBtn.style.display = ''; + } else { + // Hide settings when keyspace is locked + elements.settingsBtn.style.display = 'none'; + } + } +} + // Establish connection to background script for keep-alive function connectToBackground() { backgroundPort = chrome.runtime.connect({ name: 'popup' }); @@ -299,6 +425,7 @@ function connectToBackground() { selectedKeypairId = null; setStatus('', false); showSection('authSection'); + updateSettingsVisibility(); // Update settings visibility clearVaultState(); // Clear form inputs @@ -313,6 +440,13 @@ function connectToBackground() { backgroundPort.onDisconnect.addListener(() => { backgroundPort = null; }); + + // Immediately request status update when popup connects + setTimeout(() => { + if (backgroundPort) { + backgroundPort.postMessage({ type: 'REQUEST_IMMEDIATE_STATUS' }); + } + }, 50); // Small delay to ensure connection is established } // Initialize @@ -323,6 +457,9 @@ document.addEventListener('DOMContentLoaded', async function() { // Load timeout setting await loadTimeoutSetting(); + // Load server URL setting + await loadServerUrlSetting(); + // Ensure lock button starts hidden const lockBtn = document.getElementById('lockBtn'); if (lockBtn) { @@ -338,7 +475,9 @@ document.addEventListener('DOMContentLoaded', async function() { loginBtn: login, lockBtn: lockSession, themeToggle: toggleTheme, - settingsToggle: toggleSettingsDropdown, + settingsBtn: showSettingsPage, + headerTitle: hideSettingsPage, + saveServerUrlBtn: saveServerUrlSetting, toggleAddKeypairBtn: toggleAddKeypairForm, addKeypairBtn: addKeypair, cancelAddKeypairBtn: hideAddKeypairForm, @@ -349,7 +488,10 @@ document.addEventListener('DOMContentLoaded', async function() { }; Object.entries(eventMap).forEach(([elementKey, handler]) => { - elements[elementKey]?.addEventListener('click', handler); + const element = elements[elementKey]; + if (element) { + element.addEventListener('click', handler); + } }); // Tab functionality @@ -400,10 +542,66 @@ document.addEventListener('DOMContentLoaded', async function() { } }); + // Initialize SigSocket UI elements after DOM is ready + sigSocketElements = { + connectionStatus: document.getElementById('connectionStatus'), + connectionDot: document.getElementById('connectionDot'), + connectionText: document.getElementById('connectionText'), + requestsContainer: document.getElementById('requestsContainer'), + loadingRequestsMessage: document.getElementById('loadingRequestsMessage'), + noRequestsMessage: document.getElementById('noRequestsMessage'), + requestsList: document.getElementById('requestsList'), + refreshRequestsBtn: document.getElementById('refreshRequestsBtn') + }; + + // Add SigSocket button listeners + sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests); + + // Check if opened via notification (focus on SigSocket section) + const urlParams = new URLSearchParams(window.location.search); + const fromNotification = urlParams.get('from') === 'notification'; + // Check for existing session await checkExistingSession(); + + // If opened from notification, focus on SigSocket section and show requests + if (fromNotification) { + console.log('๐Ÿ”” Opened from notification, focusing on SigSocket section'); + focusOnSigSocketSection(); + } + + // Try to load any cached SigSocket state immediately for better UX + await loadCachedSigSocketState(); }); +// Focus on SigSocket section when opened from notification +function focusOnSigSocketSection() { + try { + // Switch to SigSocket tab if not already active + const sigSocketTab = document.querySelector('[data-tab="sigsocket"]'); + if (sigSocketTab && !sigSocketTab.classList.contains('active')) { + sigSocketTab.click(); + } + + // Scroll to requests container + if (sigSocketElements.requestsContainer) { + sigSocketElements.requestsContainer.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + + // Show a helpful toast + showToast('New signature request received! Review pending requests below.', 'info'); + + // Refresh requests to ensure latest state + setTimeout(() => refreshSigSocketRequests(), 500); + + } catch (error) { + console.error('Failed to focus on SigSocket section:', error); + } +} + async function checkExistingSession() { try { const response = await sendMessage('getStatus'); @@ -413,15 +611,31 @@ async function checkExistingSession() { elements.keyspaceInput.value = currentKeyspace; setStatus(currentKeyspace, true); showSection('vaultSection'); + updateSettingsVisibility(); // Update settings visibility await loadKeypairs(); + + // Use retry mechanism for existing sessions to handle stale connections + await loadSigSocketStateWithRetry(); } else { // No active session + currentKeyspace = null; setStatus('', false); showSection('authSection'); + updateSettingsVisibility(); // Update settings visibility + + // For no session, use regular loading (no retry needed) + await loadSigSocketState(); } } catch (error) { setStatus('', false); showSection('authSection'); + + // Still try to load SigSocket state even on error + try { + await loadSigSocketState(); + } catch (sigSocketError) { + console.warn('Failed to load SigSocket state:', sigSocketError); + } } } @@ -641,9 +855,23 @@ async function login() { currentKeyspace = auth.keyspace; setStatus(auth.keyspace, true); showSection('vaultSection'); + updateSettingsVisibility(); // Update settings visibility clearVaultState(); await loadKeypairs(); + // Clean flow: Login -> Connect -> Restore -> Display + console.log('๐Ÿ”“ Login successful, applying clean flow...'); + + // 1. Wait for SigSocket to connect and restore requests + await loadSigSocketStateWithRetry(); + + // 2. Show loading state while fetching + showRequestsLoading(); + + // 3. Refresh requests to get the clean, restored state + await refreshSigSocketRequests(); + + console.log('โœ… Login clean flow completed'); return response; } else { throw new Error(getResponseError(response, 'login')); @@ -667,6 +895,7 @@ async function lockSession() { selectedKeypairId = null; setStatus('', false); showSection('authSection'); + updateSettingsVisibility(); // Update settings visibility // Clear all form inputs elements.keyspaceInput.value = ''; @@ -936,28 +1165,8 @@ const verifySignature = () => performCryptoOperation({ // SigSocket functionality let sigSocketRequests = []; let sigSocketStatus = { isConnected: false, workspace: null }; - -// Initialize SigSocket UI elements -const sigSocketElements = { - connectionStatus: document.getElementById('connectionStatus'), - connectionDot: document.getElementById('connectionDot'), - connectionText: document.getElementById('connectionText'), - requestsContainer: document.getElementById('requestsContainer'), - noRequestsMessage: document.getElementById('noRequestsMessage'), - requestsList: document.getElementById('requestsList'), - refreshRequestsBtn: document.getElementById('refreshRequestsBtn'), - sigSocketStatusBtn: document.getElementById('sigSocketStatusBtn') -}; - -// Add SigSocket event listeners -document.addEventListener('DOMContentLoaded', () => { - // Add SigSocket button listeners - sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests); - sigSocketElements.sigSocketStatusBtn?.addEventListener('click', showSigSocketStatus); - - // Load initial SigSocket state - loadSigSocketState(); -}); +let sigSocketElements = {}; // Will be initialized in DOMContentLoaded +let isInitialLoad = true; // Track if this is the first load // Listen for messages from background script about SigSocket events if (backgroundPort) { @@ -968,6 +1177,12 @@ if (backgroundPort) { updateRequestsList(message.pendingRequests); } else if (message.type === 'KEYSPACE_UNLOCKED') { handleKeypaceUnlocked(message); + } else if (message.type === 'CONNECTION_STATUS_CHANGED') { + handleConnectionStatusChanged(message); + } else if (message.type === 'FOCUS_SIGSOCKET') { + // Handle focus request from notification click + console.log('๐Ÿ”” Received focus request from notification'); + focusOnSigSocketSection(); } }); } @@ -975,19 +1190,117 @@ if (backgroundPort) { // Load SigSocket state when popup opens async function loadSigSocketState() { try { - // Get SigSocket status - const statusResponse = await sendMessage('getSigSocketStatus'); - if (statusResponse?.success) { - updateConnectionStatus(statusResponse.status); + console.log('๐Ÿ”„ Loading SigSocket state...'); + + // Show loading state for requests + showRequestsLoading(); + + // Show loading state for connection status on initial load + if (isInitialLoad) { + showConnectionLoading(); } - // Get pending requests + // Force a fresh connection status check with enhanced testing + const statusResponse = await sendMessage('getSigSocketStatusWithTest'); + if (statusResponse?.success) { + console.log('โœ… Got SigSocket status:', statusResponse.status); + updateConnectionStatus(statusResponse.status); + } else { + console.warn('Enhanced status check failed, trying fallback...'); + // Fallback to regular status check + const fallbackResponse = await sendMessage('getSigSocketStatus'); + if (fallbackResponse?.success) { + console.log('โœ… Got fallback SigSocket status:', fallbackResponse.status); + updateConnectionStatus(fallbackResponse.status); + } else { + // If both fail, show disconnected but don't show error on initial load + updateConnectionStatus({ + isConnected: false, + workspace: null, + publicKey: null, + pendingRequestCount: 0, + serverUrl: 'ws://localhost:8080/ws' + }); + } + } + + // Get pending requests - this now works even when keyspace is locked + console.log('๐Ÿ“‹ Fetching pending requests...'); const requestsResponse = await sendMessage('getPendingSignRequests'); if (requestsResponse?.success) { + console.log(`๐Ÿ“‹ Retrieved ${requestsResponse.requests?.length || 0} pending requests:`, requestsResponse.requests); updateRequestsList(requestsResponse.requests); + } else { + console.warn('Failed to get pending requests:', requestsResponse); + updateRequestsList([]); } + + // Mark initial load as complete + isInitialLoad = false; + } catch (error) { console.warn('Failed to load SigSocket state:', error); + + // Hide loading state and show error state + hideRequestsLoading(); + + // Set disconnected state on error (but don't show error toast on initial load) + updateConnectionStatus({ + isConnected: false, + workspace: null, + publicKey: null, + pendingRequestCount: 0, + serverUrl: 'ws://localhost:8080/ws' + }); + + // Still try to show any cached requests + updateRequestsList([]); + + // Mark initial load as complete + isInitialLoad = false; + } +} + +// Load cached SigSocket state for immediate display +async function loadCachedSigSocketState() { + try { + // Try to get any cached requests from storage for immediate display + const cachedData = await chrome.storage.local.get(['sigSocketPendingRequests']); + if (cachedData.sigSocketPendingRequests && Array.isArray(cachedData.sigSocketPendingRequests)) { + console.log('๐Ÿ“‹ Loading cached requests for immediate display'); + updateRequestsList(cachedData.sigSocketPendingRequests); + } + } catch (error) { + console.warn('Failed to load cached SigSocket state:', error); + } +} + +// Load SigSocket state with simple retry for session initialization timing +async function loadSigSocketStateWithRetry() { + // First try immediately (might already be connected) + await loadSigSocketState(); + + // If still showing disconnected after initial load, try again after a short delay + if (!sigSocketStatus.isConnected) { + console.log('๐Ÿ”„ Initial load showed disconnected, retrying after delay...'); + await new Promise(resolve => setTimeout(resolve, 500)); + await loadSigSocketState(); + } +} + +// Show loading state for connection status +function showConnectionLoading() { + if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { + sigSocketElements.connectionDot.classList.remove('connected'); + sigSocketElements.connectionDot.classList.add('loading'); + sigSocketElements.connectionText.textContent = 'Checking...'; + } +} + +// Hide loading state for connection status +function hideConnectionLoading() { + if (sigSocketElements.connectionDot) { + sigSocketElements.connectionDot.classList.remove('loading'); } } @@ -995,15 +1308,42 @@ async function loadSigSocketState() { function updateConnectionStatus(status) { sigSocketStatus = status; + // Hide loading state + hideConnectionLoading(); + if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { if (status.isConnected) { sigSocketElements.connectionDot.classList.add('connected'); - sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`; + sigSocketElements.connectionText.textContent = 'Connected'; } else { sigSocketElements.connectionDot.classList.remove('connected'); sigSocketElements.connectionText.textContent = 'Disconnected'; } } + + // Log connection details for debugging + console.log('๐Ÿ”— Connection status updated:', { + connected: status.isConnected, + workspace: status.workspace, + publicKey: status.publicKey?.substring(0, 16) + '...', + serverUrl: status.serverUrl + }); +} + +// Show loading state for requests +function showRequestsLoading() { + if (!sigSocketElements.requestsContainer) return; + + sigSocketElements.loadingRequestsMessage?.classList.remove('hidden'); + sigSocketElements.noRequestsMessage?.classList.add('hidden'); + sigSocketElements.requestsList?.classList.add('hidden'); +} + +// Hide loading state for requests +function hideRequestsLoading() { + if (!sigSocketElements.requestsContainer) return; + + sigSocketElements.loadingRequestsMessage?.classList.add('hidden'); } // Update requests list display @@ -1012,6 +1352,9 @@ function updateRequestsList(requests) { if (!sigSocketElements.requestsContainer) return; + // Hide loading state + hideRequestsLoading(); + if (sigSocketRequests.length === 0) { sigSocketElements.noRequestsMessage?.classList.remove('hidden'); sigSocketElements.requestsList?.classList.add('hidden'); @@ -1036,8 +1379,42 @@ function createRequestItem(request) { const shortId = request.id.substring(0, 8) + '...'; const decodedMessage = request.message ? atob(request.message) : 'No message'; + // Check if keyspace is currently unlocked + const isKeypaceUnlocked = currentKeyspace !== null; + + // Create different UI based on keyspace lock status + let actionsHtml; + let statusIndicator = ''; + + if (isKeypaceUnlocked) { + // Normal approve/reject buttons when unlocked + actionsHtml = ` +
+ + +
+ `; + } else { + // Show pending status and unlock message when locked + statusIndicator = '
โณ Pending - Unlock keyspace to approve/reject
'; + actionsHtml = ` +
+ + +
+ `; + } + return ` -
+
${shortId}
${requestTime}
@@ -1047,14 +1424,8 @@ function createRequestItem(request) { ${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage}
-
- - -
+ ${statusIndicator} + ${actionsHtml}
`; } @@ -1091,15 +1462,61 @@ function handleNewSignRequest(message) { } } -// Handle keyspace unlocked event +// Handle keyspace unlocked event - Clean flow implementation function handleKeypaceUnlocked(message) { - // Update requests list - if (message.pendingRequests) { - updateRequestsList(message.pendingRequests); - } + console.log('๐Ÿ”“ Keyspace unlocked - applying clean flow for request display'); - // Update button states based on whether requests can be approved - updateRequestButtonStates(message.canApprove); + // Clean flow: Unlock -> Show loading -> Display requests -> Update UI + try { + // 1. Show loading state immediately + showRequestsLoading(); + + // 2. Update requests list with restored + current requests + if (message.pendingRequests && Array.isArray(message.pendingRequests)) { + console.log(`๐Ÿ“‹ Displaying ${message.pendingRequests.length} restored requests`); + updateRequestsList(message.pendingRequests); + + // 3. Update button states (should be enabled now) + updateRequestButtonStates(message.canApprove !== false); + + // 4. Show appropriate notification + const count = message.pendingRequests.length; + if (count > 0) { + showToast(`Keyspace unlocked! ${count} pending request${count > 1 ? 's' : ''} ready for review.`, 'info'); + } else { + showToast('Keyspace unlocked! No pending requests.', 'success'); + } + } else { + // 5. If no requests in message, fetch fresh from server + console.log('๐Ÿ“‹ No requests in unlock message, fetching from server...'); + setTimeout(() => refreshSigSocketRequests(), 100); + } + + console.log('โœ… Keyspace unlock flow completed'); + + } catch (error) { + console.error('โŒ Error in keyspace unlock flow:', error); + hideRequestsLoading(); + showToast('Error loading requests after unlock', 'error'); + } +} + +// Handle connection status change event +function handleConnectionStatusChanged(message) { + console.log('๐Ÿ”„ Connection status changed:', message.status); + + // Store previous state for comparison + const previousState = sigSocketStatus ? sigSocketStatus.isConnected : null; + + // Update the connection status display + updateConnectionStatus(message.status); + + // Only show toast for actual changes, not initial status, and not during initial load + if (!isInitialLoad && previousState !== null && previousState !== message.status.isConnected) { + const statusText = message.status.isConnected ? 'Connected' : 'Disconnected'; + const toastType = message.status.isConnected ? 'success' : 'warning'; + showToast(`SigSocket ${statusText}`, toastType); + } } // Show workspace mismatch warning @@ -1133,30 +1550,46 @@ function updateRequestButtonStates(canApprove) { // Approve a sign request async function approveSignRequest(requestId) { + let button = null; try { - const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); + button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); setButtonLoading(button, true); const response = await sendMessage('approveSignRequest', { requestId }); if (response?.success) { showToast('Request approved and signed!', 'success'); + showRequestsLoading(); await refreshSigSocketRequests(); } else { - throw new Error(getResponseError(response, 'approve request')); + const errorMsg = getResponseError(response, 'approve request'); + + // Check for specific connection errors + if (errorMsg.includes('Connection not found') || errorMsg.includes('public key')) { + showToast('Connection error: Please check SigSocket connection and try again', 'error'); + // Trigger a connection status refresh + await loadSigSocketState(); + } else if (errorMsg.includes('keyspace') || errorMsg.includes('locked')) { + showToast('Keyspace is locked. Please unlock to approve requests.', 'error'); + } else { + throw new Error(errorMsg); + } } } catch (error) { + console.error('Error approving request:', error); showToast(`Failed to approve request: ${error.message}`, 'error'); } finally { - const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); - setButtonLoading(button, false); + // Re-query button in case DOM was updated during the operation + const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); + setButtonLoading(finalButton, false); } } // Reject a sign request async function rejectSignRequest(requestId) { + let button = null; try { - const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); + button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); setButtonLoading(button, true); const response = await sendMessage('rejectSignRequest', { @@ -1166,6 +1599,7 @@ async function rejectSignRequest(requestId) { if (response?.success) { showToast('Request rejected', 'info'); + showRequestsLoading(); await refreshSigSocketRequests(); } else { throw new Error(getResponseError(response, 'reject request')); @@ -1173,8 +1607,9 @@ async function rejectSignRequest(requestId) { } catch (error) { showToast(`Failed to reject request: ${error.message}`, 'error'); } finally { - const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); - setButtonLoading(button, false); + // Re-query button in case DOM was updated during the operation + const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); + setButtonLoading(finalButton, false); } } @@ -1182,42 +1617,35 @@ async function rejectSignRequest(requestId) { async function refreshSigSocketRequests() { try { setButtonLoading(sigSocketElements.refreshRequestsBtn, true); + showRequestsLoading(); + console.log('๐Ÿ”„ Refreshing SigSocket requests...'); const response = await sendMessage('getPendingSignRequests'); + if (response?.success) { + console.log(`๐Ÿ“‹ Retrieved ${response.requests?.length || 0} pending requests`); updateRequestsList(response.requests); - showToast('Requests refreshed', 'success'); + + const count = response.requests?.length || 0; + if (count > 0) { + showToast(`${count} pending request${count > 1 ? 's' : ''} found`, 'success'); + } else { + showToast('No pending requests', 'info'); + } } else { + console.error('Failed to get pending requests:', response); + hideRequestsLoading(); throw new Error(getResponseError(response, 'refresh requests')); } } catch (error) { + console.error('Error refreshing requests:', error); + hideRequestsLoading(); showToast(`Failed to refresh requests: ${error.message}`, 'error'); } finally { setButtonLoading(sigSocketElements.refreshRequestsBtn, false); } } -// Show SigSocket status -async function showSigSocketStatus() { - try { - const response = await sendMessage('getSigSocketStatus'); - if (response?.success) { - const status = response.status; - const statusText = ` -SigSocket Status: -โ€ข Connected: ${status.isConnected ? 'Yes' : 'No'} -โ€ข Workspace: ${status.workspace || 'None'} -โ€ข Public Key: ${status.publicKey ? status.publicKey.substring(0, 16) + '...' : 'None'} -โ€ข Pending Requests: ${status.pendingRequestCount || 0} -โ€ข Server URL: ${status.serverUrl} - `.trim(); - showToast(statusText, 'info'); - updateConnectionStatus(status); - } else { - throw new Error(getResponseError(response, 'get status')); - } - } catch (error) { - showToast(`Failed to get status: ${error.message}`, 'error'); - } -} \ No newline at end of file + + diff --git a/crypto_vault_extension/styles/popup.css b/crypto_vault_extension/styles/popup.css index bf242f6..88e3e2c 100644 --- a/crypto_vault_extension/styles/popup.css +++ b/crypto_vault_extension/styles/popup.css @@ -188,6 +188,15 @@ body { margin: 0; } +.clickable-header { + cursor: pointer; + transition: opacity 0.2s ease; +} + +.clickable-header:hover { + opacity: 0.8; +} + .header-actions { display: flex; align-items: center; @@ -261,6 +270,75 @@ body { color: var(--text-muted); } +.server-input-group { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.server-input-group input { + flex: 1; + padding: var(--spacing-xs) var(--spacing-sm); + font-size: 14px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-input); + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; +} + +.server-input-group input:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15); +} + +.settings-help { + display: block; + font-size: 12px; + color: var(--text-muted); + margin-top: var(--spacing-xs); + font-style: italic; +} + +/* Settings page styles */ + +.settings-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.settings-header h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + + + +.about-info { + text-align: left; +} + +.about-info p { + margin: 0 0 var(--spacing-xs) 0; + font-size: 14px; + color: var(--text-primary); +} + +.about-info strong { + font-weight: 600; +} + +.version-info { + font-size: 12px; + color: var(--text-muted); + font-style: italic; +} + .btn-icon-only { background: var(--bg-button-ghost); border: none; @@ -456,6 +534,17 @@ input::placeholder, textarea::placeholder { font-size: 12px; } +/* Button icon spacing */ +.btn svg { + margin-right: var(--spacing-xs); + flex-shrink: 0; +} + +.btn svg:last-child { + margin-right: 0; + margin-left: var(--spacing-xs); +} + .btn:disabled { opacity: 0.5; cursor: not-allowed; @@ -1073,18 +1162,19 @@ input::placeholder, textarea::placeholder { /* SigSocket Requests Styles */ .sigsocket-section { - margin-bottom: 20px; + margin-bottom: var(--spacing-lg); } .section-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 15px; + margin-bottom: var(--spacing-lg); } .section-header h3 { margin: 0; + color: var(--text-primary); font-size: 16px; font-weight: 600; } @@ -1092,7 +1182,7 @@ input::placeholder, textarea::placeholder { .connection-status { display: flex; align-items: center; - gap: 6px; + gap: var(--spacing-xs); font-size: 12px; color: var(--text-secondary); } @@ -1109,16 +1199,62 @@ input::placeholder, textarea::placeholder { background: var(--accent-success); } +.status-dot.loading { + background: var(--accent-warning); + animation: pulse-dot 1.5s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.2); + } +} + .requests-container { min-height: 80px; } .empty-state { text-align: center; - padding: 20px; + padding: var(--spacing-xl); color: var(--text-secondary); } +.loading-state { + text-align: center; + padding: var(--spacing-xl); + color: var(--text-secondary); +} + +.loading-spinner { + width: 24px; + height: 24px; + border: 2px solid var(--border-color); + border-top: 2px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto var(--spacing-sm) auto; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-state p { + margin: var(--spacing-sm) 0; + font-weight: 500; +} + +.loading-state small { + opacity: 0.8; +} + .empty-icon { font-size: 24px; margin-bottom: 8px; @@ -1228,10 +1364,42 @@ input::placeholder, textarea::placeholder { .sigsocket-actions { display: flex; - gap: 8px; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--border-color); + gap: var(--spacing-sm); + margin-top: var(--spacing-md); +} + +/* Ensure refresh button follows design system */ +#refreshRequestsBtn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); +} + +/* Request item locked state styles */ +.request-item.locked { + opacity: 0.8; + border-left: 3px solid var(--warning-color, #ffa500); +} + +.request-status.pending { + background: var(--warning-bg, #fff3cd); + color: var(--warning-text, #856404); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: 4px; + font-size: 12px; + margin: var(--spacing-xs) 0; + border: 1px solid var(--warning-border, #ffeaa7); +} + +.request-actions.locked button { + opacity: 0.6; + cursor: not-allowed; +} + +.request-actions.locked button:hover { + background: var(--button-bg) !important; + transform: none !important; } .workspace-mismatch { diff --git a/crypto_vault_extension/wasm/wasm_app.js b/crypto_vault_extension/wasm/wasm_app.js index ce95c45..1e89461 100644 --- a/crypto_vault_extension/wasm/wasm_app.js +++ b/crypto_vault_extension/wasm/wasm_app.js @@ -467,31 +467,31 @@ export function run_rhai(script) { } function __wbg_adapter_34(arg0, arg1, arg2) { - wasm.closure174_externref_shim(arg0, arg1, arg2); + wasm.closure203_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_39(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1); } function __wbg_adapter_44(arg0, arg1, arg2) { - wasm.closure237_externref_shim(arg0, arg1, arg2); + wasm.closure239_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_49(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1); } function __wbg_adapter_52(arg0, arg1, arg2) { - wasm.closure308_externref_shim(arg0, arg1, arg2); + wasm.closure319_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_55(arg0, arg1, arg2) { - wasm.closure392_externref_shim(arg0, arg1, arg2); + wasm.closure395_externref_shim(arg0, arg1, arg2); } function __wbg_adapter_207(arg0, arg1, arg2, arg3) { - wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3); + wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3); } const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; @@ -1217,40 +1217,40 @@ function __wbg_get_imports() { const ret = false; return ret; }; - imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52); + imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52); return ret; }; - imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55); + imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55); return ret; }; - imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); return ret; }; - imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); return ret; }; - imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39); + imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39); return ret; }; - imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44); return ret; }; imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); + const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44); return ret; }; - imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); - return ret; - }; - imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49); + imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49); return ret; }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { diff --git a/crypto_vault_extension/wasm/wasm_app_bg.wasm b/crypto_vault_extension/wasm/wasm_app_bg.wasm index 2c5ffe3..dc06ea3 100644 Binary files a/crypto_vault_extension/wasm/wasm_app_bg.wasm and b/crypto_vault_extension/wasm/wasm_app_bg.wasm differ