First commit - all files added

This commit is contained in:
aegisinvestment
2025-08-11 20:52:14 -04:00
commit f07e8400cf
30 changed files with 10155 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
const React = window.React;
const EnhancedMinimalHeader = ({
status,
fingerprint,
verificationCode,
onDisconnect,
isConnected,
securityLevel,
sessionManager,
sessionTimeLeft
}) => {
const getStatusConfig = () => {
switch (status) {
case 'connected':
return {
text: 'Подключено',
className: 'status-connected',
badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20'
};
case 'verifying':
return {
text: 'Верификация...',
className: 'status-verifying',
badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20'
};
case 'connecting':
return {
text: 'Подключение...',
className: 'status-connecting',
badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
};
case 'retrying':
return {
text: 'Переподключение...',
className: 'status-connecting',
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
};
case 'failed':
return {
text: 'Ошибка',
className: 'status-failed',
badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20'
};
case 'reconnecting':
return {
text: 'Переподключение...',
className: 'status-connecting',
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
};
case 'peer_disconnected':
return {
text: 'Собеседник отключился',
className: 'status-failed',
badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20'
};
default:
return {
text: 'Не подключен',
className: 'status-disconnected',
badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20'
};
}
};
const config = getStatusConfig();
const handleSecurityClick = () => {
if (securityLevel?.verificationResults) {
console.log('Security verification results:', securityLevel.verificationResults);
alert('Детали проверки безопасности:\n\n' +
Object.entries(securityLevel.verificationResults)
.map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`)
.join('\n')
);
}
};
return React.createElement('header', {
className: 'header-minimal sticky top-0 z-50'
}, [
React.createElement('div', {
key: 'container',
className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
}, [
React.createElement('div', {
key: 'content',
className: 'flex items-center justify-between h-16'
}, [
// Logo and Title - Mobile Responsive
React.createElement('div', {
key: 'logo-section',
className: 'flex items-center space-x-2 sm:space-x-3'
}, [
React.createElement('div', {
key: 'logo',
className: 'icon-container w-8 h-8 sm:w-10 sm:h-10'
}, [
React.createElement('i', {
className: 'fas fa-shield-halved accent-orange text-sm sm:text-base'
})
]),
React.createElement('div', {
key: 'title-section'
}, [
React.createElement('h1', {
key: 'title',
className: 'text-lg sm:text-xl font-semibold text-primary'
}, 'LockBit.chat'),
React.createElement('p', {
key: 'subtitle',
className: 'text-xs sm:text-sm text-muted hidden sm:block'
}, 'End-to-end freedom')
])
]),
// Status and Controls - Mobile Responsive
React.createElement('div', {
key: 'status-section',
className: 'flex items-center space-x-2 sm:space-x-3'
}, [
// Session Timer - показывать если есть активная сессия
(() => {
const hasActive = sessionManager?.hasActiveSession();
const hasTimer = !!window.SessionTimer;
console.log('Header SessionTimer check:', {
hasActive,
hasTimer,
sessionTimeLeft,
sessionType: sessionManager?.currentSession?.type
});
return hasActive && hasTimer && React.createElement(window.SessionTimer, {
key: 'session-timer',
timeLeft: sessionTimeLeft,
sessionType: sessionManager.currentSession?.type || 'unknown'
});
})(),
// Security Level Indicator - Hidden on mobile, shown on tablet+ (Clickable)
securityLevel && React.createElement('div', {
key: 'security-level',
className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200',
onClick: handleSecurityClick,
title: 'Нажмите для просмотра деталей безопасности'
}, [
React.createElement('div', {
key: 'security-icon',
className: `w-6 h-6 rounded-full flex items-center justify-center ${
securityLevel.color === 'green' ? 'bg-green-500/20' :
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
}`
}, [
React.createElement('i', {
className: `fas fa-shield-alt text-xs ${
securityLevel.color === 'green' ? 'text-green-400' :
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
}`
})
]),
React.createElement('div', {
key: 'security-info',
className: 'flex flex-col'
}, [
React.createElement('div', {
key: 'security-level-text',
className: 'text-xs font-medium text-primary'
}, `${securityLevel.level} (${securityLevel.score}%)`),
securityLevel.details && React.createElement('div', {
key: 'security-details',
className: 'text-xs text-muted mt-1 hidden lg:block'
}, securityLevel.details),
React.createElement('div', {
key: 'security-progress',
className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden'
}, [
React.createElement('div', {
key: 'progress-bar',
className: `h-full transition-all duration-500 ${
securityLevel.color === 'green' ? 'bg-green-400' :
securityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
}`,
style: { width: `${securityLevel.score}%` }
})
])
])
]),
// Mobile Security Indicator - Only icon on mobile (Clickable)
securityLevel && React.createElement('div', {
key: 'mobile-security',
className: 'md:hidden flex items-center'
}, [
React.createElement('div', {
key: 'mobile-security-icon',
className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 ${
securityLevel.color === 'green' ? 'bg-green-500/20' :
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
}`,
title: `${securityLevel.level} (${securityLevel.score}%) - Нажмите для деталей`,
onClick: handleSecurityClick
}, [
React.createElement('i', {
className: `fas fa-shield-alt text-sm ${
securityLevel.color === 'green' ? 'text-green-400' :
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
}`
})
])
]),
// Status Badge - Compact on mobile
React.createElement('div', {
key: 'status-badge',
className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
}, [
React.createElement('span', {
key: 'status-dot',
className: `status-dot ${config.className}`
}),
React.createElement('span', {
key: 'status-text',
className: 'text-xs sm:text-sm font-medium'
}, config.text)
]),
// Disconnect Button - Icon only on mobile
isConnected && React.createElement('button', {
key: 'disconnect-btn',
onClick: onDisconnect,
className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm'
}, [
React.createElement('i', {
key: 'disconnect-icon',
className: 'fas fa-power-off sm:mr-2'
}),
React.createElement('span', {
key: 'disconnect-text',
className: 'hidden sm:inline'
}, 'Отключить')
])
])
])
])
]);
};
window.EnhancedMinimalHeader = EnhancedMinimalHeader;

