feat(core): update session, security system and QR exchange

- Removed session creation and Lightning payment logic
- Refactored security system:
  * no more restrictions
  * all systems enabled on session creation
- Improved QR code exchange for mobile devices
This commit is contained in:
lockbitchat
2025-09-23 20:01:02 -04:00
parent 804b384271
commit 34094956b7
396 changed files with 126516 additions and 11881 deletions

312
src/components/QRScanner.js Normal file
View File

@@ -0,0 +1,312 @@
// Simple QR Scanner Component using only Html5Qrcode
const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
const videoRef = React.useRef(null);
const qrScannerRef = React.useRef(null);
const [error, setError] = React.useState(null);
const [isScanning, setIsScanning] = React.useState(false);
const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 });
React.useEffect(() => {
if (isVisible) {
startScanner();
} else {
stopScanner();
}
return () => {
stopScanner();
};
}, [isVisible]);
React.useEffect(() => {
const onProgress = (e) => {
const { id, seq, total } = e.detail || {};
if (!id || !total) return;
setProgress({ id, seq, total });
};
const onComplete = () => {
// Close scanner once app signals completion
if (!continuous) return;
try { stopScanner(); } catch {}
};
document.addEventListener('qr-scan-progress', onProgress, { passive: true });
document.addEventListener('qr-scan-complete', onComplete, { passive: true });
return () => {
document.removeEventListener('qr-scan-progress', onProgress, { passive: true });
document.removeEventListener('qr-scan-complete', onComplete, { passive: true });
};
}, []);
const startScanner = async () => {
try {
console.log('Starting QR scanner...');
setError(null);
setIsScanning(true);
// Allow camera on HTTP as well; rely on browser permission prompts
// Check if Html5Qrcode is available
if (!window.Html5Qrcode) {
setError('QR scanner library not loaded');
setIsScanning(false);
return;
}
// Get available cameras first
console.log('Getting available cameras...');
const cameras = await window.Html5Qrcode.getCameras();
console.log('Available cameras:', cameras);
if (!cameras || cameras.length === 0) {
setError('No cameras found on this device');
setIsScanning(false);
return;
}
// Clear any existing scanner
if (qrScannerRef.current) {
try {
qrScannerRef.current.stop();
} catch (e) {
console.log('Stopping previous scanner:', e.message);
}
}
// Create video element if it doesn't exist
if (!videoRef.current) {
console.log('Video element not found');
setError('Video element not found');
setIsScanning(false);
return;
}
console.log('Video element found:', videoRef.current);
console.log('Video element ID:', videoRef.current.id);
// Create Html5Qrcode instance
console.log('Creating Html5Qrcode instance...');
const html5Qrcode = new window.Html5Qrcode(videoRef.current.id || 'qr-reader');
// Find back camera (environment facing)
let cameraId = cameras[0].id; // Default to first camera
let selectedCamera = cameras[0];
// Look for back camera
for (const camera of cameras) {
if (camera.label.toLowerCase().includes('back') ||
camera.label.toLowerCase().includes('rear') ||
camera.label.toLowerCase().includes('environment')) {
cameraId = camera.id;
selectedCamera = camera;
break;
}
}
console.log('Available cameras:');
cameras.forEach((cam, index) => {
console.log(`${index + 1}. ${cam.label} (${cam.id})`);
});
console.log('Selected camera:', selectedCamera.label, 'ID:', cameraId);
// Start camera
console.log('Starting camera with Html5Qrcode...');
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
const qrboxSize = isDesktop ? 560 : 360;
await html5Qrcode.start(
cameraId, // Use specific camera ID
{
fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3,
qrbox: { width: qrboxSize, height: qrboxSize }
},
(decodedText, decodedResult) => {
console.log('QR Code detected:', decodedText);
try {
const res = onScan(decodedText);
const handleResult = (val) => {
const shouldClose = val === true || !continuous;
if (shouldClose) {
stopScanner();
}
};
if (res && typeof res.then === 'function') {
res.then(handleResult).catch((e) => {
console.warn('onScan async handler error:', e);
if (!continuous) stopScanner();
});
} else {
handleResult(res);
}
} catch (e) {
console.warn('onScan handler threw:', e);
if (!continuous) {
stopScanner();
}
}
},
(error) => {
// Ignore decode errors, they're normal during scanning
console.log('QR decode error:', error);
}
);
// Store scanner reference
qrScannerRef.current = html5Qrcode;
console.log('QR scanner started successfully');
} catch (err) {
console.error('Error starting QR scanner:', err);
let errorMessage = 'Failed to start camera';
if (err.name === 'NotAllowedError') {
errorMessage = 'Camera access denied. Please allow camera access and try again.';
} else if (err.name === 'NotFoundError') {
errorMessage = 'No camera found on this device.';
} else if (err.name === 'NotSupportedError') {
errorMessage = 'Camera not supported on this device.';
} else if (err.name === 'NotReadableError') {
errorMessage = 'Camera is already in use by another application.';
} else if (err.message) {
errorMessage = err.message;
}
setError(errorMessage);
setIsScanning(false);
}
};
const stopScanner = () => {
if (qrScannerRef.current) {
try {
qrScannerRef.current.stop().then(() => {
console.log('QR scanner stopped');
}).catch((err) => {
console.log('Error stopping scanner:', err);
});
} catch (err) {
console.log('Error stopping scanner:', err);
}
qrScannerRef.current = null;
}
setIsScanning(false);
try {
// iOS Safari workaround: small delay before closing modal to release camera
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
setTimeout(() => {
// no-op; allow camera to settle
}, 150);
}
} catch {}
};
const handleClose = () => {
stopScanner();
onClose();
};
if (!isVisible) {
return null;
}
return React.createElement('div', {
className: "fixed inset-0 bg-black/80 flex items-center justify-center z-50"
}, [
React.createElement('div', {
key: 'scanner-modal',
className: "bg-gray-800 rounded-lg p-6 w-full mx-4 max-w-2xl"
}, [
React.createElement('div', {
key: 'scanner-header',
className: "flex items-center justify-between mb-4"
}, [
React.createElement('h3', {
key: 'title',
className: "text-lg font-medium text-white"
}, 'Scan QR Code'),
React.createElement('button', {
key: 'close-btn',
onClick: handleClose,
className: "text-gray-400 hover:text-white transition-colors"
}, [
React.createElement('i', {
className: 'fas fa-times text-xl'
})
])
]),
React.createElement('div', {
key: 'scanner-content',
className: "relative"
}, [
React.createElement('div', {
key: 'video-container',
id: 'qr-reader',
ref: videoRef,
className: "w-full h-80 md:h-[32rem] bg-gray-700 rounded-lg"
}),
error && React.createElement('div', {
key: 'error',
className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
}, [
React.createElement('div', {
key: 'error-content',
className: "text-center text-white p-4"
}, [
React.createElement('i', {
key: 'error-icon',
className: 'fas fa-exclamation-triangle text-2xl mb-2'
}),
React.createElement('p', {
key: 'error-text',
className: "text-sm"
}, error)
])
]),
!error && !isScanning && React.createElement('div', {
key: 'loading',
className: "absolute inset-0 flex items-center justify-center bg-gray-700/50 rounded-lg"
}, [
React.createElement('div', {
key: 'loading-content',
className: "text-center text-white"
}, [
React.createElement('i', {
key: 'loading-icon',
className: 'fas fa-spinner fa-spin text-2xl mb-2'
}),
React.createElement('p', {
key: 'loading-text',
className: "text-sm"
}, 'Starting camera...')
])
]),
!error && isScanning && React.createElement('div', {
key: 'scanning-overlay',
className: "absolute inset-0 flex items-center justify-center"
}, [
React.createElement('div', {
key: 'scanning-content',
className: "text-center text-white bg-black/50 rounded-lg px-4 py-2"
}, [
React.createElement('i', {
key: 'scanning-icon',
className: 'fas fa-qrcode text-xl mb-1'
}),
React.createElement('p', {
key: 'scanning-text',
className: "text-xs"
}, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code')
])
]),
// Bottom overlay kept simple on mobile
]),
])
]);
};
// Export for use in other files
window.QRScanner = QRScanner;
console.log('QRScanner component loaded and available on window.QRScanner');

View File

@@ -0,0 +1 @@

View File

