/**
* 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'}
`;
}
/**
* 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
${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 `
${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