View File

@@ -0,0 +1,392 @@
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 {
console.log('Creating Lightning invoice for', sessionType);
console.log('Payment manager available:', !!paymentManager);
if (!paymentManager) {
throw new Error('Payment manager not available. Please check sessionManager initialization.');
}
// Создаем инвойс через paymentManager
const createdInvoice = await paymentManager.createLightningInvoice(sessionType);
if (!createdInvoice) {
throw new Error('Failed to create invoice');
}
setInvoice(createdInvoice);
setPaymentStatus('created');
// Создаем QR код
if (createdInvoice.paymentRequest) {
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`;
setQrCodeUrl(qrUrl);
}
console.log('Invoice created successfully:', createdInvoice);
} catch (err) {
console.error('Invoice creation failed:', err);
setError(`Ошибка создания инвойса: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleWebLNPayment = async () => {
if (!window.webln) {
setError('WebLN не поддерживается. Используйте кошелек Alby или Zeus');
return;
}
if (!invoice || !invoice.paymentRequest) {
setError('Инвойс не готов для оплаты');
return;
}
setIsProcessing(true);
setError('');
try {
console.log('Enabling WebLN...');
await window.webln.enable();
console.log('Sending WebLN payment...');
const result = await window.webln.sendPayment(invoice.paymentRequest);
if (result.preimage) {
console.log('WebLN payment successful, preimage:', result.preimage);
setPaymentStatus('paid');
// Активируем сессию
await activateSession(result.preimage);
} else {
setError('Платеж не содержит preimage');
}
} catch (err) {
console.error('WebLN payment failed:', err);
setError(`Ошибка WebLN: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handleManualVerification = async () => {
const trimmedPreimage = preimage.trim();
if (!trimmedPreimage) {
setError('Введите preimage платежа');
return;
}
if (trimmedPreimage.length !== 64) {
setError('Preimage должен содержать ровно 64 символа');
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
setError('Preimage должен содержать только шестнадцатеричные символы (0-9, a-f, A-F)');
return;
}
if (trimmedPreimage === '1'.repeat(64) ||
trimmedPreimage === 'a'.repeat(64) ||
trimmedPreimage === 'f'.repeat(64)) {
setError('Введенный preimage слишком простой. Проверьте правильность ключа.');
return;
}
setError('');
setIsProcessing(true);
try {
await activateSession(trimmedPreimage);
} catch (err) {
setError(`Ошибка активации: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const activateSession = async (preimageValue) => {
try {
console.log('🚀 Activating session with preimage:', preimageValue);
console.log('Payment manager available:', !!paymentManager);
console.log('Invoice available:', !!invoice);
let result;
if (paymentManager) {
const paymentHash = invoice?.paymentHash || 'dummy_hash';
console.log('Using payment hash:', paymentHash);
result = await paymentManager.safeActivateSession(sessionType, preimageValue, paymentHash);
} else {
console.warn('Payment manager not available, using fallback');
// Fallback если paymentManager недоступен
result = { success: true, method: 'fallback' };
}
if (result.success) {
console.log('✅ Session activated successfully:', result);
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(`Ошибка активации бесплатной сессии: ${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' }, [
// Header
React.createElement('div', { key: 'header', className: 'text-center' }, [
React.createElement('h3', {
key: 'title',
className: 'text-xl font-semibold text-white mb-2'
}, sessionType === 'free' ? 'Бесплатная сессия' : 'Оплата Lightning'),
React.createElement('div', {
key: 'amount',
className: 'text-2xl font-bold text-orange-400'
}, sessionType === 'free'
? '1 сат за 1 минуту'
: `${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' }),
'Создание инвойса...'
])
]),
// 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'
}, 'Будет активирована бесплатная сессия на 1 минуту.'),
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 ? 'Активация...' : 'Начать бесплатную сессию'
])
]),
// 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'
}, 'Сканируйте QR код любым Lightning кошельком')
]),
// 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 кошелек (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 ? 'Обработка...' : 'Оплатить через WebLN'
])
]),
// Manual Payment
React.createElement('div', {
key: 'divider',
className: 'text-center text-gray-400'
}, 'или'),
React.createElement('div', {
key: 'manual-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'manual-title',
className: 'text-white font-medium'
}, 'Ручная проверка платежа'),
React.createElement('input', {
key: 'preimage-input',
type: 'text',
value: preimage,
onChange: (e) => setPreimage(e.target.value),
placeholder: 'Введите preimage после оплаты...',
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 ? 'Проверка...' : 'Подтвердить платеж'
])
])
]),
// 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' }, 'Платеж подтвержден!'),
React.createElement('div', { key: 'success-subtext', className: 'text-green-400 text-sm' }, 'Сессия активирована')
]),
// 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('инвойса') && React.createElement('button', {
key: 'retry-btn',
onClick: createInvoice,
className: 'ml-2 text-orange-400 hover:text-orange-300 underline'
}, 'Попробовать снова')
]),
// 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'
}, 'Отмена')
]);
};
window.LightningPayment = IntegratedLightningPayment;