@@ -114,7 +114,7 @@ const EnhancedMinimalHeader = ({
const interval = setInterval(updateRealSecurityStatus, 30000);
return () => clearInterval(interval);
}, [webrtcManager, isConnected, lastSecurityUpdate, realSecurityLevel]);
}, [webrtcManager, isConnected]);
// ============================================
// FIXED EVENT HANDLERS
@@ -178,46 +178,25 @@ const EnhancedMinimalHeader = ({
// ============================================
React.useEffect(() => {
const updateSessionInfo = () => {
if (sessionManager) {
const isActive = sessionManager.hasActiveSession();
const timeLeft = sessionManager.getTimeLeft();
const currentSession = sessionManager.currentSession;
setHasActiveSession(isActive);
setCurrentTimeLeft(timeLeft);
setSessionType(currentSession?.type || 'unknown');
}
};
updateSessionInfo();
const interval = setInterval(updateSessionInfo, 1000);
return () => clearInterval(interval);
}, [sessionManager]);
// All security features are enabled by default - no session management needed
setHasActiveSession(true);
setCurrentTimeLeft(0);
setSessionType('premium'); // All features enabled
}, []);
React.useEffect(() => {
if (sessionManager?.hasActiveSession()) {
setCurrentTimeLeft(sessionManager.getTimeLeft());
setHasActiveSession(true);
} else {
setHasActiveSession(false);
setRealSecurityLevel(null);
setLastSecurityUpdate(0);
setSessionType('unknown');
}
}, [sessionManager, sessionTimeLeft]);
// All security features are enabled by default
setHasActiveSession(true);
setCurrentTimeLeft(0);
setSessionType('premium'); // All features enabled
}, [sessionTimeLeft]);
React.useEffect(() => {
const handleForceUpdate = (event) => {
if (sessionManager) {
const isActive = sessionManager.hasActiveSession();
const timeLeft = sessionManager.getTimeLeft();
const currentSession = sessionManager.currentSession;
setHasActiveSession(isActive);
setCurrentTimeLeft(timeLeft);
setSessionType(currentSession?.type || 'unknown');
}
// All security features are enabled by default
setHasActiveSession(true);
setCurrentTimeLeft(0);
setSessionType('premium'); // All features enabled
};
// Connection cleanup handler (use existing event from module)
@@ -243,22 +222,36 @@ const EnhancedMinimalHeader = ({
setLastSecurityUpdate(0);
};
const handleDisconnected = () => {
if (window.DEBUG_MODE) {
console.log('🔌 Disconnected - clearing security data in header');
}
setRealSecurityLevel(null);
setLastSecurityUpdate(0);
setHasActiveSession(false);
setCurrentTimeLeft(0);
setSessionType('unknown');
};
document.addEventListener('force-header-update', handleForceUpdate);
document.addEventListener('peer-disconnect', handlePeerDisconnect);
document.addEventListener('connection-cleaned', handleConnectionCleaned);
document.addEventListener('disconnected', handleDisconnected);
return () => {
document.removeEventListener('force-header-update', handleForceUpdate);
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
document.removeEventListener('disconnected', handleDisconnected);
};
}, [sessionManager]);
}, []);
// ============================================
// SECURITY INDICATOR CLICK HANDLER
// ============================================
const handleSecurityClick = (event) => {
const handleSecurityClick = async (event) => {
// Check if it's a right-click or Ctrl+click to disconnect
if (event && (event.button === 2 || event.ctrlKey || event.metaKey)) {
if (onDisconnect && typeof onDisconnect === 'function') {
@@ -267,86 +260,190 @@ const EnhancedMinimalHeader = ({
}
}
if (!realSecurityLevel) {
// Prevent default behavior
event.preventDefault();
event.stopPropagation();
// Debug information
console.log('🔍 Security click debug:', {
hasWebrtcManager: !!webrtcManager,
hasCryptoUtils: !!window.EnhancedSecureCryptoUtils,
hasRealSecurityLevel: !!realSecurityLevel,
connectionStatus: webrtcManager?.connectionState || 'unknown'
});
// Run real security tests if webrtcManager is available
let realTestResults = null;
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
try {
console.log('🔍 Running real security tests...');
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
console.log('✅ Real security tests completed:', realTestResults);
} catch (error) {
console.error('❌ Real security tests failed:', error);
}
} else {
console.log('⚠️ Cannot run security tests:', {
webrtcManager: !!webrtcManager,
cryptoUtils: !!window.EnhancedSecureCryptoUtils
});
}
// If no real test results and no existing security level, show progress message
if (!realTestResults && !realSecurityLevel) {
alert('Security verification in progress...\nPlease wait for real-time cryptographic verification to complete.');
return;
}
// Use real test results if available, otherwise fall back to current data
let securityData = realTestResults || realSecurityLevel;
// If still no security data, create a basic fallback
if (!securityData) {
securityData = {
level: 'UNKNOWN',
score: 0,
color: 'gray',
verificationResults: {},
timestamp: Date.now(),
details: 'Security verification not available',
isRealData: false,
passedChecks: 0,
totalChecks: 0,
sessionType: 'unknown'
};
console.log('⚠️ Using fallback security data:', securityData);
}
// Detailed information about the REAL security check
let message = `🔒 REAL-TIME SECURITY VERIFICATION\n\n`;
message += `Security Level: ${realSecurityLevel.level} (${realSecurityLevel.score}%)\n`;
message += `Session Type: ${realSecurityLevel.sessionType || 'demo'}\n`;
message += `Verification Time: ${new Date(realSecurityLevel.timestamp).toLocaleTimeString()}\n`;
message += `Data Source: ${realSecurityLevel.isRealData ? 'Real Cryptographic Tests' : 'Simulated Data'}\n\n`;
message += `Security Level: ${securityData.level} (${securityData.score}%)\n`;
message += `Session Type: ${securityData.sessionType || 'premium'}\n`;
message += `Verification Time: ${new Date(securityData.timestamp).toLocaleTimeString()}\n`;
message += `Data Source: ${securityData.isRealData ? 'Real Cryptographic Tests' : 'Simulated Data'}\n\n`;
if (realSecurityLevel.verificationResults) {
if (securityData.verificationResults) {
message += 'DETAILED CRYPTOGRAPHIC TESTS:\n';
message += '=' + '='.repeat(40) + '\n';
const passedTests = Object.entries(realSecurityLevel.verificationResults).filter(([key, result]) => result.passed);
const failedTests = Object.entries(realSecurityLevel.verificationResults).filter(([key, result]) => !result.passed);
const passedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => result.passed);
const failedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => !result.passed);
if (passedTests.length > 0) {
message += '✅ PASSED TESTS:\n';
passedTests.forEach(([key, result]) => {
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
message += ` ${testName}: ${result.details}\n`;
message += ` ${testName}: ${result.details || 'Test passed'}\n`;
});
message += '\n';
}
if (failedTests.length > 0) {
message += '❌ UNAVAILABLE/Failed TESTS:\n';
message += '❌ FAILED/UNAVAILABLE TESTS:\n';
failedTests.forEach(([key, result]) => {
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
message += ` ${testName}: ${result.details}\n`;
message += ` ${testName}: ${result.details || 'Test failed or unavailable'}\n`;
});
message += '\n';
}
message += `SUMMARY:\n`;
message += `Passed: ${realSecurityLevel.passedChecks}/${realSecurityLevel.totalChecks} tests\n`;
message += `Passed: ${securityData.passedChecks}/${securityData.totalChecks} tests\n`;
message += `Score: ${securityData.score}/${securityData.maxPossibleScore || 100} points\n\n`;
}
// Add information about what is available in other sessions
message += `\n📋 WHAT'S AVAILABLE IN OTHER SESSIONS:\n`;
// Real security features status
message += `🔒 SECURITY FEATURES STATUS:\n`;
message += '=' + '='.repeat(40) + '\n';
if (realSecurityLevel.sessionType === 'demo') {
message += `🔒 BASIC SESSION (5,000 sat - $2.00):\n`;
message += ` • ECDSA Digital Signatures\n`;
message += ` • Metadata Protection\n`;
message += ` • Perfect Forward Secrecy\n`;
message += ` • Nested Encryption\n`;
message += ` • Packet Padding\n\n`;
if (securityData.verificationResults) {
const features = {
'ECDSA Digital Signatures': securityData.verificationResults.verifyECDSASignatures?.passed || false,
'ECDH Key Exchange': securityData.verificationResults.verifyECDHKeyExchange?.passed || false,
'AES-GCM Encryption': securityData.verificationResults.verifyEncryption?.passed || false,
'Message Integrity (HMAC)': securityData.verificationResults.verifyMessageIntegrity?.passed || false,
'Perfect Forward Secrecy': securityData.verificationResults.verifyPerfectForwardSecrecy?.passed || false,
'Replay Protection': securityData.verificationResults.verifyReplayProtection?.passed || false,
'DTLS Fingerprint': securityData.verificationResults.verifyDTLSFingerprint?.passed || false,
'SAS Verification': securityData.verificationResults.verifySASVerification?.passed || false,
'Metadata Protection': securityData.verificationResults.verifyMetadataProtection?.passed || false,
'Traffic Obfuscation': securityData.verificationResults.verifyTrafficObfuscation?.passed || false
};
message += `🚀 PREMIUM SESSION (20,000 sat - $8.00):\n`;
message += ` • All Basic + Enhanced features\n`;
message += ` • Traffic Obfuscation\n`;
message += ` • Fake Traffic Generation\n`;
message += ` • Decoy Channels\n`;
message += ` • Anti-Fingerprinting\n`;
message += ` • Message Chunking\n`;
message += ` • Advanced Replay Protection\n`;
} else if (realSecurityLevel.sessionType === 'basic') {
message += `🚀 PREMIUM SESSION (20,000 sat - $8.00):\n`;
message += ` • Traffic Obfuscation\n`;
message += ` • Fake Traffic Generation\n`;
message += ` • Decoy Channels\n`;
message += ` • Anti-Fingerprinting\n`;
message += ` • Message Chunking\n`;
message += ` • Advanced Replay Protection\n`;
Object.entries(features).forEach(([feature, isEnabled]) => {
message += `${isEnabled ? '✅' : '❌'} ${feature}\n`;
});
} else {
// Fallback if no verification results
message += `✅ ECDSA Digital Signatures\n`;
message += `✅ ECDH Key Exchange\n`;
message += `✅ AES-GCM Encryption\n`;
message += `✅ Message Integrity (HMAC)\n`;
message += ` Perfect Forward Secrecy\n`;
message += `✅ Replay Protection\n`;
message += `✅ DTLS Fingerprint\n`;
message += `✅ SAS Verification\n`;
message += `✅ Metadata Protection\n`;
message += `✅ Traffic Obfuscation\n`;
}
message += `\n${realSecurityLevel.details || 'Real cryptographic verification completed'}`;
message += `\n${securityData.details || 'Real cryptographic verification completed'}`;
if (realSecurityLevel.isRealData) {
if (securityData.isRealData) {
message += '\n\n✅ This is REAL-TIME verification using actual cryptographic functions.';
} else {
message += '\n\n⚠ Warning: This data may be simulated. Connection may not be fully established.';
}
alert(message);
// Show in a more user-friendly way
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
font-family: monospace;
`;
const content = document.createElement('div');
content.style.cssText = `
background: #1a1a1a;
color: #fff;
padding: 20px;
border-radius: 8px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
white-space: pre-line;
border: 1px solid #333;
`;
content.textContent = message;
modal.appendChild(content);
// Close on click outside
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
// Close on Escape key
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
document.body.removeChild(modal);
document.removeEventListener('keydown', handleKeyDown);
}
};
document.addEventListener('keydown', handleKeyDown);
document.body.appendChild(modal);
};
// ============================================
@@ -407,7 +504,7 @@ const EnhancedMinimalHeader = ({
};
const config = getStatusConfig();
const displaySecurityLevel = realSecurityLevel || securityLevel;
const displaySecurityLevel = isConnected ? (realSecurityLevel || securityLevel) : null;
const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer;
@@ -514,12 +611,11 @@ const EnhancedMinimalHeader = ({
key: 'status-section',
className: 'flex items-center space-x-2 sm:space-x-3'
}, [
// Session Timer
// Session Timer - all features enabled by default
shouldShowTimer && React.createElement(window.SessionTimer, {
key: 'session-timer',
timeLeft: currentTimeLeft,
sessionType: sessionType,
sessionManager: sessionManager,
onDisconnect: onDisconnect
}),
@@ -628,7 +724,7 @@ const EnhancedMinimalHeader = ({
React.createElement('span', {
key: 'status-text',
className: 'text-xs sm:text-sm font-medium'
}, config.text)
}, config.text),
]),
// Disconnect Button

View File

@@ -1,378 +0,0 @@
const React = window.React;
const { useState, useEffect } = React;
const IntegratedLightningPayment = ({ sessionType, onSuccess, onCancel, paymentManager }) => {
const [paymentMethod, setPaymentMethod] = useState('webln');
const [preimage, setPreimage] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState('');
const [invoice, setInvoice] = useState(null);
const [paymentStatus, setPaymentStatus] = useState('pending'); // pending, created, paid, expired
const [qrCodeUrl, setQrCodeUrl] = useState('');
useEffect(() => {
createInvoice();
}, [sessionType]);
const createInvoice = async () => {
if (sessionType === 'free') {
setPaymentStatus('free');
return;
}
setIsProcessing(true);
setError('');
try {
if (!paymentManager) {
throw new Error('Payment manager not available. Please check sessionManager initialization.');
}
const createdInvoice = await paymentManager.createLightningInvoice(sessionType);
if (!createdInvoice) {
throw new Error('Failed to create invoice');
}
setInvoice(createdInvoice);
setPaymentStatus('created');
if (createdInvoice.paymentRequest) {
try {
const dataUrl = await window.generateQRCode(createdInvoice.paymentRequest, { size: 300, margin: 2, errorCorrectionLevel: 'M' });
setQrCodeUrl(dataUrl);
} catch (e) {
console.warn('QR local generation failed, showing placeholder');
const dataUrl = await window.generateQRCode(createdInvoice.paymentRequest, { size: 300 });
setQrCodeUrl(dataUrl);
}
}
} catch (err) {
console.error('Invoice creation failed:', err);
setError(`Error creating invoice: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleWebLNPayment = async () => {
if (!window.webln) {
setError('WebLN is not supported. Please use the Alby or Zeus wallet. SecureBit.chat v4.02.442 - ASN.1 Validated requires WebLN for Lightning payments.');
return;
}
if (!invoice || !invoice.paymentRequest) {
setError('Invoice is not ready for payment');
return;
}
setIsProcessing(true);
setError('');
try {
await window.webln.enable();
const result = await window.webln.sendPayment(invoice.paymentRequest);
if (result.preimage) {
setPaymentStatus('paid');
await activateSession(result.preimage);
} else {
setError('Payment does not contain preimage');
}
} catch (err) {
console.error('WebLN payment failed:', err);
setError(`WebLN Error: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleManualVerification = async () => {
const trimmedPreimage = preimage.trim();
if (!trimmedPreimage) {
setError('Enter payment preimage');
return;
}
if (trimmedPreimage.length !== 64) {
setError('The preimage must be exactly 64 characters long.');
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
setError('The preimage must contain only hexadecimal characters (0-9, a-f, A-F).');
return;
}
if (trimmedPreimage === '1'.repeat(64) ||
trimmedPreimage === 'a'.repeat(64) ||
trimmedPreimage === 'f'.repeat(64)) {
setError('The entered preimage is too weak. Please verify the key..');
return;
}
setError('');
setIsProcessing(true);
try {
await activateSession(trimmedPreimage);
} catch (err) {
setError(`Activation error: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const activateSession = async (preimageValue) => {
try {
let result;
if (paymentManager) {
const paymentHash = invoice?.paymentHash || 'dummy_hash';
result = await paymentManager.safeActivateSession(sessionType, preimageValue, paymentHash);
} else {
console.warn('Payment manager not available, using fallback');
// Fallback if paymentManager is unavailable
result = { success: true, method: 'fallback' };
}
if (result.success) {
setPaymentStatus('paid');
onSuccess(preimageValue, invoice);
} else {
console.error('❌ Session activation failed:', result);
throw new Error(`Session activation failed: ${result.reason}`);
}
} catch (err) {
console.error('❌ Session activation failed:', err);
throw err;
}
};
const handleFreeSession = async () => {
setIsProcessing(true);
try {
await activateSession('0'.repeat(64));
} catch (err) {
setError(`Free session activation error: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
});
};
const pricing = {
free: { sats: 1, hours: 1/60 },
basic: { sats: 500, hours: 1 },
premium: { sats: 1000, hours: 4 },
extended: { sats: 2000, hours: 24 }
}[sessionType];
return React.createElement('div', { className: 'space-y-4 max-w-md mx-auto' }, [
React.createElement('div', { key: 'header', className: 'text-center' }, [
React.createElement('h3', {
key: 'title',
className: 'text-xl font-semibold text-white mb-2'
}, sessionType === 'free' ? 'Free session' : 'Lightning payment'),
React.createElement('div', {
key: 'amount',
className: 'text-2xl font-bold text-orange-400'
}, sessionType === 'free'
? '0 sat per minute'
: `${pricing.sats} сат за ${pricing.hours}ч`
),
sessionType !== 'free' && React.createElement('div', {
key: 'usd',
className: 'text-sm text-gray-400 mt-1'
}, `$${(pricing.sats * 0.0004).toFixed(2)} USD`)
]),
// Loading State
isProcessing && paymentStatus === 'pending' && React.createElement('div', {
key: 'loading',
className: 'text-center'
}, [
React.createElement('div', {
key: 'spinner',
className: 'text-orange-400'
}, [
React.createElement('i', { className: 'fas fa-spinner fa-spin mr-2' }),
'Creating invoice...'
])
]),
// Free Session
sessionType === 'free' && React.createElement('div', {
key: 'free-session',
className: 'space-y-3'
}, [
React.createElement('div', {
key: 'info',
className: 'p-3 bg-blue-500/10 border border-blue-500/20 rounded text-blue-300 text-sm'
}, 'A free 1-minute session will be activated.'),
React.createElement('button', {
key: 'start-btn',
onClick: handleFreeSession,
disabled: isProcessing,
className: 'w-full bg-blue-600 hover:bg-blue-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
}, [
React.createElement('i', {
key: 'icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2`
}),
isProcessing ? 'Activation...' : 'Start free session'
])
]),
// Paid Sessions
sessionType !== 'free' && paymentStatus === 'created' && invoice && React.createElement('div', {
key: 'paid-session',
className: 'space-y-4'
}, [
// QR Code
qrCodeUrl && React.createElement('div', {
key: 'qr-section',
className: 'text-center'
}, [
React.createElement('div', {
key: 'qr-container',
className: 'bg-white p-4 rounded-lg inline-block'
}, [
React.createElement('img', {
key: 'qr-img',
src: qrCodeUrl,
alt: 'Payment QR Code',
className: 'w-48 h-48'
})
]),
React.createElement('div', {
key: 'qr-hint',
className: 'text-xs text-gray-400 mt-2'
}, 'Scan the QR code with any Lightning wallet')
]),
// Payment Request
invoice.paymentRequest && React.createElement('div', {
key: 'payment-request',
className: 'space-y-2'
}, [
React.createElement('div', {
key: 'label',
className: 'text-sm font-medium text-white'
}, 'Payment Request:'),
React.createElement('div', {
key: 'request',
className: 'p-3 bg-gray-800 rounded border text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700',
onClick: () => copyToClipboard(invoice.paymentRequest)
}, [
invoice.paymentRequest.substring(0, 50) + '...',
React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' })
])
]),
// WebLN Payment
React.createElement('div', {
key: 'webln-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'webln-title',
className: 'text-white font-medium flex items-center'
}, [
React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }),
'WebLN wallet (Alby, Zeus)'
]),
React.createElement('button', {
key: 'webln-btn',
onClick: handleWebLNPayment,
disabled: isProcessing,
className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
}, [
React.createElement('i', {
key: 'webln-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2`
}),
isProcessing ? 'Processing...' : 'Pay via WebLN'
])
]),
// Manual Payment
React.createElement('div', {
key: 'divider',
className: 'text-center text-gray-400'
}, 'or'),
React.createElement('div', {
key: 'manual-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'manual-title',
className: 'text-white font-medium'
}, 'Manual payment verification'),
React.createElement('input', {
key: 'preimage-input',
type: 'text',
value: preimage,
onChange: (e) => setPreimage(e.target.value),
placeholder: 'Enter the preimage after payment...',
className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm'
}),
React.createElement('button', {
key: 'verify-btn',
onClick: handleManualVerification,
disabled: isProcessing,
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
}, [
React.createElement('i', {
key: 'verify-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2`
}),
isProcessing ? 'Verification...' : 'Confirm payment'
])
])
]),
// Success State
paymentStatus === 'paid' && React.createElement('div', {
key: 'success',
className: 'text-center p-4 bg-green-500/10 border border-green-500/20 rounded'
}, [
React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-2xl mb-2' }),
React.createElement('div', { key: 'success-text', className: 'text-green-300 font-medium' }, 'Payment confirmed!'),
React.createElement('div', { key: 'success-subtext', className: 'text-green-400 text-sm' }, 'Session activated')
]),
// Error State
error && React.createElement('div', {
key: 'error',
className: 'p-3 bg-red-500/10 border border-red-500/20 rounded text-red-400 text-sm'
}, [
React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle mr-2' }),
error,
error.includes('invoice') && React.createElement('button', {
key: 'retry-btn',
onClick: createInvoice,
className: 'ml-2 text-orange-400 hover:text-orange-300 underline'
}, 'Try again')
]),
// Cancel Button
React.createElement('button', {
key: 'cancel-btn',
onClick: onCancel,
className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded'
}, 'Cancel')
]);
};
window.LightningPayment = IntegratedLightningPayment;

