First commit - all files added
This commit is contained in:
248
src/components/ui/Header.jsx
Normal file
248
src/components/ui/Header.jsx
Normal 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;
|
||||
392
src/components/ui/LightningPayment.jsx
Normal file
392
src/components/ui/LightningPayment.jsx
Normal 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;
|
||||
91
src/components/ui/PasswordModal.jsx
Normal file
91
src/components/ui/PasswordModal.jsx
Normal 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;
|
||||
574
src/components/ui/PaymentModal.jsx
Normal file
574
src/components/ui/PaymentModal.jsx
Normal 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;
|
||||
45
src/components/ui/SessionTimer.jsx
Normal file
45
src/components/ui/SessionTimer.jsx
Normal 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;
|
||||
110
src/components/ui/SessionTypeSelector.jsx
Normal file
110
src/components/ui/SessionTypeSelector.jsx
Normal 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;
|
||||
1883
src/crypto/EnhancedSecureCryptoUtils.js
Normal file
1883
src/crypto/EnhancedSecureCryptoUtils.js
Normal file
File diff suppressed because it is too large
Load Diff
0
src/main.js
Normal file
0
src/main.js
Normal file
1448
src/network/EnhancedSecureWebRTCManager.js
Normal file
1448
src/network/EnhancedSecureWebRTCManager.js
Normal file
File diff suppressed because it is too large
Load Diff
588
src/session/PayPerSessionManager.js
Normal file
588
src/session/PayPerSessionManager.js
Normal file
@@ -0,0 +1,588 @@
|
||||
class PayPerSessionManager {
|
||||
constructor(config = {}) {
|
||||
this.sessionPrices = {
|
||||
free: { sats: 0, 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 }
|
||||
};
|
||||
this.currentSession = null;
|
||||
this.sessionTimer = null;
|
||||
this.onSessionExpired = null;
|
||||
this.staticLightningAddress = "dullpastry62@walletofsatoshi.com";
|
||||
|
||||
// Конфигурация для LNbits (ваши реальные данные)
|
||||
this.verificationConfig = {
|
||||
method: config.method || 'lnbits',
|
||||
apiUrl: config.apiUrl || 'https://demo.lnbits.com',
|
||||
apiKey: config.apiKey || '623515641d2e4ebcb1d5992d6d78419c', // Ваш Invoice/read ключ
|
||||
walletId: config.walletId || 'bcd00f561c7b46b4a7b118f069e68997',
|
||||
// Дополнительные настройки для демо
|
||||
isDemo: true,
|
||||
demoTimeout: 30000, // 30 секунд для демо
|
||||
retryAttempts: 3
|
||||
};
|
||||
}
|
||||
|
||||
hasActiveSession() {
|
||||
if (!this.currentSession) return false;
|
||||
return Date.now() < this.currentSession.expiresAt;
|
||||
}
|
||||
|
||||
createInvoice(sessionType) {
|
||||
const pricing = this.sessionPrices[sessionType];
|
||||
if (!pricing) throw new Error('Invalid session type');
|
||||
|
||||
return {
|
||||
amount: pricing.sats,
|
||||
memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`,
|
||||
sessionType: sessionType,
|
||||
timestamp: Date.now(),
|
||||
paymentHash: Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join(''),
|
||||
lightningAddress: this.staticLightningAddress
|
||||
};
|
||||
}
|
||||
|
||||
// Создание реального Lightning инвойса через LNbits
|
||||
async createLightningInvoice(sessionType) {
|
||||
const pricing = this.sessionPrices[sessionType];
|
||||
if (!pricing) throw new Error('Invalid session type');
|
||||
|
||||
try {
|
||||
console.log(`Creating ${sessionType} invoice for ${pricing.sats} sats...`);
|
||||
|
||||
// Проверяем доступность API
|
||||
const healthCheck = await fetch(`${this.verificationConfig.apiUrl}/api/v1/health`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': this.verificationConfig.apiKey
|
||||
}
|
||||
});
|
||||
|
||||
if (!healthCheck.ok) {
|
||||
throw new Error(`LNbits API недоступен: ${healthCheck.status}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': this.verificationConfig.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
out: false, // incoming payment
|
||||
amount: pricing.sats,
|
||||
memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`,
|
||||
unit: 'sat',
|
||||
expiry: this.verificationConfig.isDemo ? 300 : 900 // 5 минут для демо, 15 для продакшена
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('LNbits API response:', errorText);
|
||||
throw new Error(`LNbits API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('✅ Lightning invoice created successfully!', data);
|
||||
|
||||
return {
|
||||
paymentRequest: data.bolt11 || data.payment_request, // BOLT11 invoice для QR кода
|
||||
paymentHash: data.payment_hash,
|
||||
checkingId: data.checking_id || data.payment_hash, // Для проверки статуса
|
||||
amount: data.amount || pricing.sats,
|
||||
sessionType: sessionType,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + (this.verificationConfig.isDemo ? 5 * 60 * 1000 : 15 * 60 * 1000), // 5 минут для демо
|
||||
description: data.description || data.memo || `LockBit.chat ${sessionType} session`,
|
||||
lnurl: data.lnurl || null,
|
||||
memo: data.memo || `LockBit.chat ${sessionType} session`,
|
||||
bolt11: data.bolt11 || data.payment_request,
|
||||
// Дополнительные поля для совместимости
|
||||
payment_request: data.bolt11 || data.payment_request,
|
||||
checking_id: data.checking_id || data.payment_hash
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating Lightning invoice:', error);
|
||||
|
||||
// Для демо режима создаем фиктивный инвойс
|
||||
if (this.verificationConfig.isDemo && error.message.includes('API')) {
|
||||
console.log('🔄 Creating demo invoice for testing...');
|
||||
return this.createDemoInvoice(sessionType);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Создание демо инвойса для тестирования
|
||||
createDemoInvoice(sessionType) {
|
||||
const pricing = this.sessionPrices[sessionType];
|
||||
const demoHash = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return {
|
||||
paymentRequest: `lntb${pricing.sats}1p${demoHash}...`, // Фиктивный BOLT11
|
||||
paymentHash: demoHash,
|
||||
checkingId: demoHash,
|
||||
amount: pricing.sats,
|
||||
sessionType: sessionType,
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + (5 * 60 * 1000), // 5 минут
|
||||
description: `LockBit.chat ${sessionType} session (DEMO)`,
|
||||
isDemo: true
|
||||
};
|
||||
}
|
||||
|
||||
// Проверка статуса платежа через LNbits
|
||||
async checkPaymentStatus(checkingId) {
|
||||
try {
|
||||
console.log(`🔍 Checking payment status for: ${checkingId}`);
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${checkingId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': this.verificationConfig.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Payment status check failed:', errorText);
|
||||
throw new Error(`Payment check failed: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('📊 Payment status response:', data);
|
||||
|
||||
return {
|
||||
paid: data.paid || false,
|
||||
preimage: data.preimage || null,
|
||||
details: data.details || {},
|
||||
amount: data.amount || 0,
|
||||
fee: data.fee || 0,
|
||||
timestamp: data.timestamp || Date.now(),
|
||||
bolt11: data.bolt11 || null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error checking payment status:', error);
|
||||
|
||||
// Для демо режима возвращаем фиктивный статус
|
||||
if (this.verificationConfig.isDemo && error.message.includes('API')) {
|
||||
console.log('🔄 Returning demo payment status...');
|
||||
return {
|
||||
paid: false,
|
||||
preimage: null,
|
||||
details: { demo: true },
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 1: Верификация через LNbits API
|
||||
async verifyPaymentLNbits(preimage, paymentHash) {
|
||||
try {
|
||||
console.log(`🔐 Verifying payment via LNbits: ${paymentHash}`);
|
||||
|
||||
if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) {
|
||||
throw new Error('LNbits API configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${paymentHash}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': this.verificationConfig.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('LNbits verification failed:', errorText);
|
||||
throw new Error(`API request failed: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const paymentData = await response.json();
|
||||
console.log('📋 Payment verification data:', paymentData);
|
||||
|
||||
// Проверяем статус платежа
|
||||
if (paymentData.paid && paymentData.preimage === preimage) {
|
||||
console.log('✅ Payment verified successfully via LNbits');
|
||||
return {
|
||||
verified: true,
|
||||
amount: paymentData.amount,
|
||||
fee: paymentData.fee || 0,
|
||||
timestamp: paymentData.timestamp || Date.now(),
|
||||
method: 'lnbits'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('❌ Payment verification failed: paid=', paymentData.paid, 'preimage match=', paymentData.preimage === preimage);
|
||||
return {
|
||||
verified: false,
|
||||
reason: 'Payment not paid or preimage mismatch',
|
||||
method: 'lnbits'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ LNbits payment verification failed:', error);
|
||||
|
||||
// Для демо режима возвращаем успешную верификацию
|
||||
if (this.verificationConfig.isDemo && error.message.includes('API')) {
|
||||
console.log('🔄 Demo payment verification successful');
|
||||
return {
|
||||
verified: true,
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
timestamp: Date.now(),
|
||||
method: 'demo'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
verified: false,
|
||||
reason: error.message,
|
||||
method: 'lnbits'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 2: Верификация через LND REST API
|
||||
async verifyPaymentLND(preimage, paymentHash) {
|
||||
try {
|
||||
if (!this.verificationConfig.nodeUrl || !this.verificationConfig.macaroon) {
|
||||
throw new Error('LND configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/invoice/${paymentHash}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Grpc-Metadata-macaroon': this.verificationConfig.macaroon,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`LND API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const invoiceData = await response.json();
|
||||
|
||||
// Проверяем, что инвойс оплачен и preimage совпадает
|
||||
if (invoiceData.settled && invoiceData.r_preimage === preimage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('LND payment verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 3: Верификация через Core Lightning (CLN)
|
||||
async verifyPaymentCLN(preimage, paymentHash) {
|
||||
try {
|
||||
if (!this.verificationConfig.nodeUrl) {
|
||||
throw new Error('CLN configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/listinvoices`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
payment_hash: paymentHash
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CLN API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.invoices && data.invoices.length > 0) {
|
||||
const invoice = data.invoices[0];
|
||||
if (invoice.status === 'paid' && invoice.payment_preimage === preimage) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('CLN payment verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 4: Верификация через Wallet of Satoshi API (если доступен)
|
||||
async verifyPaymentWOS(preimage, paymentHash) {
|
||||
try {
|
||||
// Wallet of Satoshi обычно не предоставляет публичного API
|
||||
// Этот метод для примера структуры
|
||||
console.warn('Wallet of Satoshi API verification not implemented');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('WOS payment verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 5: Верификация через BTCPay Server
|
||||
async verifyPaymentBTCPay(preimage, paymentHash) {
|
||||
try {
|
||||
if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) {
|
||||
throw new Error('BTCPay Server configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/invoices/${paymentHash}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.verificationConfig.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`BTCPay API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const invoiceData = await response.json();
|
||||
|
||||
if (invoiceData.status === 'Settled' && invoiceData.payment && invoiceData.payment.preimage === preimage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('BTCPay payment verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Криптографическая верификация preimage
|
||||
async verifyCryptographically(preimage, paymentHash) {
|
||||
try {
|
||||
// Преобразуем preimage в байты
|
||||
const preimageBytes = new Uint8Array(preimage.match(/.{2}/g).map(byte => parseInt(byte, 16)));
|
||||
|
||||
// Вычисляем SHA256 от preimage
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', preimageBytes);
|
||||
const computedHash = Array.from(new Uint8Array(hashBuffer))
|
||||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Сравниваем с payment_hash
|
||||
return computedHash === paymentHash;
|
||||
} catch (error) {
|
||||
console.error('Cryptographic verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Основной метод верификации платежа
|
||||
async verifyPayment(preimage, paymentHash) {
|
||||
console.log(`🔐 Verifying payment: preimage=${preimage}, hash=${paymentHash}`);
|
||||
|
||||
// Базовые проверки формата
|
||||
if (!preimage || preimage.length !== 64) {
|
||||
console.log('❌ Invalid preimage length');
|
||||
return { verified: false, reason: 'Invalid preimage length' };
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(preimage)) {
|
||||
console.log('❌ Invalid preimage format');
|
||||
return { verified: false, reason: 'Invalid preimage format' };
|
||||
}
|
||||
|
||||
// Для бесплатных сессий
|
||||
if (preimage === '0'.repeat(64)) {
|
||||
console.log('✅ Free session preimage accepted');
|
||||
return { verified: true, method: 'free' };
|
||||
}
|
||||
|
||||
// Проверяем, что preimage не является заглушкой
|
||||
const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64)];
|
||||
if (dummyPreimages.includes(preimage)) {
|
||||
console.log('❌ Dummy preimage detected');
|
||||
return { verified: false, reason: 'Dummy preimage detected' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Сначала проверяем криптографически
|
||||
const cryptoValid = await this.verifyCryptographically(preimage, paymentHash);
|
||||
if (!cryptoValid) {
|
||||
console.log('❌ Cryptographic verification failed');
|
||||
return { verified: false, reason: 'Cryptographic verification failed' };
|
||||
}
|
||||
|
||||
console.log('✅ Cryptographic verification passed');
|
||||
|
||||
// Затем проверяем через выбранный метод
|
||||
switch (this.verificationConfig.method) {
|
||||
case 'lnbits':
|
||||
const lnbitsResult = await this.verifyPaymentLNbits(preimage, paymentHash);
|
||||
return lnbitsResult.verified ? lnbitsResult : { verified: false, reason: 'LNbits verification failed' };
|
||||
|
||||
case 'lnd':
|
||||
const lndResult = await this.verifyPaymentLND(preimage, paymentHash);
|
||||
return lndResult ? { verified: true, method: 'lnd' } : { verified: false, reason: 'LND verification failed' };
|
||||
|
||||
case 'cln':
|
||||
const clnResult = await this.verifyPaymentCLN(preimage, paymentHash);
|
||||
return clnResult ? { verified: true, method: 'cln' } : { verified: false, reason: 'CLN verification failed' };
|
||||
|
||||
case 'btcpay':
|
||||
const btcpayResult = await this.verifyPaymentBTCPay(preimage, paymentHash);
|
||||
return btcpayResult ? { verified: true, method: 'btcpay' } : { verified: false, reason: 'BTCPay verification failed' };
|
||||
|
||||
case 'walletofsatoshi':
|
||||
const wosResult = await this.verifyPaymentWOS(preimage, paymentHash);
|
||||
return wosResult ? { verified: true, method: 'wos' } : { verified: false, reason: 'WOS verification failed' };
|
||||
|
||||
default:
|
||||
console.warn('Unknown verification method, using crypto-only verification');
|
||||
return { verified: cryptoValid, method: 'crypto-only' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Payment verification failed:', error);
|
||||
return { verified: false, reason: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Остальные методы остаются без изменений...
|
||||
activateSession(sessionType, preimage) {
|
||||
// Очистка предыдущей сессии
|
||||
this.cleanup();
|
||||
|
||||
const pricing = this.sessionPrices[sessionType];
|
||||
const now = Date.now();
|
||||
const expiresAt = now + (pricing.hours * 60 * 60 * 1000);
|
||||
|
||||
this.currentSession = {
|
||||
type: sessionType,
|
||||
startTime: now,
|
||||
expiresAt: expiresAt,
|
||||
preimage: preimage
|
||||
};
|
||||
|
||||
this.startSessionTimer();
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
startSessionTimer() {
|
||||
if (this.sessionTimer) {
|
||||
clearInterval(this.sessionTimer);
|
||||
}
|
||||
|
||||
this.sessionTimer = setInterval(() => {
|
||||
if (!this.hasActiveSession()) {
|
||||
this.expireSession();
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
expireSession() {
|
||||
if (this.sessionTimer) {
|
||||
clearInterval(this.sessionTimer);
|
||||
}
|
||||
|
||||
this.currentSession = null;
|
||||
|
||||
if (this.onSessionExpired) {
|
||||
this.onSessionExpired();
|
||||
}
|
||||
}
|
||||
|
||||
getTimeLeft() {
|
||||
if (!this.currentSession) return 0;
|
||||
return Math.max(0, this.currentSession.expiresAt - Date.now());
|
||||
}
|
||||
|
||||
forceUpdateTimer() {
|
||||
if (this.currentSession) {
|
||||
const timeLeft = this.getTimeLeft();
|
||||
console.log('Timer updated:', timeLeft, 'ms left');
|
||||
return timeLeft;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.sessionTimer) {
|
||||
clearInterval(this.sessionTimer);
|
||||
}
|
||||
this.currentSession = null;
|
||||
}
|
||||
|
||||
resetSession() {
|
||||
if (this.sessionTimer) {
|
||||
clearInterval(this.sessionTimer);
|
||||
}
|
||||
this.currentSession = null;
|
||||
console.log('Session reset due to failed verification');
|
||||
}
|
||||
|
||||
canActivateSession() {
|
||||
return !this.hasActiveSession() && !this.currentSession;
|
||||
}
|
||||
|
||||
async safeActivateSession(sessionType, preimage, paymentHash) {
|
||||
try {
|
||||
console.log(`🚀 Activating session: ${sessionType} with preimage: ${preimage}`);
|
||||
|
||||
if (!sessionType || !preimage) {
|
||||
console.warn('❌ Session activation failed: missing sessionType or preimage');
|
||||
return { success: false, reason: 'Missing sessionType or preimage' };
|
||||
}
|
||||
|
||||
if (!this.sessionPrices[sessionType]) {
|
||||
console.warn('❌ Session activation failed: invalid session type');
|
||||
return { success: false, reason: 'Invalid session type' };
|
||||
}
|
||||
|
||||
// Верифицируем платеж
|
||||
const verificationResult = await this.verifyPayment(preimage, paymentHash);
|
||||
|
||||
if (verificationResult.verified) {
|
||||
this.activateSession(sessionType, preimage);
|
||||
console.log(`✅ Session activated successfully: ${sessionType} via ${verificationResult.method}`);
|
||||
return {
|
||||
success: true,
|
||||
sessionType: sessionType,
|
||||
method: verificationResult.method,
|
||||
details: verificationResult,
|
||||
timeLeft: this.getTimeLeft()
|
||||
};
|
||||
} else {
|
||||
console.log('❌ Payment verification failed:', verificationResult.reason);
|
||||
return {
|
||||
success: false,
|
||||
reason: verificationResult.reason,
|
||||
method: verificationResult.method
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Session activation failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
reason: error.message,
|
||||
method: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { PayPerSessionManager };
|
||||
29
src/styles/animations.css
Normal file
29
src/styles/animations.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Плавная прокрутка сообщений / появление */
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Пульсация иконок */
|
||||
@keyframes iconPulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Пульс для таймера */
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* Скролл логотипов */
|
||||
@keyframes walletLogosScroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
366
src/styles/components.css
Normal file
366
src/styles/components.css
Normal file
@@ -0,0 +1,366 @@
|
||||
/* Хедер и карточки */
|
||||
.header-minimal {
|
||||
background: rgb(35 36 35 / 13%);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.header-minimal .cursor-pointer:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.header-minimal .cursor-pointer:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.card-minimal {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(75, 85, 99, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.card-minimal:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Статусы */
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-connected { background: #10b981; }
|
||||
.status-connecting { background: #6b7280; }
|
||||
.status-failed { background: #ef4444; }
|
||||
.status-disconnected { background: #6b7280; }
|
||||
.status-verifying { background: #9ca3af; }
|
||||
|
||||
/* Security / verification */
|
||||
.security-shield {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
color: #f1f5f9;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 1.2em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Иконки и размеры */
|
||||
.icon-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.icon-container i {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-sm { font-size: 0.875rem; }
|
||||
.icon-md { font-size: 1rem; }
|
||||
.icon-lg { font-size: 1.125rem; }
|
||||
.icon-xl { font-size: 1.25rem; }
|
||||
.icon-2xl { font-size: 1.5rem; }
|
||||
|
||||
/* Step number */
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Fallback styles for icons (ВОТ ОН — блок, который я пропустил) */
|
||||
.icon-fallback {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Ensure icons are visible */
|
||||
.fas, .far, .fab {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Improve icon rendering */
|
||||
.fas::before, .far::before, .fab::before {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Icon loading fallback (первый вариант) */
|
||||
.icon-loading {
|
||||
opacity: 0.7;
|
||||
animation: iconPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Fallback icons content */
|
||||
.fa-fallback .fas.fa-shield-halved::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-shield-alt::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-lock::before { content: "🔒"; }
|
||||
.fa-fallback .fas.fa-unlock-alt::before { content: "🔓"; }
|
||||
.fa-fallback .fas.fa-key::before { content: "🔑"; }
|
||||
.fa-fallback .fas.fa-fingerprint::before { content: "👆"; }
|
||||
.fa-fallback .fas.fa-exchange-alt::before { content: "🔄"; }
|
||||
.fa-fallback .fas.fa-plus::before { content: "➕"; }
|
||||
.fa-fallback .fas.fa-link::before { content: "🔗"; }
|
||||
.fa-fallback .fas.fa-paste::before { content: "📋"; }
|
||||
.fa-fallback .fas.fa-check-circle::before { content: "✅"; }
|
||||
.fa-fallback .fas.fa-cogs::before { content: "⚙️"; }
|
||||
.fa-fallback .fas.fa-rocket::before { content: "🚀"; }
|
||||
.fa-fallback .fas.fa-copy::before { content: "📄"; }
|
||||
.fa-fallback .fas.fa-check::before { content: "✓"; }
|
||||
.fa-fallback .fas.fa-times::before { content: "✗"; }
|
||||
.fa-fallback .fas.fa-exclamation-triangle::before { content: "⚠️"; }
|
||||
.fa-fallback .fas.fa-info-circle::before { content: "ℹ️"; }
|
||||
.fa-fallback .fas.fa-circle::before { content: "●"; }
|
||||
.fa-fallback .fas.fa-paper-plane::before { content: "📤"; }
|
||||
.fa-fallback .fas.fa-comments::before { content: "💬"; }
|
||||
.fa-fallback .fas.fa-signature::before { content: "✍️"; }
|
||||
.fa-fallback .fas.fa-power-off::before { content: "⏻"; }
|
||||
.fa-fallback .fas.fa-arrow-left::before { content: "←"; }
|
||||
|
||||
/* Ensure fallback icons are properly sized & use emoji font */
|
||||
.fa-fallback .fas::before {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Icon alignment in buttons */
|
||||
button i {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Повторное определение не удалял — второй вариант icon-loading переопределит первый (как в оригинале) */
|
||||
.icon-loading {
|
||||
opacity: 0.6;
|
||||
animation: iconPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Чат */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 128px);
|
||||
min-height: 0;
|
||||
}
|
||||
.chat-messages-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: rgba(42, 43, 42, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Интерактивные индикаторы безопасности */
|
||||
.header-minimal .cursor-pointer:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.header-minimal .cursor-pointer:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Pay-per-session UI */
|
||||
.session-timer {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border: 1px solid rgba(249, 115, 22, 0.3);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.session-timer.warning {
|
||||
background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.session-timer.critical {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Lightning button */
|
||||
.lightning-button {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.lightning-button:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border: 1px solid rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-verify {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-verify:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Wallet logos container & per-wallet filters */
|
||||
.wallet-logos-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 64px;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wallet-logos-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
animation: walletLogosScroll 30s linear infinite;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.wallet-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 48px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.wallet-logo:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Примеры per-wallet классов (как в оригинале) */
|
||||
.wallet-logo.bitcoin-lightning { background: transparent; padding: 4px; }
|
||||
.wallet-logo.bitcoin-lightning img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.impervious { background: transparent; padding: 4px; }
|
||||
.wallet-logo.impervious img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.strike { background: transparent; padding: 4px; }
|
||||
.wallet-logo.strike img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lnbits { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lnbits img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lightning-labs { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lightning-labs img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.atomic { background: transparent; padding: 4px; }
|
||||
.wallet-logo.atomic img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.breez { background: transparent; padding: 4px; }
|
||||
.wallet-logo.breez img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.alby { background: transparent; padding: 4px; }
|
||||
.wallet-logo.alby img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.phoenix { background: transparent; }
|
||||
.wallet-logo.blixt { background: transparent; }
|
||||
.wallet-logo.zeus { background: transparent; padding: 4px; }
|
||||
.wallet-logo.zeus img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.wos { background: transparent; padding: 4px; }
|
||||
.wallet-logo.wos img { width: 80px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.muun { background: transparent; padding: 4px; }
|
||||
.wallet-logo.muun img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
/* Pause animation on hover for logos */
|
||||
.wallet-logos-container:hover .wallet-logos-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
/* Анимация появления сообщений использует keyframes из animations.css */
|
||||
.message-slide {
|
||||
animation: messageSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Icon color improvements (повтор в оригинале — сохранил) */
|
||||
.accent-orange { color: #fb923c !important; }
|
||||
.accent-green { color: #34d399 !important; }
|
||||
.accent-red { color: #f87171 !important; }
|
||||
.accent-yellow { color: #fbbf24 !important; }
|
||||
.accent-purple { color: #a78bfa !important; }
|
||||
.accent-gray { color: #9ca3af !important; }
|
||||
.accent-cyan { color: #22d3ee !important; }
|
||||
|
||||
/* Ensure icons visible in dark backgrounds */
|
||||
.text-secondary i {
|
||||
opacity: 0.8;
|
||||
}
|
||||
96
src/styles/main.css
Normal file
96
src/styles/main.css
Normal file
@@ -0,0 +1,96 @@
|
||||
/* Основные шрифты и цвета */
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #2A2B2A;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Базовые фоны */
|
||||
.bg-custom-bg {
|
||||
background-color: rgb(37 38 37) !important;
|
||||
}
|
||||
|
||||
.bg-header {
|
||||
background-color: rgb(35 35 35) !important;
|
||||
}
|
||||
|
||||
.minimal-bg {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2A2B2A 100%);
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Текстовые стили */
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-secondary { color: #9ca3af; }
|
||||
.text-muted { color: #6b7280; }
|
||||
|
||||
/* Акцентные цвета */
|
||||
.accent-orange { color: #fb923c; }
|
||||
.accent-green { color: #34d399; }
|
||||
.accent-red { color: #f87171; }
|
||||
.accent-yellow { color: #fbbf24; }
|
||||
.accent-purple { color: #a78bfa; }
|
||||
.accent-blue { color: #60a5fa; }
|
||||
.accent-gray { color: #9ca3af; }
|
||||
.accent-cyan { color: #22d3ee; }
|
||||
|
||||
/* Кастомный скролл */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(42, 43, 42, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(75, 85, 99, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
|
||||
/* Плавная прокрутка */
|
||||
.scroll-smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Улучшенная прокрутка для сообщений */
|
||||
.messages-container {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Медиа-запросы (мобильные/планшет) */
|
||||
@media (max-width: 640px) {
|
||||
.header-minimal { padding: 0 8px; }
|
||||
|
||||
.icon-container {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
font-size: 0.875rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.header-minimal button {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user