/** * Sign Request Manager Component * * Handles the display and management of SigSocket sign requests in the popup. * Manages different UI states: * 1. Keyspace locked: Show unlock form * 2. Wrong keyspace: Show mismatch message * 3. Correct keyspace: Show approval UI */ class SignRequestManager { constructor() { this.pendingRequests = []; this.isKeypaceUnlocked = false; this.keypaceMatch = false; this.connectionStatus = { isConnected: false }; this.container = null; this.initialized = false; } /** * Initialize the component * @param {HTMLElement} container - Container element to render into */ async initialize(container) { this.container = container; this.initialized = true; // Load initial state await this.loadState(); // Render initial UI this.render(); // Set up event listeners this.setupEventListeners(); // Listen for background messages this.setupBackgroundListener(); } /** * Load current state from background script */ async loadState() { try { // Check if keyspace is unlocked const unlockedResponse = await this.sendMessage('isUnlocked'); this.isKeypaceUnlocked = unlockedResponse?.unlocked || false; // Get pending requests const requestsResponse = await this.sendMessage('getPendingRequests'); this.pendingRequests = requestsResponse?.requests || []; // Get SigSocket status const statusResponse = await this.sendMessage('getSigSocketStatus'); this.connectionStatus = statusResponse?.status || { isConnected: false }; // If keyspace is unlocked, notify background to check keyspace match if (this.isKeypaceUnlocked) { await this.sendMessage('keypaceUnlocked'); } } catch (error) { console.error('Failed to load sign request state:', error); } } /** * Render the component UI */ render() { if (!this.container) return; const hasRequests = this.pendingRequests.length > 0; if (!hasRequests) { this.renderNoRequests(); return; } if (!this.isKeypaceUnlocked) { this.renderUnlockPrompt(); return; } if (!this.keypaceMatch) { this.renderKeypaceMismatch(); return; } this.renderApprovalUI(); } /** * Render no requests state */ renderNoRequests() { this.container.innerHTML = `
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}

No pending sign requests

`; } /** * Render unlock prompt */ renderUnlockPrompt() { const requestCount = this.pendingRequests.length; this.container.innerHTML = `
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}

🔒 Unlock Keyspace

Unlock your keyspace to see ${requestCount} pending sign request${requestCount !== 1 ? 's' : ''}.

Use the login form above to unlock your keyspace.

`; } /** * Render keyspace mismatch message */ renderKeypaceMismatch() { this.container.innerHTML = `
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}

⚠️ Wrong Keyspace

The unlocked keyspace doesn't match the connected SigSocket session.

Please unlock the correct keyspace to approve sign requests.

`; } /** * Render approval UI with pending requests */ renderApprovalUI() { const requestsHtml = this.pendingRequests.map(request => this.renderSignRequestCard(request)).join(''); this.container.innerHTML = `
SigSocket: Connected

📝 Sign Requests (${this.pendingRequests.length})