View File

@@ -1,877 +0,0 @@
const React = window.React;
const { useState, useEffect, useRef } = React;
const PaymentModal = ({ isOpen, onClose, sessionManager, onSessionPurchased }) => {
const [step, setStep] = React.useState('select');
const [selectedType, setSelectedType] = React.useState(null);
const [invoice, setInvoice] = React.useState(null);
const [paymentStatus, setPaymentStatus] = React.useState('pending');
const [error, setError] = React.useState('');
const [paymentMethod, setPaymentMethod] = React.useState('webln');
const [preimageInput, setPreimageInput] = React.useState('');
const [isProcessing, setIsProcessing] = React.useState(false);
const [qrCodeUrl, setQrCodeUrl] = React.useState('');
const [paymentTimer, setPaymentTimer] = React.useState(null);
const [timeLeft, setTimeLeft] = React.useState(0);
const [showSecurityDetails, setShowSecurityDetails] = React.useState(false);
const pollInterval = React.useRef(null);
React.useEffect(() => {
if (!isOpen) {
resetModal();
if (pollInterval.current) {
clearInterval(pollInterval.current);
}
if (paymentTimer) {
clearInterval(paymentTimer);
}
}
}, [isOpen]);
const resetModal = () => {
setStep('select');
setSelectedType(null);
setInvoice(null);
setPaymentStatus('pending');
setError('');
setPaymentMethod('webln');
setPreimageInput('');
setIsProcessing(false);
setQrCodeUrl('');
setTimeLeft(0);
setShowSecurityDetails(false);
};
const getSecurityFeaturesInfo = (sessionType) => {
const features = {
demo: {
title: 'Demo Session - Basic Security',
description: 'Limited testing session with basic security features',
available: [
'🔐 Basic end-to-end encryption (AES-GCM 256)',
'🔑 Simple key exchange (ECDH P-384)',
'✅ Message integrity verification',
'⚡ Rate limiting protection'
],
unavailable: [
'🔐 ECDSA Digital Signatures',
'🛡️ Metadata Protection',
'🔄 Perfect Forward Secrecy',
'🔐 Nested Encryption',
'📦 Packet Padding',
'🎭 Traffic Obfuscation',
'🎪 Fake Traffic Generation',
'🕵️ Decoy Channels',
'🚫 Anti-Fingerprinting',
'📝 Message Chunking',
'🔄 Advanced Replay Protection'
],
upgrade: {
next: 'Basic Session (5,000 sat - $2.00)',
features: [
'🔐 ECDSA Digital Signatures',
'🛡️ Metadata Protection',
'🔄 Perfect Forward Secrecy',
'🔐 Nested Encryption',
'📦 Packet Padding'
]
}
},
basic: {
title: 'Basic Session - Enhanced Security',
description: 'Full featured session with enhanced security features',
available: [
'🔐 Basic end-to-end encryption (AES-GCM 256)',
'🔑 Simple key exchange (ECDH P-384)',
'✅ Message integrity verification',
'⚡ Rate limiting protection',
'🔐 ECDSA Digital Signatures',
'🛡️ Metadata Protection',
'🔄 Perfect Forward Secrecy',
'🔐 Nested Encryption',
'📦 Packet Padding',
'🔒 Complete ASN.1 validation',
'🔍 OID and EC point verification',
'🏗️ SPKI structure validation',
'🛡️ 18-layer security architecture'
],
unavailable: [
'🎭 Traffic Obfuscation',
'🎪 Fake Traffic Generation',
'🕵️ Decoy Channels',
'🚫 Anti-Fingerprinting',
'📝 Message Chunking',
'🔄 Advanced Replay Protection'
],
upgrade: {
next: 'Premium Session (20,000 sat - $8.00)',
features: [
'🎭 Traffic Obfuscation',
'🎪 Fake Traffic Generation',
'🕵️ Decoy Channels',
'🚫 Anti-Fingerprinting',
'📝 Message Chunking',
'🔄 Advanced Replay Protection'
]
}
},
premium: {
title: 'Premium Session - Maximum Security',
description: 'Extended session with maximum security protection',
available: [
'🔐 Basic end-to-end encryption (AES-GCM 256)',
'🔑 Simple key exchange (ECDH P-384)',
'✅ Message integrity verification',
'⚡ Rate limiting protection',
'🔐 ECDSA Digital Signatures',
'🛡️ Metadata Protection',
'🔄 Perfect Forward Secrecy',
'🔐 Nested Encryption',
'📦 Packet Padding',
'🎭 Traffic Obfuscation',
'🎪 Fake Traffic Generation',
'🕵️ Decoy Channels',
'🚫 Anti-Fingerprinting',
'📝 Message Chunking',
'🔄 Advanced Replay Protection',
'🔒 Complete ASN.1 validation',
'🔍 OID and EC point verification',
'🏗️ SPKI structure validation',
'🛡️ 18-layer security architecture',
'🚀 ASN.1 Validated'
],
unavailable: [],
upgrade: {
next: 'Maximum security achieved!',
features: ['🎉 All security features unlocked!']
}
}
};
return features[sessionType] || features.demo;
};
const handleSelectType = async (type) => {
setSelectedType(type);
setError('');
if (type === 'demo') {
try {
if (!sessionManager || !sessionManager.createDemoSession) {
throw new Error('Demo session manager not available');
}
const demoSession = sessionManager.createDemoSession();
if (!demoSession.success) {
throw new Error(demoSession.reason);
}
setInvoice({
sessionType: 'demo',
amount: 0,
paymentHash: demoSession.paymentHash,
memo: `Demo session (${demoSession.durationMinutes} minutes)`,
createdAt: Date.now(),
isDemo: true,
preimage: demoSession.preimage,
warning: demoSession.warning,
securityLevel: 'Basic'
});
setPaymentStatus('demo');
} catch (error) {
setError(`Demo session creation failed: ${error.message}`);
return;
}
} else {
await createRealInvoice(type);
}
setStep('payment');
};
const createRealInvoice = async (type) => {
setPaymentStatus('creating');
setIsProcessing(true);
setError('');
try {
console.log(`Creating Lightning invoice for ${type} session...`);
if (!sessionManager) {
throw new Error('Session manager not initialized');
}
const createdInvoice = await sessionManager.createLightningInvoice(type);
if (!createdInvoice || !createdInvoice.paymentRequest) {
throw new Error('Failed to create Lightning invoice');
}
createdInvoice.securityLevel = sessionManager.getSecurityLevelForSession(type);
setInvoice(createdInvoice);
setPaymentStatus('created');
try {
const dataUrl = await window.generateQRCode(createdInvoice.paymentRequest, { size: 300, margin: 2, errorCorrectionLevel: 'M' });
setQrCodeUrl(dataUrl);
} catch (e) {
console.warn('QR local generation failed, showing placeholder');
const dataUrl = await window.generateQRCode(createdInvoice.paymentRequest, { size: 300 });
setQrCodeUrl(dataUrl);
}
const expirationTime = 15 * 60 * 1000;
setTimeLeft(expirationTime);
const timer = setInterval(() => {
setTimeLeft(prev => {
const newTime = prev - 1000;
if (newTime <= 0) {
clearInterval(timer);
setPaymentStatus('expired');
setError('Payment time has expired. Create a new invoice.');
return 0;
}
return newTime;
});
}, 1000);
setPaymentTimer(timer);
startPaymentPolling(createdInvoice.checkingId);
console.log('✅ Lightning invoice created successfully:', createdInvoice);
} catch (err) {
console.error('❌ Invoice creation failed:', err);
setError(`Invoice creation error: ${err.message}`);
setPaymentStatus('failed');
} finally {
setIsProcessing(false);
}
};
const startPaymentPolling = (checkingId) => {
if (pollInterval.current) {
clearInterval(pollInterval.current);
}
pollInterval.current = setInterval(async () => {
try {
const status = await sessionManager.checkPaymentStatus(checkingId);
if (status.paid && status.preimage) {
clearInterval(pollInterval.current);
setPaymentStatus('paid');
await handlePaymentSuccess(status.preimage);
}
} catch (error) {
console.warn('Payment status check failed:', error);
}
}, 3000);
};
const handleWebLNPayment = async () => {
if (!window.webln) {
setError('WebLN is not supported. Please install the Alby or Zeus wallet.');
return;
}
if (!invoice || !invoice.paymentRequest) {
setError('Invoice is not ready for payment.');
return;
}
setIsProcessing(true);
setError('');
setPaymentStatus('paying');
try {
await window.webln.enable();
const result = await window.webln.sendPayment(invoice.paymentRequest);
if (result.preimage) {
setPaymentStatus('paid');
await handlePaymentSuccess(result.preimage);
} else {
throw new Error('Payment does not contain preimage');
}
} catch (err) {
console.error('❌ WebLN payment failed:', err);
setError(`WebLN payment error: ${err.message}`);
setPaymentStatus('created');
} finally {
setIsProcessing(false);
}
};
const handleManualVerification = async () => {
const trimmedPreimage = preimageInput.trim();
if (!trimmedPreimage) {
setError('Enter payment preimage');
return;
}
if (trimmedPreimage.length !== 64) {
setError('The preimage must be exactly 64 characters long.');
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
setError('The preimage must contain only hexadecimal characters (0-9, a-f, A-F).');
return;
}
const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64), '0'.repeat(64)];
if (dummyPreimages.includes(trimmedPreimage) && selectedType !== 'demo') {
setError('The entered preimage is invalid. Please use the actual preimage from the payment.');
return;
}
setIsProcessing(true);
setError('');
setPaymentStatus('paying');
try {
await handlePaymentSuccess(trimmedPreimage);
} catch (err) {
setError(err.message);
setPaymentStatus('created');
} finally {
setIsProcessing(false);
}
};
const handleDemoSession = async () => {
setIsProcessing(true);
setError('');
try {
if (!invoice?.preimage) {
throw new Error('Demo preimage not available');
}
const isValid = await sessionManager.verifyPayment(invoice.preimage, invoice.paymentHash);
if (isValid && isValid.verified) {
onSessionPurchased({
type: 'demo',
preimage: invoice.preimage,
paymentHash: invoice.paymentHash,
amount: 0,
isDemo: true,
warning: invoice.warning,
securityLevel: 'basic'
});
setTimeout(() => {
onClose();
}, 1500);
} else {
throw new Error(isValid?.reason || 'Demo session verification failed');
}
} catch (err) {
setError(`Demo session activation error: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handlePaymentSuccess = async (preimage) => {
try {
console.log('🔍 Verifying payment...', { selectedType, preimage });
let isValid;
if (selectedType === 'demo') {
return;
} else {
isValid = await sessionManager.verifyPayment(preimage, invoice.paymentHash);
}
if (isValid) {
if (pollInterval.current) {
clearInterval(pollInterval.current);
}
if (paymentTimer) {
clearInterval(paymentTimer);
}
onSessionPurchased({
type: selectedType,
preimage,
paymentHash: invoice.paymentHash,
amount: invoice.amount,
securityLevel: invoice.securityLevel || (selectedType === 'basic' ? 'enhanced' : 'maximum')
});
setTimeout(() => {
onClose();
}, 1500);
} else {
throw new Error('Payment verification failed. Please check the preimage for correctness or try again.');
}
} catch (error) {
console.error('❌ Payment verification failed:', error);
throw error;
}
};
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const formatTime = (ms) => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const getSecurityBadgeColor = (level) => {
switch (level?.toLowerCase()) {
case 'basic': return 'bg-blue-500/20 text-blue-300 border-blue-500/30';
case 'enhanced': return 'bg-orange-500/20 text-orange-300 border-orange-500/30';
case 'maximum': return 'bg-green-500/20 text-green-300 border-green-500/30';
default: return 'bg-gray-500/20 text-gray-300 border-gray-500/30';
}
};
const pricing = sessionManager?.sessionPrices || {
demo: { sats: 0, hours: 0.1, usd: 0.00 },
basic: { sats: 5000, hours: 1, usd: 2.00 },
premium: { sats: 20000, hours: 6, usd: 8.00 }
};
if (!isOpen) return null;
return React.createElement('div', {
className: 'fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4'
}, [
React.createElement('div', {
key: 'modal',
className: 'card-minimal rounded-xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto custom-scrollbar'
}, [
React.createElement('div', {
key: 'header',
className: 'flex items-center justify-between mb-6'
}, [
React.createElement('h2', {
key: 'title',
className: 'text-xl font-semibold text-primary'
}, step === 'select' ? 'Select session type' :
step === 'details' ? 'Security Features Details' : 'Session payment'),
React.createElement('button', {
key: 'close',
onClick: onClose,
className: 'text-gray-400 hover:text-white transition-colors'
}, React.createElement('i', { className: 'fas fa-times' }))
]),
step === 'select' && window.SessionTypeSelector && React.createElement(window.SessionTypeSelector, {
key: 'selector',
onSelectType: handleSelectType,
onCancel: onClose,
sessionManager: sessionManager
}),
step === 'payment' && React.createElement('div', {
key: 'payment-step',
className: 'space-y-6'
}, [
React.createElement('div', {
key: 'session-info',
className: 'text-center p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg'
}, [
React.createElement('h3', {
key: 'session-title',
className: 'text-lg font-semibold text-orange-400 mb-2'
}, [
`${selectedType.charAt(0).toUpperCase() + selectedType.slice(1)} session`,
invoice?.securityLevel && React.createElement('span', {
key: 'security-badge',
className: `text-xs px-2 py-1 rounded-full border ${getSecurityBadgeColor(invoice.securityLevel)}`
}, invoice.securityLevel.toUpperCase())
]),
React.createElement('div', {
key: 'session-details',
className: 'text-sm text-secondary'
}, [
React.createElement('div', { key: 'amount' }, `${pricing[selectedType].sats} sat for ${pricing[selectedType].hours}h`),
pricing[selectedType].usd > 0 && React.createElement('div', {
key: 'usd',
className: 'text-gray-400'
}, `${pricing[selectedType].usd} USD`),
React.createElement('button', {
key: 'details-btn',
onClick: () => setStep('details'),
className: 'mt-2 text-xs text-blue-400 hover:text-blue-300 underline cursor-pointer'
}, '📋 View Security Details')
])
]),
timeLeft > 0 && paymentStatus === 'created' && React.createElement('div', {
key: 'timer',
className: 'text-center p-3 bg-yellow-500/10 border border-yellow-500/20 rounded'
}, [
React.createElement('div', {
key: 'timer-text',
className: 'text-yellow-400 font-medium'
}, `⏱️ Time to pay: ${formatTime(timeLeft)}`)
]),
paymentStatus === 'demo' && React.createElement('div', {
key: 'demo-payment',
className: 'space-y-4'
}, [
React.createElement('div', {
key: 'demo-info',
className: 'p-4 bg-green-500/10 border border-green-500/20 rounded text-green-300 text-sm text-center'
}, [
React.createElement('div', { key: 'demo-title', className: 'font-medium mb-1' }, '🎮 Demo Session Available'),
React.createElement('div', { key: 'demo-details', className: 'text-xs' },
`Limited to ${invoice?.durationMinutes || 6} minutes for testing`)
]),
invoice?.warning && React.createElement('div', {
key: 'demo-warning',
className: 'p-3 bg-yellow-500/10 border border-yellow-500/20 rounded text-yellow-300 text-xs text-center'
}, invoice.warning),
React.createElement('div', {
key: 'demo-preimage',
className: 'p-3 bg-gray-800/50 rounded border border-gray-600 text-xs font-mono text-gray-300'
}, [
React.createElement('div', { key: 'preimage-label', className: 'text-gray-400 mb-1' }, 'Demo Preimage:'),
React.createElement('div', { key: 'preimage-value', className: 'break-all' },
invoice?.preimage || 'Generating...')
]),
React.createElement('button', {
key: 'demo-btn',
onClick: handleDemoSession,
disabled: isProcessing,
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed'
}, [
React.createElement('i', {
key: 'demo-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2`
}),
isProcessing ? 'Activating...' : 'Activate Demo Session'
])
]),
paymentStatus === 'creating' && React.createElement('div', {
key: 'creating',
className: 'text-center p-4'
}, [
React.createElement('i', { className: 'fas fa-spinner fa-spin text-orange-400 text-2xl mb-2' }),
React.createElement('div', { className: 'text-primary' }, 'Creating Lightning invoice...'),
React.createElement('div', { className: 'text-secondary text-sm mt-1' }, 'Connecting to the Lightning Network...')
]),
(paymentStatus === 'created' || paymentStatus === 'paying') && invoice && React.createElement('div', {
key: 'payment-methods',
className: 'space-y-6'
}, [
qrCodeUrl && React.createElement('div', {
key: 'qr-section',
className: 'text-center'
}, [
React.createElement('div', {
key: 'qr-container',
className: 'bg-white p-4 rounded-lg inline-block'
}, [
React.createElement('img', {
key: 'qr-img',
src: qrCodeUrl,
alt: 'Lightning Payment QR Code',
className: 'w-48 h-48'
})
]),
React.createElement('div', {
key: 'qr-hint',
className: 'text-xs text-gray-400 mt-2'
}, 'Scan with any Lightning wallet')
]),
invoice.paymentRequest && React.createElement('div', {
key: 'payment-request',
className: 'space-y-2'
}, [
React.createElement('div', {
key: 'pr-label',
className: 'text-sm font-medium text-primary'
}, 'Lightning Payment Request:'),
React.createElement('div', {
key: 'pr-container',
className: 'p-3 bg-gray-800/50 rounded border border-gray-600 text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700/50 transition-colors',
onClick: () => copyToClipboard(invoice.paymentRequest),
title: 'Click to copy'
}, [
invoice.paymentRequest.substring(0, 60) + '...',
React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' })
])
]),
// WebLN Payment
React.createElement('div', {
key: 'webln-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'webln-title',
className: 'text-primary font-medium flex items-center'
}, [
React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }),
'WebLN wallet (recommended)'
]),
React.createElement('div', {
key: 'webln-info',
className: 'text-xs text-gray-400 mb-2'
}, 'Alby, Zeus, or other WebLN-compatible wallets'),
React.createElement('button', {
key: 'webln-btn',
onClick: handleWebLNPayment,
disabled: isProcessing || paymentStatus === 'paying',
className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
}, [
React.createElement('i', {
key: 'webln-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2`
}),
paymentStatus === 'paying' ? 'Processing payment...' : 'Pay via WebLN'
])
]),
// Divider
React.createElement('div', {
key: 'divider',
className: 'text-center text-gray-400 text-sm'
}, '— or —'),
// Manual Verification
React.createElement('div', {
key: 'manual-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'manual-title',
className: 'text-primary font-medium'
}, 'Manual payment confirmation'),
React.createElement('div', {
key: 'manual-info',
className: 'text-xs text-gray-400'
}, 'Pay the invoice in any wallet and enter the preimage.:'),
React.createElement('input', {
key: 'preimage-input',
type: 'text',
value: preimageInput,
onChange: (e) => setPreimageInput(e.target.value),
placeholder: 'Enter the preimage (64 hex characters)...',
className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm font-mono',
maxLength: 64
}),
React.createElement('button', {
key: 'verify-btn',
onClick: handleManualVerification,
disabled: isProcessing || !preimageInput.trim(),
className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors'
}, [
React.createElement('i', {
key: 'verify-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2`
}),
isProcessing ? 'Checking payment...' : 'Confirm payment'
])
])
]),
// Success State
paymentStatus === 'paid' && React.createElement('div', {
key: 'success',
className: 'text-center p-6 bg-green-500/10 border border-green-500/20 rounded-lg'
}, [
React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-3xl mb-3' }),
React.createElement('div', { key: 'success-title', className: 'text-green-300 font-semibold text-lg mb-1' }, '✅ Payment confirmed!'),
React.createElement('div', { key: 'success-text', className: 'text-green-400 text-sm' }, 'The session will be activated upon connecting to the chat.')
]),
// Error State
error && React.createElement('div', {
key: 'error',
className: 'p-4 bg-red-500/10 border border-red-500/20 rounded-lg'
}, [
React.createElement('div', {
key: 'error-content',
className: 'flex items-start space-x-3'
}, [
React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle text-red-400 mt-0.5' }),
React.createElement('div', { key: 'error-text', className: 'flex-1' }, [
React.createElement('div', { key: 'error-message', className: 'text-red-400 text-sm' }, error),
(error.includes('invoice') || paymentStatus === 'failed') && React.createElement('button', {
key: 'retry-btn',
onClick: () => createRealInvoice(selectedType),
className: 'mt-2 text-orange-400 hover:text-orange-300 underline text-sm'
}, 'Create a new invoice')
])
])
]),
paymentStatus !== 'paid' && React.createElement('div', {
key: 'back-section',
className: 'pt-4 border-t border-gray-600'
}, [
React.createElement('button', {
key: 'back-btn',
onClick: () => setStep('select'),
className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded transition-colors'
}, [
React.createElement('i', { key: 'back-icon', className: 'fas fa-arrow-left mr-2' }),
'Choose another session'
])
])
]),
// Security Details Step
step === 'details' && React.createElement('div', {
key: 'details-step',
className: 'space-y-6'
}, [
React.createElement('div', {
key: 'details-header',
className: 'text-center p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg'
}, [
React.createElement('h3', {
key: 'details-title',
className: 'text-lg font-semibold text-blue-400 mb-2'
}, getSecurityFeaturesInfo(selectedType).title),
React.createElement('p', {
key: 'details-description',
className: 'text-sm text-blue-300'
}, getSecurityFeaturesInfo(selectedType).description)
]),
// Available Features
React.createElement('div', { key: 'available-features' }, [
React.createElement('h4', {
key: 'available-title',
className: 'text-sm font-medium text-green-300 mb-3 flex items-center'
}, [
React.createElement('i', {
key: 'check-icon',
className: 'fas fa-check-circle mr-2'
}),
'Available Security Features'
]),
React.createElement('div', {
key: 'available-list',
className: 'grid grid-cols-1 gap-2'
}, getSecurityFeaturesInfo(selectedType).available.map((feature, index) =>
React.createElement('div', {
key: index,
className: 'flex items-center gap-2 text-sm text-green-300'
}, [
React.createElement('i', {
key: 'check',
className: 'fas fa-check text-green-400 w-4'
}),
React.createElement('span', {
key: 'text'
}, feature)
])
))
]),
// Unavailable Features (if any)
getSecurityFeaturesInfo(selectedType).unavailable.length > 0 && React.createElement('div', { key: 'unavailable-features' }, [
React.createElement('h4', {
key: 'unavailable-title',
className: 'text-sm font-medium text-red-300 mb-3 flex items-center'
}, [
React.createElement('i', {
key: 'minus-icon',
className: 'fas fa-minus-circle mr-2'
}),
'Not Available in This Session'
]),
React.createElement('div', {
key: 'unavailable-list',
className: 'grid grid-cols-1 gap-2'
}, getSecurityFeaturesInfo(selectedType).unavailable.map((feature, index) =>
React.createElement('div', {
key: index,
className: 'flex items-center gap-2 text-sm text-red-300'
}, [
React.createElement('i', {
key: 'minus',
className: 'fas fa-minus text-red-400 w-4'
}),
React.createElement('span', {
key: 'text'
}, feature)
])
))
]),
// Upgrade Information
React.createElement('div', { key: 'upgrade-info' }, [
React.createElement('h4', {
key: 'upgrade-title',
className: 'text-sm font-medium text-blue-300 mb-3 flex items-center'
}, [
React.createElement('i', {
key: 'upgrade-icon',
className: 'fas fa-arrow-up mr-2'
}),
'Upgrade for More Security'
]),
React.createElement('div', {
key: 'upgrade-content',
className: 'p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg'
}, [
React.createElement('div', {
key: 'upgrade-next',
className: 'text-sm font-medium text-blue-300 mb-2'
}, getSecurityFeaturesInfo(selectedType).upgrade.next),
React.createElement('div', {
key: 'upgrade-features',
className: 'grid grid-cols-1 gap-1'
}, getSecurityFeaturesInfo(selectedType).upgrade.features.map((feature, index) =>
React.createElement('div', {
key: index,
className: 'flex items-center gap-2 text-xs text-blue-300'
}, [
React.createElement('i', {
key: 'arrow',
className: 'fas fa-arrow-right text-blue-400 w-3'
}),
React.createElement('span', {
key: 'text'
}, feature)
])
))
])
]),
// Back Button
React.createElement('div', {
key: 'details-back-section',
className: 'pt-4 border-t border-gray-600'
}, [
React.createElement('button', {
key: 'details-back-btn',
onClick: () => setStep('payment'),
className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded transition-colors'
}, [
React.createElement('i', { key: 'back-icon', className: 'fas fa-arrow-left mr-2' }),
'Back to Payment'
])
])
])
])
]);
};
window.PaymentModal = PaymentModal;

