import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:cryptography/cryptography.dart'; import '../../core/constants.dart'; import '../../domain/services/crypto_service.dart'; import '../../domain/services/api_service.dart'; import '../../logic/auth_provider.dart'; import 'contacts_screen.dart'; class QrLoginScreen extends StatefulWidget { const QrLoginScreen({super.key}); @override State createState() => _QrLoginScreenState(); } class _QrLoginScreenState extends State { final _cryptoService = CryptoService(); String? _roomId; SimpleKeyPair? _tempKeyPair; String? _publicKeyBase64; String? _deviceName; WebSocketChannel? _channel; bool _isInitializing = true; String? _errorMessage; StreamSubscription? _subscription; @override void initState() { super.initState(); _initQrSession(); } String _generateRandomRoomId() { final rand = Random.secure(); final bytes = List.generate(16, (i) => rand.nextInt(256)); return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } Future _initQrSession() async { try { setState(() { _isInitializing = true; _errorMessage = null; }); // 1. Генерируем ID комнаты сопряжения final roomId = _generateRandomRoomId(); // 2. Генерируем временные ключи X25519 для шифрования канала final tempKeyPair = await _cryptoService.generateTempKeyPair(); final publicKey = await tempKeyPair.extractPublicKey(); final publicKeyBase64 = base64Encode(publicKey.bytes); // Получаем имя текущего устройства final apiService = ApiService(); final deviceName = await apiService.getDeviceName(); setState(() { _roomId = roomId; _tempKeyPair = tempKeyPair; _publicKeyBase64 = publicKeyBase64; _deviceName = deviceName; _isInitializing = false; }); // 3. Подключаемся к WebSocket серверу сопряжения _connectWebSocket(roomId); } catch (e) { setState(() { _isInitializing = false; _errorMessage = 'Ошибка инициализации: $e'; }); } } void _connectWebSocket(String roomId) { try { final wsUri = Uri.parse('${AppConstants.wsUrl}/ws/qr?room_id=$roomId'); _channel = WebSocketChannel.connect(wsUri); _subscription = _channel!.stream.listen( (data) { _handleWsMessage(data); }, onError: (error) { setState(() { _errorMessage = 'Сбой подключения: $error. Переподключение...'; }); // Попытка переподключения через 3 секунды Future.delayed(const Duration(seconds: 3), () { if (mounted && _roomId == roomId) { _connectWebSocket(roomId); } }); }, onDone: () { print('QR WS соединение закрыто'); }, ); } catch (e) { setState(() { _errorMessage = 'Ошибка WebSocket: $e'; }); } } Future _handleWsMessage(dynamic rawData) async { try { final data = jsonDecode(rawData) as Map; if (data['type'] == 'qr_authorized') { final accessToken = data['access_token'] as String; final refreshToken = data['refresh_token'] as String; final userId = data['user_id'].toString(); final phoneTempPub = data['phone_temp_pub'] as String; final encPrivateKeyPayload = data['enc_private_key_payload'] as String; if (_tempKeyPair == null) return; // Расшифровываем основной E2EE private_key final decryptedPrivateKey = await _cryptoService.decryptPrivateKeyWithEcdh( tempKeyPair: _tempKeyPair!, tempRemotePublicKeyBase64: phoneTempPub, encryptedPayloadBase64: encPrivateKeyPayload, ); // Завершаем авторизацию и сохраняем все данные final authProvider = context.read(); await authProvider.loginWithQrData( accessToken: accessToken, refreshToken: refreshToken, userId: userId, privateKey: decryptedPrivateKey, ); if (mounted) { // Запускаем WebSocket клиент мессенджера await authProvider.initRealtime(); // Перенаправляем на главный экран Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const ContactsScreen()), (route) => false, ); } } } catch (e) { setState(() { _errorMessage = 'Не удалось завершить авторизацию: $e'; }); } } @override void dispose() { _subscription?.cancel(); _channel?.sink.close(); super.dispose(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; // Содержимое QR-кода final encodedPublicKey = Uri.encodeComponent(_publicKeyBase64 ?? ''); final encodedDeviceName = Uri.encodeComponent(_deviceName ?? 'Новое устройство'); final qrData = 'chepuhagram:qr_login?room_id=$_roomId&public_key=$encodedPublicKey&device_name=$encodedDeviceName'; return Scaffold( appBar: AppBar( title: Text('Вход по QR-коду', style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), backgroundColor: Colors.transparent, elevation: 0, ), body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_isInitializing) const Column( children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Генерация сессии авторизации...'), ], ) else if (_errorMessage != null && _roomId == null) Column( children: [ Icon(Icons.error_outline, size: 60, color: colorScheme.error), const SizedBox(height: 16), Text(_errorMessage!, textAlign: TextAlign.center), const SizedBox(height: 24), ElevatedButton( onPressed: _initQrSession, child: const Text('Повторить попытку'), ), ], ) else Column( children: [ Text( 'Войти в Чепухаграм', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: colorScheme.primary, ), ), const SizedBox(height: 12), const Text( 'Откройте Чепухаграм на вашем телефоне, зайдите в Настройки -> Устройства -> Подключить устройство и отсканируйте код.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey, fontSize: 14), ), const SizedBox(height: 36), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: QrImageView( data: qrData, version: QrVersions.auto, size: 260.0, gapless: false, ), ), const SizedBox(height: 36), if (_errorMessage != null) Text( _errorMessage!, style: TextStyle(color: colorScheme.error, fontSize: 13), textAlign: TextAlign.center, ) else const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 12), Text( 'Ожидание сканирования...', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), ], ), ], ), ], ), ), ), ), ); } }