View File

@@ -0,0 +1,91 @@
const React = window.React;
const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPassword }) => {
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (password.trim()) {
onSubmit(password.trim());
setPassword('');
}
};
const getActionText = () => {
return action === 'offer' ? 'приглашения' : 'ответа';
};
return React.createElement('div', {
className: 'fixed inset-0 bg-black/50 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-md w-full border-purple-500/20'
}, [
React.createElement('div', {
key: 'header',
className: 'flex items-center mb-4'
}, [
React.createElement('div', {
key: 'icon',
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
}, [
React.createElement('i', {
className: 'fas fa-key accent-purple'
})
]),
React.createElement('h3', {
key: 'title',
className: 'text-lg font-medium text-primary'
}, 'Ввод пароля')
]),
React.createElement('form', {
key: 'form',
onSubmit: handleSubmit,
className: 'space-y-4'
}, [
React.createElement('p', {
key: 'description',
className: 'text-secondary text-sm'
}, `Введите пароль для расшифровки ${getActionText()}:`),
React.createElement('input', {
key: 'password-input',
type: 'password',
value: password,
onChange: (e) => setPassword(e.target.value),
placeholder: 'Введите пароль...',
className: 'w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg text-primary placeholder-gray-500 focus:border-purple-500/40 focus:outline-none transition-all',
autoFocus: true
}),
React.createElement('div', {
key: 'buttons',
className: 'flex space-x-3'
}, [
React.createElement('button', {
key: 'submit',
type: 'submit',
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
className: 'fas fa-unlock-alt mr-2'
}),
'Расшифровать'
]),
React.createElement('button', {
key: 'cancel',
type: 'button',
onClick: onClose,
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
className: 'fas fa-times mr-2'
}),
'Отмена'
])
])
])
])
]);
};
window.PasswordModal = PasswordModal;