${requestsHtml}
`; } /** * Render individual sign request card * @param {Object} request - Sign request data * @returns {string} - HTML string for the request card */ renderSignRequestCard(request) { const timestamp = new Date(request.timestamp).toLocaleTimeString(); const messagePreview = this.getMessagePreview(request.message); return `
Request: ${request.id.substring(0, 8)}...
${timestamp}
${messagePreview}
`; } /** * Get a preview of the message content * @param {string} messageBase64 - Base64 encoded message * @returns {string} - Preview text */ getMessagePreview(messageBase64) { try { const decoded = atob(messageBase64); const preview = decoded.length > 50 ? decoded.substring(0, 50) + '...' : decoded; return preview; } catch (error) { return `Base64: ${messageBase64.substring(0, 20)}...`; } } /** * Set up event listeners */ setupEventListeners() { if (!this.container) return; // Use event delegation for dynamic content this.container.addEventListener('click', (e) => { const target = e.target; if (target.classList.contains('btn-approve')) { const requestId = target.getAttribute('data-request-id'); this.approveRequest(requestId); } else if (target.classList.contains('btn-reject')) { const requestId = target.getAttribute('data-request-id'); this.rejectRequest(requestId); } else if (target.classList.contains('expand-message')) { const requestId = target.getAttribute('data-request-id'); this.toggleMessageExpansion(requestId); } }); } /** * Set up listener for background script messages */ setupBackgroundListener() { // Listen for keyspace unlock events chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'KEYSPACE_UNLOCKED') { this.isKeypaceUnlocked = true; this.keypaceMatch = message.keypaceMatches; this.pendingRequests = message.pendingRequests || []; this.render(); } }); } /** * Approve a sign request * @param {string} requestId - Request ID to approve */ async approveRequest(requestId) { try { const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`); if (button) { button.disabled = true; button.textContent = 'Signing...'; } const response = await this.sendMessage('approveSignRequest', { requestId }); if (response?.success) { // Remove the request from UI this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId); this.render(); // Show success message this.showToast('Sign request approved successfully!', 'success'); } else { throw new Error(response?.error || 'Failed to approve request'); } } catch (error) { console.error('Failed to approve request:', error); this.showToast('Failed to approve request: ' + error.message, 'error'); // Re-enable button const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`); if (button) { button.disabled = false; button.textContent = '✅ Approve & Sign'; } } } /** * Reject a sign request * @param {string} requestId - Request ID to reject */ async rejectRequest(requestId) { try { const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`); if (button) { button.disabled = true; button.textContent = 'Rejecting...'; } const response = await this.sendMessage('rejectSignRequest', { requestId, reason: 'User rejected' }); if (response?.success) { // Remove the request from UI this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId); this.render(); // Show success message this.showToast('Sign request rejected', 'info'); } else { throw new Error(response?.error || 'Failed to reject request'); } } catch (error) { console.error('Failed to reject request:', error); this.showToast('Failed to reject request: ' + error.message, 'error'); // Re-enable button const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`); if (button) { button.disabled = false; button.textContent = '❌ Reject'; } } } /** * Toggle message expansion * @param {string} requestId - Request ID */ toggleMessageExpansion(requestId) { const request = this.pendingRequests.find(r => r.id === requestId); if (!request) return; const card = this.container.querySelector(`[data-request-id="${requestId}"]`); const messageContent = card.querySelector('.message-content'); const expandButton = card.querySelector('.expand-message'); const isExpanded = messageContent.classList.contains('expanded'); if (isExpanded) { messageContent.classList.remove('expanded'); messageContent.querySelector('.message-preview').textContent = this.getMessagePreview(request.message); expandButton.querySelector('.expand-text').textContent = 'Show Full'; } else { messageContent.classList.add('expanded'); try { const fullMessage = atob(request.message); messageContent.querySelector('.message-preview').textContent = fullMessage; } catch (error) { messageContent.querySelector('.message-preview').textContent = `Base64: ${request.message}`; } expandButton.querySelector('.expand-text').textContent = 'Show Less'; } } /** * Send message to background script * @param {string} action - Action to perform * @param {Object} data - Additional data * @returns {Promise} - Response from background script */ async sendMessage(action, data = {}) { return new Promise((resolve) => { chrome.runtime.sendMessage({ action, ...data }, resolve); }); } /** * Show toast notification * @param {string} message - Message to show * @param {string} type - Toast type (success, error, info) */ showToast(message, type = 'info') { // Use the existing toast system from popup.js if (typeof showToast === 'function') { showToast(message, type); } else { console.log(`[${type.toUpperCase()}] ${message}`); } } /** * Update component state * @param {Object} newState - New state data */ updateState(newState) { console.log('SignRequestManager.updateState called with:', newState); console.log('Current state before update:', { isKeypaceUnlocked: this.isKeypaceUnlocked, keypaceMatch: this.keypaceMatch, pendingRequests: this.pendingRequests.length }); Object.assign(this, newState); // Fix the property name mismatch if (newState.keypaceMatches !== undefined) { this.keypaceMatch = newState.keypaceMatches; } console.log('State after update:', { isKeypaceUnlocked: this.isKeypaceUnlocked, keypaceMatch: this.keypaceMatch, pendingRequests: this.pendingRequests.length }); if (this.initialized) { console.log('Rendering SignRequestManager with new state'); this.render(); } } /** * Refresh component data */ async refresh() { await this.loadState(); this.render(); } } // Export for use in popup if (typeof module !== 'undefined' && module.exports) { module.exports = SignRequestManager; } else { window.SignRequestManager = SignRequestManager; }