sal-modular/crypto_vault_extension/popup/components/SignRequestManager.js
Sameh Abouel-saad 580fd72dce 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.
2025-06-04 16:55:15 +03:00

444 lines
15 KiB
JavaScript

/**
* 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;
}