View File

@@ -0,0 +1,574 @@
const React = window.React;
const { useState, useEffect, useRef } = React;
const PaymentModal = ({ isOpen, onClose, sessionManager, onSessionPurchased }) => {
const [step, setStep] = useState('select');
const [selectedType, setSelectedType] = useState(null);
const [invoice, setInvoice] = useState(null);
const [paymentStatus, setPaymentStatus] = useState('pending'); // pending, creating, created, paying, paid, failed, expired
const [error, setError] = useState('');
const [paymentMethod, setPaymentMethod] = useState('webln'); // webln, manual, qr
const [preimageInput, setPreimageInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [qrCodeUrl, setQrCodeUrl] = useState('');
const [paymentTimer, setPaymentTimer] = useState(null);
const [timeLeft, setTimeLeft] = useState(0);
const pollInterval = useRef(null);
// Cleanup на закрытие
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);
};
const handleSelectType = async (type) => {
setSelectedType(type);
setError('');
if (type === 'free') {
// Для бесплатной сессии создаем фиктивный инвойс
setInvoice({
sessionType: 'free',
amount: 1,
paymentHash: '0'.repeat(64),
memo: 'Free session (1 minute)',
createdAt: Date.now()
});
setPaymentStatus('free');
} else {
await createRealInvoice(type);
}
setStep('payment');
};
const createRealInvoice = async (type) => {
setPaymentStatus('creating');
setIsProcessing(true);
setError('');
try {
console.log(`Creating real Lightning invoice for ${type} session...`);
if (!sessionManager) {
throw new Error('Session manager не инициализирован');
}
// Создаем реальный Lightning инвойс через LNbits
const createdInvoice = await sessionManager.createLightningInvoice(type);
if (!createdInvoice || !createdInvoice.paymentRequest) {
throw new Error('Не удалось создать Lightning инвойс');
}
setInvoice(createdInvoice);
setPaymentStatus('created');
// Создаем QR код для инвойса
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`;
setQrCodeUrl(qrUrl);
// Запускаем таймер на 15 минут
const expirationTime = 15 * 60 * 1000; // 15 минут
setTimeLeft(expirationTime);
const timer = setInterval(() => {
setTimeLeft(prev => {
const newTime = prev - 1000;
if (newTime <= 0) {
clearInterval(timer);
setPaymentStatus('expired');
setError('Время для оплаты истекло. Создайте новый инвойс.');
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(`Ошибка создания инвойса: ${err.message}`);
setPaymentStatus('failed');
} finally {
setIsProcessing(false);
}
};
// Автопроверка статуса платежа каждые 3 секунды
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) {
console.log('✅ Payment confirmed automatically!', status);
clearInterval(pollInterval.current);
setPaymentStatus('paid');
await handlePaymentSuccess(status.preimage);
}
} catch (error) {
console.warn('Payment status check failed:', error);
// Продолжаем проверять, не останавливаем polling из-за одной ошибки
}
}, 3000); // Проверяем каждые 3 секунды
};
const handleWebLNPayment = async () => {
if (!window.webln) {
setError('WebLN не поддерживается. Установите кошелек Alby или Zeus');
return;
}
if (!invoice || !invoice.paymentRequest) {
setError('Инвойс не готов для оплаты');
return;
}
setIsProcessing(true);
setError('');
setPaymentStatus('paying');
try {
console.log('🔌 Enabling WebLN...');
await window.webln.enable();
console.log('💰 Sending WebLN payment...');
const result = await window.webln.sendPayment(invoice.paymentRequest);
if (result.preimage) {
console.log('✅ WebLN payment successful!', result);
setPaymentStatus('paid');
await handlePaymentSuccess(result.preimage);
} else {
throw new Error('Платеж не содержит preimage');
}
} catch (err) {
console.error('❌ WebLN payment failed:', err);
setError(`Ошибка WebLN платежа: ${err.message}`);
setPaymentStatus('created'); // Возвращаем к состоянию "создан"
} finally {
setIsProcessing(false);
}
};
const handleManualVerification = async () => {
const trimmedPreimage = preimageInput.trim();
if (!trimmedPreimage) {
setError('Введите preimage платежа');
return;
}
if (trimmedPreimage.length !== 64) {
setError('Preimage должен содержать ровно 64 символа');
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) {
setError('Preimage должен содержать только шестнадцатеричные символы (0-9, a-f, A-F)');
return;
}
// Проверяем на простые/тестовые preimage
const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64), '0'.repeat(64)];
if (dummyPreimages.includes(trimmedPreimage) && selectedType !== 'free') {
setError('Введенный preimage недействителен. Используйте настоящий preimage от платежа.');
return;
}
setIsProcessing(true);
setError('');
setPaymentStatus('paying');
try {
await handlePaymentSuccess(trimmedPreimage);
} catch (err) {
setError(err.message);
setPaymentStatus('created');
} finally {
setIsProcessing(false);
}
};
const handleFreeSession = async () => {
setIsProcessing(true);
setError('');
try {
await handlePaymentSuccess('0'.repeat(64));
} catch (err) {
setError(`Ошибка активации бесплатной сессии: ${err.message}`);
} finally {
setIsProcessing(false);
}
};
const handlePaymentSuccess = async (preimage) => {
try {
console.log('🔍 Verifying payment...', { selectedType, preimage });
let isValid;
if (selectedType === 'free') {
isValid = true;
} else {
// Верифицируем реальный платеж
isValid = await sessionManager.verifyPayment(preimage, invoice.paymentHash);
}
if (isValid) {
console.log('✅ Payment verified successfully!');
// Останавливаем polling и таймеры
if (pollInterval.current) {
clearInterval(pollInterval.current);
}
if (paymentTimer) {
clearInterval(paymentTimer);
}
// Передаем данные о покупке
onSessionPurchased({
type: selectedType,
preimage,
paymentHash: invoice.paymentHash,
amount: invoice.amount
});
// Закрываем модалку с задержкой для показа успеха
setTimeout(() => {
onClose();
}, 1500);
} else {
throw new Error('Платеж не прошел верификацию. Проверьте правильность preimage или попробуйте снова.');
}
} catch (error) {
console.error('❌ Payment verification failed:', error);
throw error;
}
};
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
// Можно добавить visual feedback
} 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 pricing = sessionManager?.sessionPrices || {
free: { sats: 1, hours: 1/60, usd: 0.00 },
basic: { sats: 500, hours: 1, usd: 0.20 },
premium: { sats: 1000, hours: 4, usd: 0.40 },
extended: { sats: 2000, hours: 24, usd: 0.80 }
};
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'
}, [
// Header с кнопкой закрытия
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' ? 'Выберите тип сессии' : 'Оплата сессии'),
React.createElement('button', {
key: 'close',
onClick: onClose,
className: 'text-gray-400 hover:text-white transition-colors'
}, React.createElement('i', { className: 'fas fa-times' }))
]),
// Step 1: Session Type Selection
step === 'select' && window.SessionTypeSelector && React.createElement(window.SessionTypeSelector, {
key: 'selector',
onSelectType: handleSelectType,
onCancel: onClose
}),
// Step 2: Payment Processing
step === 'payment' && React.createElement('div', {
key: 'payment-step',
className: 'space-y-6'
}, [
// Session Info
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)} сессия`),
React.createElement('div', {
key: 'session-details',
className: 'text-sm text-secondary'
}, [
React.createElement('div', { key: 'amount' }, `${pricing[selectedType].sats} сат за ${pricing[selectedType].hours}ч`),
pricing[selectedType].usd > 0 && React.createElement('div', {
key: 'usd',
className: 'text-gray-400'
}, `$${pricing[selectedType].usd} USD`)
])
]),
// Timer для платных сессий
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'
}, `⏱️ Время на оплату: ${formatTime(timeLeft)}`)
]),
// Бесплатная сессия
paymentStatus === 'free' && React.createElement('div', {
key: 'free-payment',
className: 'space-y-4'
}, [
React.createElement('div', {
key: 'free-info',
className: 'p-4 bg-blue-500/10 border border-blue-500/20 rounded text-blue-300 text-sm text-center'
}, '🎉 Бесплатная сессия на 1 минуту'),
React.createElement('button', {
key: 'free-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 disabled:cursor-not-allowed'
}, [
React.createElement('i', {
key: 'free-icon',
className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2`
}),
isProcessing ? 'Активация...' : 'Активировать бесплатную сессию'
])
]),
// Создание инвойса
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' }, 'Создание Lightning инвойса...'),
React.createElement('div', { className: 'text-secondary text-sm mt-1' }, 'Подключение к Lightning Network...')
]),
// Платная сессия с инвойсом
(paymentStatus === 'created' || paymentStatus === 'paying') && invoice && React.createElement('div', {
key: 'payment-methods',
className: 'space-y-6'
}, [
// 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: 'Lightning Payment QR Code',
className: 'w-48 h-48'
})
]),
React.createElement('div', {
key: 'qr-hint',
className: 'text-xs text-gray-400 mt-2'
}, 'Сканируйте любым Lightning кошельком')
]),
// Payment Request для копирования
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: 'Нажмите для копирования'
}, [
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 кошелек (рекомендуется)'
]),
React.createElement('div', {
key: 'webln-info',
className: 'text-xs text-gray-400 mb-2'
}, 'Alby, Zeus, или другие WebLN совместимые кошельки'),
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' ? 'Обработка платежа...' : 'Оплатить через WebLN'
])
]),
// Divider
React.createElement('div', {
key: 'divider',
className: 'text-center text-gray-400 text-sm'
}, '— или —'),
// Manual Verification
React.createElement('div', {
key: 'manual-section',
className: 'space-y-3'
}, [
React.createElement('h4', {
key: 'manual-title',
className: 'text-primary font-medium'
}, 'Ручное подтверждение платежа'),
React.createElement('div', {
key: 'manual-info',
className: 'text-xs text-gray-400'
}, 'Оплатите инвойс в любом кошельке и введите preimage:'),
React.createElement('input', {
key: 'preimage-input',
type: 'text',
value: preimageInput,
onChange: (e) => setPreimageInput(e.target.value),
placeholder: 'Введите preimage (64 hex символа)...',
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 ? 'Проверка платежа...' : 'Подтвердить платеж'
])
])
]),
// 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' }, '✅ Платеж подтвержден!'),
React.createElement('div', { key: 'success-text', className: 'text-green-400 text-sm' }, 'Сессия будет активирована при подключении к чату')
]),
// 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('инвойса') || paymentStatus === 'failed') && React.createElement('button', {
key: 'retry-btn',
onClick: () => createRealInvoice(selectedType),
className: 'mt-2 text-orange-400 hover:text-orange-300 underline text-sm'
}, 'Создать новый инвойс')
])
])
]),
// Back button (кроме случая успешной оплаты)
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' }),
'Выбрать другую сессию'
])
])
])
])
]);
};
window.PaymentModal = PaymentModal;

View File

@@ -0,0 +1,45 @@
const React = window.React;
const SessionTimer = ({ timeLeft, sessionType }) => {
// Отладочная информация
console.log('SessionTimer render:', { timeLeft, sessionType });
if (!timeLeft || timeLeft <= 0) {
console.log('SessionTimer: no time left, not rendering');
return null;
}
const totalMinutes = Math.floor(timeLeft / (60 * 1000));
const isWarning = totalMinutes <= 10;
const isCritical = totalMinutes <= 5;
const formatTime = (ms) => {
const hours = Math.floor(ms / (60 * 60 * 1000));
const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((ms % (60 * 1000)) / 1000);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
};
return React.createElement('div', {
className: `session-timer ${isCritical ? 'critical' : isWarning ? 'warning' : ''}`
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-clock'
}),
React.createElement('span', {
key: 'time'
}, formatTime(timeLeft)),
React.createElement('span', {
key: 'type',
className: 'text-xs opacity-80'
}, sessionType?.toUpperCase() || '')
]);
};
window.SessionTimer = SessionTimer;

View File

@@ -0,0 +1,110 @@
const React = window.React;
const SessionTypeSelector = ({ onSelectType, onCancel }) => {
const [selectedType, setSelectedType] = React.useState(null);
const sessionTypes = [
{
id: 'free',
name: 'Бесплатная',
duration: '1 минута',
price: '0 сат',
usd: '$0.00',
popular: true
},
{
id: 'basic',
name: 'Базовая',
duration: '1 час',
price: '500 сат',
usd: '$0.20'
},
{
id: 'premium',
name: 'Премиум',
duration: '4 часа',
price: '1000 сат',
usd: '$0.40',
popular: true
},
{
id: 'extended',
name: 'Расширенная',
duration: '24 часа',
price: '2000 сат',
usd: '$0.80'
}
];
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'
}, 'Выберите тариф'),
React.createElement('p', {
key: 'subtitle',
className: 'text-gray-300 text-sm'
}, 'Оплатите через Lightning Network для доступа к чату')
]),
React.createElement('div', { key: 'types', className: 'space-y-3' },
sessionTypes.map(type =>
React.createElement('div', {
key: type.id,
onClick: () => setSelectedType(type.id),
className: `card-minimal rounded-lg p-4 cursor-pointer border-2 transition-all ${
selectedType === type.id ? 'border-orange-500 bg-orange-500/10' : 'border-gray-600 hover:border-orange-400'
} ${type.popular ? 'relative' : ''}`
}, [
type.popular && React.createElement('div', {
key: 'badge',
className: 'absolute -top-2 right-3 bg-orange-500 text-white text-xs px-2 py-1 rounded-full'
}, 'Популярный'),
React.createElement('div', { key: 'content', className: 'flex items-center justify-between' }, [
React.createElement('div', { key: 'info' }, [
React.createElement('h4', {
key: 'name',
className: 'text-lg font-semibold text-white'
}, type.name),
React.createElement('p', {
key: 'duration',
className: 'text-gray-300 text-sm'
}, type.duration)
]),
React.createElement('div', { key: 'pricing', className: 'text-right' }, [
React.createElement('div', {
key: 'sats',
className: 'text-lg font-bold text-orange-400'
}, type.price),
React.createElement('div', {
key: 'usd',
className: 'text-xs text-gray-400'
}, type.usd)
])
])
])
)
),
React.createElement('div', { key: 'buttons', className: 'flex space-x-3' }, [
React.createElement('button', {
key: 'continue',
onClick: () => selectedType && onSelectType(selectedType),
disabled: !selectedType,
className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50'
}, [
React.createElement('i', { className: 'fas fa-bolt mr-2' }),
'Продолжить к оплате'
]),
React.createElement('button', {
key: 'cancel',
onClick: onCancel,
className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg'
}, 'Отмена')
])
]);
};
window.SessionTypeSelector = SessionTypeSelector;