feat: Implement Sign Request Manager component for handling sign requests in the popup (WIP)
- Added SignRequestManager.js to manage sign requests, including UI states for keyspace lock, mismatch, and approval. - Implemented methods for loading state, rendering UI, and handling user interactions (approve/reject requests). - Integrated background message listeners for keyspace unlock events and request updates.
This commit is contained in:
		
							
								
								
									
										443
									
								
								crypto_vault_extension/popup/components/SignRequestManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								crypto_vault_extension/popup/components/SignRequestManager.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,443 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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 = `
 | 
			
		||||
            <div class="sign-request-manager">
 | 
			
		||||
                <div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
 | 
			
		||||
                    <span class="status-indicator"></span>
 | 
			
		||||
                    SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="no-requests">
 | 
			
		||||
                    <p>No pending sign requests</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render unlock prompt
 | 
			
		||||
     */
 | 
			
		||||
    renderUnlockPrompt() {
 | 
			
		||||
        const requestCount = this.pendingRequests.length;
 | 
			
		||||
        this.container.innerHTML = `
 | 
			
		||||
            <div class="sign-request-manager">
 | 
			
		||||
                <div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
 | 
			
		||||
                    <span class="status-indicator"></span>
 | 
			
		||||
                    SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="unlock-prompt">
 | 
			
		||||
                    <h3>🔒 Unlock Keyspace</h3>
 | 
			
		||||
                    <p>Unlock your keyspace to see ${requestCount} pending sign request${requestCount !== 1 ? 's' : ''}.</p>
 | 
			
		||||
                    <p class="hint">Use the login form above to unlock your keyspace.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render keyspace mismatch message
 | 
			
		||||
     */
 | 
			
		||||
    renderKeypaceMismatch() {
 | 
			
		||||
        this.container.innerHTML = `
 | 
			
		||||
            <div class="sign-request-manager">
 | 
			
		||||
                <div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
 | 
			
		||||
                    <span class="status-indicator"></span>
 | 
			
		||||
                    SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="keyspace-mismatch">
 | 
			
		||||
                    <h3>⚠️ Wrong Keyspace</h3>
 | 
			
		||||
                    <p>The unlocked keyspace doesn't match the connected SigSocket session.</p>
 | 
			
		||||
                    <p class="hint">Please unlock the correct keyspace to approve sign requests.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render approval UI with pending requests
 | 
			
		||||
     */
 | 
			
		||||
    renderApprovalUI() {
 | 
			
		||||
        const requestsHtml = this.pendingRequests.map(request => this.renderSignRequestCard(request)).join('');
 | 
			
		||||
        
 | 
			
		||||
        this.container.innerHTML = `
 | 
			
		||||
            <div class="sign-request-manager">
 | 
			
		||||
                <div class="connection-status connected">
 | 
			
		||||
                    <span class="status-indicator"></span>
 | 
			
		||||
                    SigSocket: Connected
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="requests-header">
 | 
			
		||||
                    <h3>📝 Sign Requests (${this.pendingRequests.length})</h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="requests-list">
 | 
			
		||||
                    ${requestsHtml}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 `
 | 
			
		||||
            <div class="sign-request-card" data-request-id="${request.id}">
 | 
			
		||||
                <div class="request-header">
 | 
			
		||||
                    <div class="request-id">Request: ${request.id.substring(0, 8)}...</div>
 | 
			
		||||
                    <div class="request-time">${timestamp}</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="request-message">
 | 
			
		||||
                    <label>Message:</label>
 | 
			
		||||
                    <div class="message-content">
 | 
			
		||||
                        <div class="message-preview">${messagePreview}</div>
 | 
			
		||||
                        <button class="expand-message" data-request-id="${request.id}">
 | 
			
		||||
                            <span class="expand-text">Show Full</span>
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="request-actions">
 | 
			
		||||
                    <button class="btn-reject" data-request-id="${request.id}">
 | 
			
		||||
                        ❌ Reject
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button class="btn-approve" data-request-id="${request.id}">
 | 
			
		||||
                        ✅ Approve & Sign
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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<Object>} - 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;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user