- 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.
444 lines
15 KiB
JavaScript
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;
|
|
}
|