View File

@@ -1,394 +0,0 @@
const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => {
const [selectedType, setSelectedType] = React.useState(null);
const [demoInfo, setDemoInfo] = React.useState(null);
const [refreshTimer, setRefreshTimer] = React.useState(null);
const [lastRefresh, setLastRefresh] = React.useState(Date.now());
// We receive up-to-date information about demo limits
const updateDemoInfo = React.useCallback(() => {
if (sessionManager && sessionManager.getDemoSessionInfo) {
try {
const info = sessionManager.getDemoSessionInfo();
if (window.DEBUG_MODE) {
console.log('🔄 Demo info updated:', info);
}
setDemoInfo(info);
setLastRefresh(Date.now());
} catch (error) {
console.error('Failed to get demo info:', error);
}
}
}, [sessionManager]);
// Update information on load and every 10 seconds
React.useEffect(() => {
updateDemoInfo();
const interval = setInterval(updateDemoInfo, 10000);
setRefreshTimer(interval);
return () => {
if (interval) clearInterval(interval);
};
}, [updateDemoInfo]);
// Clear timer on unmount
React.useEffect(() => {
return () => {
if (refreshTimer) {
clearInterval(refreshTimer);
}
};
}, [refreshTimer]);
const sessionTypes = [
{
id: 'demo',
name: 'Demo',
duration: '6 minutes',
price: '0 sat',
usd: '$0.00',
popular: false,
securityLevel: 'Basic',
securityBadge: 'BASIC',
securityColor: 'bg-blue-500/20 text-blue-300',
description: 'Limited testing session with basic security',
features: [
'Basic end-to-end encryption',
'Simple key exchange',
'Message integrity',
'Rate limiting'
],
limitations: [
'No advanced security features',
'No traffic obfuscation',
'No metadata protection'
]
},
{
id: 'basic',
name: 'Basic',
duration: '1 hour',
price: '5,000 sat',
usd: '$2.00',
securityLevel: 'Enhanced',
securityBadge: 'ENHANCED',
securityColor: 'bg-orange-500/20 text-orange-300',
popular: true,
description: 'Full featured session with enhanced security',
features: [
'All basic features',
'ECDSA digital signatures',
'Metadata protection',
'Perfect forward secrecy',
'Nested encryption',
'Packet padding',
'Complete ASN.1 validation',
'OID and EC point verification',
'SPKI structure validation',
'18-layer security architecture',
'ASN.1 Validated'
],
limitations: [
'Limited traffic obfuscation',
'No fake traffic generation'
]
},
{
id: 'premium',
name: 'Premium',
duration: '6 hours',
price: '20,000 sat',
usd: '$8.00',
securityLevel: 'Maximum',
securityBadge: 'MAXIMUM',
securityColor: 'bg-green-500/20 text-green-300',
description: 'Extended session with maximum security protection',
features: [
'All enhanced features',
'Traffic obfuscation',
'Fake traffic generation',
'Decoy channels',
'Anti-fingerprinting',
'Message chunking',
'Advanced replay protection',
'Complete ASN.1 validation',
'OID and EC point verification',
'SPKI structure validation',
'18-layer security architecture',
'ASN.1 Validated'
],
limitations: []
}
];
const handleTypeSelect = (typeId) => {
console.log(`🎯 Selecting session type: ${typeId}`);
if (typeId === 'demo') {
if (demoInfo && !demoInfo.canUseNow) {
let message = `Demo session not available.\n\n`;
if (demoInfo.blockingReason === 'global_limit') {
message += `Reason: Too many global demo sessions active (${demoInfo.globalActive}/${demoInfo.globalLimit})\n`;
message += `Please try again in a few minutes.`;
} else if (demoInfo.blockingReason === 'daily_limit') {
message += `Reason: Daily limit reached (${demoInfo.used}/${demoInfo.total})\n`;
message += `Next available: ${demoInfo.nextAvailable}`;
} else if (demoInfo.blockingReason === 'session_cooldown') {
message += `Reason: Cooldown between sessions\n`;
message += `Next available: ${demoInfo.nextAvailable}`;
} else if (demoInfo.blockingReason === 'completion_cooldown') {
message += `Reason: Wait period after last session\n`;
message += `Next available: ${demoInfo.nextAvailable}`;
} else {
message += `Next available: ${demoInfo.nextAvailable}`;
}
alert(message);
return;
}
}
setSelectedType(typeId);
};
const formatCooldownTime = (minutes) => {
if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
return `${minutes}m`;
};
return React.createElement('div', { className: 'space-y-6' }, [
React.createElement('div', { key: 'header', className: 'text-center' }, [
React.createElement('h3', {
key: 'title',
className: 'text-xl font-semibold text-white mb-2'
}, 'Choose Your Session'),
React.createElement('p', {
key: 'subtitle',
className: 'text-gray-300 text-sm'
}, 'Different security levels for different needs')
]),
React.createElement('div', { key: 'types', className: 'space-y-4' },
sessionTypes.map(type => {
const isDemo = type.id === 'demo';
const isDisabled = isDemo && demoInfo && !demoInfo.canUseNow;
return React.createElement('div', {
key: type.id,
onClick: () => !isDisabled && handleTypeSelect(type.id),
className: `relative card-minimal ${selectedType === type.id ? 'card-minimal--selected' : ''} rounded-lg p-5 border-2 transition-all ${
selectedType === type.id
? 'border-orange-500 bg-orange-500/15 ring-2 ring-orange-400 ring-offset-2 ring-offset-black/30'
: 'border-gray-600 hover:border-orange-400'
} ${type.popular && selectedType !== type.id ? 'ring-2 ring-orange-500/30' : ''} ${
isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
}`
}, [
// Popular badge
type.popular && React.createElement('div', {
key: 'popular-badge',
className: 'absolute -top-2 right-3 bg-orange-500 text-white text-xs px-3 py-1 rounded-full font-medium'
}, 'Most Popular'),
React.createElement('div', { key: 'content', className: 'space-y-4' }, [
// Header with name and security level
React.createElement('div', { key: 'header', className: 'flex items-start justify-between' }, [
React.createElement('div', { key: 'title-section' }, [
React.createElement('div', { key: 'name-row', className: 'flex items-center gap-3 mb-2' }, [
React.createElement('h4', {
key: 'name',
className: 'text-xl font-bold text-white'
}, type.name),
isDemo && React.createElement('span', {
key: 'free-badge',
className: 'text-xs bg-blue-500/20 text-blue-300 px-2 py-1 rounded-full font-medium'
}, 'FREE'),
React.createElement('span', {
key: 'security-badge',
className: `text-xs px-2 py-1 rounded-full font-medium ${type.securityColor}`
}, type.securityBadge)
]),
React.createElement('p', {
key: 'duration',
className: 'text-gray-300 font-medium mb-1'
}, `Duration: ${type.duration}`),
React.createElement('p', {
key: 'description',
className: 'text-sm text-gray-400'
}, type.description)
]),
React.createElement('div', { key: 'pricing', className: 'text-right' }, [
React.createElement('div', {
key: 'sats',
className: `text-xl font-bold ${isDemo ? 'text-green-400' : 'text-orange-400'}`
}, type.price),
React.createElement('div', {
key: 'usd',
className: 'text-sm text-gray-400'
}, type.usd)
])
]),
// Demo status info
isDemo && demoInfo && React.createElement('div', {
key: 'demo-status',
className: 'p-3 bg-blue-900/20 border border-blue-700/30 rounded-lg'
}, [
React.createElement('div', {
key: 'availability',
className: `text-sm font-medium ${demoInfo.canUseNow ? 'text-green-400' : 'text-yellow-400'}`
}, demoInfo.canUseNow ?
`✅ Available (${demoInfo.available}/${demoInfo.total} today)` :
`⏰ Next: ${demoInfo.nextAvailable}`
),
demoInfo.globalActive > 0 && React.createElement('div', {
key: 'global-status',
className: 'text-blue-300 text-xs mt-1'
}, `🌐 Global: ${demoInfo.globalActive}/${demoInfo.globalLimit} active`)
]),
// Security features
React.createElement('div', { key: 'features-section', className: 'space-y-3' }, [
React.createElement('div', { key: 'features' }, [
React.createElement('h5', {
key: 'features-title',
className: 'text-sm font-medium text-green-300 mb-2 flex items-center'
}, [
React.createElement('i', {
key: 'shield-icon',
className: 'fas fa-shield-alt mr-2'
}),
'Security Features'
]),
React.createElement('div', {
key: 'features-list',
className: 'grid grid-cols-1 gap-1'
}, type.features.map((feature, index) =>
React.createElement('div', {
key: index,
className: 'flex items-center gap-2 text-xs text-gray-300'
}, [
React.createElement('i', {
key: 'check',
className: 'fas fa-check text-green-400 w-3'
}),
React.createElement('span', {
key: 'text'
}, feature)
])
))
]),
// Limitations (if any)
type.limitations && type.limitations.length > 0 && React.createElement('div', { key: 'limitations' }, [
React.createElement('h5', {
key: 'limitations-title',
className: 'text-sm font-medium text-yellow-300 mb-2 flex items-center'
}, [
React.createElement('i', {
key: 'info-icon',
className: 'fas fa-info-circle mr-2'
}),
'Limitations'
]),
React.createElement('div', {
key: 'limitations-list',
className: 'grid grid-cols-1 gap-1'
}, type.limitations.map((limitation, index) =>
React.createElement('div', {
key: index,
className: 'flex items-center gap-2 text-xs text-gray-400'
}, [
React.createElement('i', {
key: 'minus',
className: 'fas fa-minus text-yellow-400 w-3'
}),
React.createElement('span', {
key: 'text'
}, limitation)
])
))
])
])
])
])
})
),
demoInfo && React.createElement('div', {
key: 'demo-info',
className: 'bg-gradient-to-r from-blue-900/20 to-purple-900/20 border border-blue-700/50 rounded-lg p-4'
}, [
React.createElement('div', {
key: 'demo-header',
className: 'flex items-center gap-2 text-blue-300 text-sm font-medium mb-3'
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-info-circle'
}),
React.createElement('span', {
key: 'title'
}, 'Demo Session Information')
]),
React.createElement('div', {
key: 'demo-details',
className: 'grid grid-cols-1 md:grid-cols-2 gap-3 text-blue-200 text-xs'
}, [
React.createElement('div', { key: 'limits', className: 'space-y-1' }, [
React.createElement('div', { key: 'daily' }, `📅 Daily limit: ${demoInfo.total} sessions`),
React.createElement('div', { key: 'duration' }, `⏱️ Duration: ${demoInfo.durationMinutes} minutes each`),
React.createElement('div', { key: 'cooldown' }, `⏰ Cooldown: ${demoInfo.sessionCooldownMinutes} min between sessions`)
]),
React.createElement('div', { key: 'status', className: 'space-y-1' }, [
React.createElement('div', { key: 'used' }, `📊 Used today: ${demoInfo.used}/${demoInfo.total}`),
React.createElement('div', { key: 'global' }, `🌐 Global active: ${demoInfo.globalActive}/${demoInfo.globalLimit}`),
React.createElement('div', {
key: 'next',
className: demoInfo.canUseNow ? 'text-green-300' : 'text-yellow-300'
}, `🎯 Status: ${demoInfo.canUseNow ? 'Available now' : demoInfo.nextAvailable}`)
])
]),
React.createElement('div', {
key: 'security-note',
className: 'mt-3 p-2 bg-yellow-500/10 border border-yellow-500/20 rounded text-yellow-200 text-xs'
}, '⚠️ Demo sessions use basic security only. Upgrade to paid sessions for enhanced protection.'),
React.createElement('div', {
key: 'last-updated',
className: 'text-xs text-gray-400 mt-2 text-center'
}, `Last updated: ${new Date(lastRefresh).toLocaleTimeString()}`)
]),
// Action buttons
React.createElement('div', { key: 'buttons', className: 'flex space-x-3' }, [
React.createElement('button', {
key: 'continue',
onClick: () => {
if (selectedType) {
console.log(`🚀 Proceeding with session type: ${selectedType}`);
onSelectType(selectedType);
}
},
disabled: !selectedType || (selectedType === 'demo' && demoInfo && !demoInfo.canUseNow),
className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all'
}, [
React.createElement('i', {
key: 'icon',
className: selectedType === 'demo' ? 'fas fa-play mr-2' : 'fas fa-bolt mr-2'
}),
selectedType === 'demo' ? 'Start Demo Session' : 'Continue to Payment'
]),
React.createElement('button', {
key: 'cancel',
onClick: onCancel,
className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-all'
}, 'Cancel'),
])
]);
};
window.SessionTypeSelector = SessionTypeSelector;

View File

@@ -1,527 +0,0 @@
// ============================================
// TOKEN AUTHENTICATION MODAL
// ============================================
// Модальное окно для авторизации через Web3 токены
// Поддерживает покупку, проверку и управление токенами
// Enhanced with complete ASN.1 validation
// ============================================
const TokenAuthModal = ({
isOpen,
onClose,
onAuthenticated,
tokenAuthManager,
web3ContractManager
}) => {
const [currentStep, setCurrentStep] = React.useState('connect'); // connect, purchase, authenticate, success
const [walletAddress, setWalletAddress] = React.useState('');
const [isConnecting, setIsConnecting] = React.useState(false);
const [isPurchasing, setIsPurchasing] = React.useState(false);
const [isAuthenticating, setIsAuthenticating] = React.useState(false);
const [selectedTokenType, setSelectedTokenType] = React.useState('monthly');
const [tokenPrices, setTokenPrices] = React.useState(null);
const [userTokens, setUserTokens] = React.useState([]);
const [activeToken, setActiveToken] = React.useState(null);
const [error, setError] = React.useState('');
const [success, setSuccess] = React.useState('');
// Состояния для разных шагов
const [purchaseAmount, setPurchaseAmount] = React.useState('');
const [tokenId, setTokenId] = React.useState('');
React.useEffect(() => {
if (isOpen) {
initializeModal();
}
}, [isOpen]);
// Инициализация модального окна
const initializeModal = async () => {
try {
setCurrentStep('connect');
setError('');
setSuccess('');
// Проверяем статус кошелька
if (tokenAuthManager && tokenAuthManager.walletAddress) {
setWalletAddress(tokenAuthManager.walletAddress);
await checkUserTokens();
setCurrentStep('authenticate');
}
} catch (error) {
console.error('Modal initialization failed:', error);
setError('Failed to initialize authentication');
}
};
// Подключение кошелька
const connectWallet = async () => {
try {
setIsConnecting(true);
setError('');
if (!tokenAuthManager) {
throw new Error('Token auth manager not available');
}
// Инициализируем Web3
await tokenAuthManager.initialize();
if (tokenAuthManager.walletAddress) {
setWalletAddress(tokenAuthManager.walletAddress);
await checkUserTokens();
setCurrentStep('authenticate');
} else {
throw new Error('Failed to connect wallet');
}
} catch (error) {
console.error('Wallet connection failed:', error);
setError(error.message || 'Failed to connect wallet');
} finally {
setIsConnecting(false);
}
};
// Проверка токенов пользователя
const checkUserTokens = async () => {
try {
if (!web3ContractManager || !walletAddress) return;
// Получаем активные токены пользователя
const activeTokens = await web3ContractManager.getActiveUserTokens(walletAddress);
if (activeTokens.length > 0) {
// Получаем информацию о первом активном токене
const tokenInfo = await web3ContractManager.getTokenInfo(activeTokens[0]);
setActiveToken(tokenInfo);
setUserTokens(activeTokens);
}
} catch (error) {
console.error('Failed to check user tokens:', error);
}
};
// Получение цен токенов
const loadTokenPrices = async () => {
try {
if (!web3ContractManager) return;
const prices = await web3ContractManager.getTokenPrices();
setTokenPrices(prices);
} catch (error) {
console.error('Failed to load token prices:', error);
}
};
// Покупка токена
const purchaseToken = async () => {
try {
setIsPurchasing(true);
setError('');
if (!web3ContractManager || !walletAddress) {
throw new Error('Web3 contract manager not available');
}
let result;
if (selectedTokenType === 'monthly') {
result = await web3ContractManager.purchaseMonthlyToken(tokenPrices.monthlyWei);
} else {
result = await web3ContractManager.purchaseYearlyToken(tokenPrices.yearlyWei);
}
// Получаем ID токена из события
const tokenId = result.events.TokenMinted.returnValues.tokenId;
setTokenId(tokenId);
setSuccess(`Token purchased successfully! Token ID: ${tokenId}`);
setCurrentStep('authenticate');
// Обновляем список токенов
await checkUserTokens();
} catch (error) {
console.error('Token purchase failed:', error);
setError(error.message || 'Failed to purchase token');
} finally {
setIsPurchasing(false);
}
};
// Авторизация через токен
const authenticateWithToken = async (tokenId) => {
try {
setIsAuthenticating(true);
setError('');
if (!tokenAuthManager) {
throw new Error('Token auth manager not available');
}
// Определяем тип токена
let tokenType = 'monthly';
if (activeToken) {
tokenType = activeToken.tokenType === 0 ? 'monthly' : 'yearly';
}
// Авторизуемся через токен
const session = await tokenAuthManager.authenticateWithToken(tokenId, tokenType);
setSuccess('Authentication successful!');
setCurrentStep('success');
// Вызываем callback
if (onAuthenticated) {
onAuthenticated(session);
}
} catch (error) {
console.error('Authentication failed:', error);
setError(error.message || 'Failed to authenticate');
} finally {
setIsAuthenticating(false);
}
};
// Переключение на шаг покупки
const goToPurchase = () => {
setCurrentStep('purchase');
loadTokenPrices();
};
// Переключение на шаг авторизации
const goToAuthenticate = () => {
setCurrentStep('authenticate');
};
// Закрытие модального окна
const handleClose = () => {
setCurrentStep('connect');
setError('');
setSuccess('');
setTokenId('');
setActiveToken(null);
onClose();
};
// Форматирование цены
const formatPrice = (price) => {
if (!price) return 'Loading...';
return `${parseFloat(price).toFixed(4)} ETH`;
};
// Форматирование времени истечения
const formatExpiry = (timestamp) => {
if (!timestamp) return 'Unknown';
const date = new Date(timestamp * 1000);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
// Получение названия типа токена
const getTokenTypeName = (type) => {
return type === 0 ? 'Monthly' : 'Yearly';
};
// Рендер шага подключения
const renderConnectStep = () => (
<div className="text-center">
<div className="mb-6">
<i className="fas fa-wallet text-4xl text-blue-500 mb-4"></i>
<h3 className="text-xl font-semibold mb-2">Connect Your Wallet</h3>
<p className="text-gray-600">Connect your MetaMask or other Web3 wallet to continue</p>
</div>
<button
onClick={connectWallet}
disabled={isConnecting}
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
{isConnecting ? (
<>
<i className="fas fa-spinner fa-spin mr-2"></i>
Connecting...
</>
) : (
<>
<i className="fas fa-wallet mr-2"></i>
Connect Wallet
</>
)}
</button>
{error && (
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
{error}
</div>
)}
</div>
);
// Рендер шага покупки
const renderPurchaseStep = () => (
<div>
<div className="mb-6">
<h3 className="text-xl font-semibold mb-2">Purchase Access Token</h3>
<p className="text-gray-600">Choose your subscription plan</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
selectedTokenType === 'monthly'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedTokenType('monthly')}
>
<div className="text-center">
<i className="fas fa-calendar-alt text-2xl text-blue-500 mb-2"></i>
<h4 className="font-semibold">Monthly Plan</h4>
<p className="text-2xl font-bold text-blue-600">
{formatPrice(tokenPrices?.monthly)}
</p>
<p className="text-sm text-gray-600">30 days access</p>
</div>
</div>
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
selectedTokenType === 'yearly'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedTokenType('yearly')}
>
<div className="text-center">
<i className="fas fa-calendar text-2xl text-green-500 mb-2"></i>
<h4 className="font-semibold">Yearly Plan</h4>
<p className="text-2xl font-bold text-green-600">
{formatPrice(tokenPrices?.yearly)}
</p>
<p className="text-sm text-gray-600">365 days access</p>
<div className="mt-2">
<span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full">
Save 17%
</span>
</div>
</div>
</div>
</div>
<div className="flex justify-between items-center">
<button
onClick={() => setCurrentStep('connect')}
className="text-gray-600 hover:text-gray-800 transition-colors"
>
<i className="fas fa-arrow-left mr-2"></i>
Back
</button>
<button
onClick={purchaseToken}
disabled={isPurchasing || !tokenPrices}
className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
{isPurchasing ? (
<>
<i className="fas fa-spinner fa-spin mr-2"></i>
Purchasing...
</>
) : (
<>
<i className="fas fa-credit-card mr-2"></i>
Purchase Token
</>
)}
</button>
</div>
{error && (
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
{error}
</div>
)}
</div>
);
// Рендер шага авторизации
const renderAuthenticateStep = () => (
<div>
<div className="mb-6">
<h3 className="text-xl font-semibold mb-2">Authenticate with Token</h3>
<p className="text-gray-600">Use your access token to authenticate</p>
</div>
{activeToken ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div className="flex items-center mb-2">
<i className="fas fa-check-circle text-green-500 mr-2"></i>
<span className="font-semibold text-green-800">Active Token Found</span>
</div>
<div className="text-sm text-green-700">
<p><strong>Token ID:</strong> {activeToken.tokenId}</p>
<p><strong>Type:</strong> {getTokenTypeName(activeToken.tokenType)}</p>
<p><strong>Expires:</strong> {formatExpiry(activeToken.expiryDate)}</p>
</div>
</div>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="flex items-center mb-2">
<i className="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>
<span className="font-semibold text-yellow-800">No Active Token</span>
</div>
<p className="text-sm text-yellow-700">
You don't have an active access token. Please purchase one first.
</p>
</div>
)}
{tokenId && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div className="flex items-center mb-2">
<i className="fas fa-info-circle text-blue-500 mr-2"></i>
<span className="font-semibold text-blue-800">New Token Purchased</span>
</div>
<p className="text-sm text-blue-700">
<strong>Token ID:</strong> {tokenId}
</p>
</div>
)}
<div className="space-y-3">
{activeToken && (
<button
onClick={() => authenticateWithToken(activeToken.tokenId)}
disabled={isAuthenticating}
className="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
{isAuthenticating ? (
<>
<i className="fas fa-spinner fa-spin mr-2"></i>
Authenticating...
</>
) : (
<>
<i className="fas fa-sign-in-alt mr-2"></i>
Authenticate with Active Token
</>
)}
</button>
)}
{tokenId && (
<button
onClick={() => authenticateWithToken(tokenId)}
disabled={isAuthenticating}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
{isAuthenticating ? (
<>
<i className="fas fa-spinner fa-spin mr-2"></i>
Authenticating...
</>
) : (
<>
<i className="fas fa-sign-in-alt mr-2"></i>
Authenticate with New Token
</>
)}
</button>
)}
<button
onClick={goToPurchase}
className="w-full bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
<i className="fas fa-plus mr-2"></i>
Purchase New Token
</button>
</div>
{error && (
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
{error}
</div>
)}
{success && (
<div className="mt-4 p-3 bg-green-100 border border-green-300 text-green-700 rounded-lg">
{success}
</div>
)}
</div>
);
// Рендер шага успеха
const renderSuccessStep = () => (
<div className="text-center">
<div className="mb-6">
<i className="fas fa-check-circle text-6xl text-green-500 mb-4"></i>
<h3 className="text-xl font-semibold mb-2">Authentication Successful!</h3>
<p className="text-gray-600">You are now authenticated and can access the service</p>
</div>
<button
onClick={handleClose}
className="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
<i className="fas fa-check mr-2"></i>
Continue
</button>
</div>
);
// Рендер основного контента
const renderContent = () => {
switch (currentStep) {
case 'connect':
return renderConnectStep();
case 'purchase':
return renderPurchaseStep();
case 'authenticate':
return renderAuthenticateStep();
case 'success':
return renderSuccessStep();
default:
return renderConnectStep();
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-xl font-semibold">Token Authentication</h2>
<button
onClick={handleClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<i className="fas fa-times text-xl"></i>
</button>
</div>
{/* Content */}
<div className="p-6">
{renderContent()}
</div>
{/* Footer */}
<div className="p-6 border-t bg-gray-50">
<div className="text-center text-sm text-gray-600">
<p>Secure authentication powered by Web3</p>
<p className="mt-1">Your wallet address: {walletAddress ? `${walletAddress.substring(0, 6)}...${walletAddress.substring(38)}` : 'Not connected'}</p>
</div>
</div>
</div>
</div>
);
};
export default TokenAuthModal;

View File

@@ -1,290 +0,0 @@
// ============================================
// TOKEN STATUS COMPONENT
// ============================================
// Компонент для отображения статуса токена доступа
// Показывает информацию о текущем токене и времени до истечения
// ============================================
const TokenStatus = ({
tokenAuthManager,
web3ContractManager,
onShowTokenModal
}) => {
const [tokenInfo, setTokenInfo] = React.useState(null);
const [timeLeft, setTimeLeft] = React.useState('');
const [isExpired, setIsExpired] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);
const [updateInterval, setUpdateInterval] = React.useState(null);
React.useEffect(() => {
if (tokenAuthManager) {
loadTokenStatus();
startUpdateTimer();
}
return () => {
if (updateInterval) {
clearInterval(updateInterval);
}
};
}, [tokenAuthManager]);
// Загрузка статуса токена
const loadTokenStatus = async () => {
try {
setIsLoading(true);
if (!tokenAuthManager || !tokenAuthManager.isAuthenticated()) {
setTokenInfo(null);
setTimeLeft('');
setIsExpired(false);
return;
}
const session = tokenAuthManager.getCurrentSession();
if (!session) {
setTokenInfo(null);
return;
}
// Получаем информацию о токене
const info = tokenAuthManager.getTokenInfo();
setTokenInfo(info);
// Проверяем, не истек ли токен
const now = Date.now();
const expiresAt = info.expiresAt;
const timeRemaining = expiresAt - now;
if (timeRemaining <= 0) {
setIsExpired(true);
setTimeLeft('Expired');
} else {
setIsExpired(false);
updateTimeLeft(timeRemaining);
}
} catch (error) {
console.error('Failed to load token status:', error);
setTokenInfo(null);
} finally {
setIsLoading(false);
}
};
// Запуск таймера обновления
const startUpdateTimer = () => {
const interval = setInterval(() => {
if (tokenInfo && !isExpired) {
const now = Date.now();
const expiresAt = tokenInfo.expiresAt;
const timeRemaining = expiresAt - now;
if (timeRemaining <= 0) {
setIsExpired(true);
setTimeLeft('Expired');
// Уведомляем о истечении токена
handleTokenExpired();
} else {
updateTimeLeft(timeRemaining);
}
}
}, 1000); // Обновляем каждую секунду
setUpdateInterval(interval);
};
// Обновление оставшегося времени
const updateTimeLeft = (timeRemaining) => {
const days = Math.floor(timeRemaining / (24 * 60 * 60 * 1000));
const hours = Math.floor((timeRemaining % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
const minutes = Math.floor((timeRemaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((timeRemaining % (60 * 1000)) / 1000);
let timeString = '';
if (days > 0) {
timeString = `${days}d ${hours}h`;
} else if (hours > 0) {
timeString = `${hours}h ${minutes}m`;
} else if (minutes > 0) {
timeString = `${minutes}m ${seconds}s`;
} else {
timeString = `${seconds}s`;
}
setTimeLeft(timeString);
};
// Обработка истечения токена
const handleTokenExpired = () => {
// Показываем уведомление
showExpiredNotification();
// Можно также автоматически открыть модальное окно для покупки нового токена
// if (onShowTokenModal) {
// setTimeout(() => onShowTokenModal(), 2000);
// }
};
// Показ уведомления об истечении
const showExpiredNotification = () => {
// Создаем уведомление в браузере
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('SecureBit Token Expired', {
body: 'Your access token has expired. Please purchase a new one to continue.',
icon: '/logo/icon-192x192.png',
tag: 'token-expired'
});
}
// Показываем toast уведомление
showToast('Token expired', 'Your access token has expired. Please purchase a new one.', 'warning');
};
// Показ toast уведомления
const showToast = (title, message, type = 'info') => {
// Создаем toast элемент
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${
type === 'warning' ? 'bg-yellow-500 text-white' :
type === 'error' ? 'bg-red-500 text-white' :
type === 'success' ? 'bg-green-500 text-white' :
'bg-blue-500 text-white'
}`;
toast.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0">
<i class="fas fa-${type === 'warning' ? 'exclamation-triangle' :
type === 'error' ? 'times-circle' :
type === 'success' ? 'check-circle' : 'info-circle'}"></i>
</div>
<div class="ml-3 flex-1">
<p class="font-medium">${title}</p>
<p class="text-sm opacity-90">${message}</p>
</div>
<button class="ml-4 text-white opacity-70 hover:opacity-100" onclick="this.parentElement.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
`;
document.body.appendChild(toast);
// Автоматически удаляем через 5 секунд
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 5000);
};
// Получение названия типа токена
const getTokenTypeName = (type) => {
return type === 'monthly' ? 'Monthly' : 'Yearly';
};
// Получение иконки типа токена
const getTokenTypeIcon = (type) => {
return type === 'monthly' ? 'fa-calendar-alt' : 'fa-calendar';
};
// Получение цвета для типа токена
const getTokenTypeColor = (type) => {
return type === 'monthly' ? 'text-blue-500' : 'text-green-500';
};
// Получение цвета для статуса
const getStatusColor = () => {
if (isExpired) return 'text-red-500';
if (tokenInfo && tokenInfo.timeLeft < 24 * 60 * 60 * 1000) return 'text-yellow-500'; // Меньше дня
return 'text-green-500';
};
// Получение иконки статуса
const getStatusIcon = () => {
if (isExpired) return 'fa-times-circle';
if (tokenInfo && tokenInfo.timeLeft < 24 * 60 * 60 * 1000) return 'fa-exclamation-triangle';
return 'fa-check-circle';
};
// Если токен не загружен или не авторизован
if (isLoading) {
return (
<div className="flex items-center space-x-2 px-3 py-2 bg-gray-100 rounded-lg">
<i className="fas fa-spinner fa-spin text-gray-400"></i>
<span className="text-sm text-gray-500">Loading token...</span>
</div>
);
}
if (!tokenInfo) {
return (
<button
onClick={onShowTokenModal}
className="flex items-center space-x-2 px-3 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
>
<i className="fas fa-key"></i>
<span className="text-sm font-medium">Connect Token</span>
</button>
);
}
// Если токен истек
if (isExpired) {
return (
<button
onClick={onShowTokenModal}
className="flex items-center space-x-2 px-3 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
>
<i className="fas fa-exclamation-triangle"></i>
<span className="text-sm font-medium">Token Expired</span>
<i className="fas fa-arrow-right text-xs"></i>
</button>
);
}
// Отображение активного токена
return (
<div className="flex items-center space-x-3">
{/* Статус токена */}
<div className="flex items-center space-x-2 px-3 py-2 bg-green-100 rounded-lg">
<i className={`fas ${getStatusIcon()} ${getStatusColor()}`}></i>
<div className="text-sm">
<div className="font-medium text-gray-800">
{getTokenTypeName(tokenInfo.tokenType)} Token
</div>
<div className={`text-xs ${getStatusColor()}`}>
{timeLeft} left
</div>
</div>
</div>
{/* Информация о токене */}
<div className="hidden md:flex items-center space-x-2 px-3 py-2 bg-gray-100 rounded-lg">
<i className={`fas ${getTokenTypeIcon(tokenInfo.tokenType)} ${getTokenTypeColor(tokenInfo.tokenType)}`}></i>
<div className="text-sm">
<div className="text-gray-800">
ID: {tokenInfo.tokenId}
</div>
<div className="text-xs text-gray-500">
{getTokenTypeName(tokenInfo.tokenType)}
</div>
</div>
</div>
{/* Кнопка управления */}
<button
onClick={onShowTokenModal}
className="flex items-center space-x-2 px-3 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
title="Manage token"
>
<i className="fas fa-cog"></i>
<span className="hidden sm:inline text-sm font-medium">Manage</span>
</button>
</div>
);
};
export default TokenStatus;