import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:local_auth/local_auth.dart'; import '../../domain/services/crypto_service.dart'; import '../../domain/services/api_service.dart'; class QrScanScreen extends StatefulWidget { const QrScanScreen({super.key}); @override State createState() => _QrScanScreenState(); } class _QrScanScreenState extends State { final _mobileScannerController = MobileScannerController(); final _localAuth = LocalAuthentication(); final _cryptoService = CryptoService(); bool _isProcessing = false; String? _statusMessage; @override void dispose() { _mobileScannerController.dispose(); super.dispose(); } Future _handleQrDetected(BarcodeCapture capture) async { if (_isProcessing) return; final barcode = capture.barcodes.firstOrNull; if (barcode == null || barcode.rawValue == null) return; final scannedValue = barcode.rawValue!; print('Сканирован QR: $scannedValue'); try { final uri = Uri.parse(scannedValue); if (uri.scheme != 'chepuhagram' || !scannedValue.contains('qr_login')) { setState(() { _statusMessage = 'Неверный формат QR-кода Чепухаграм'; }); return; } final roomId = uri.queryParameters['room_id']; final tempPublicKeyBase64 = uri.queryParameters['public_key']?.replaceAll(' ', '+'); final connectingDeviceName = uri.queryParameters['device_name']; if (roomId == null || tempPublicKeyBase64 == null) { setState(() { _statusMessage = 'Отсутствуют параметры сопряжения'; }); return; } // Нашли валидный QR, останавливаем сканер и начинаем обработку setState(() { _isProcessing = true; _statusMessage = 'Обработка запроса сопряжения...'; }); _mobileScannerController.stop(); // Запрашиваем подтверждение final confirmed = await _showConfirmDialog(context); if (!confirmed) { _resetScanner(); return; } // Биометрическая аутентификация с ручным фолбеком при ошибке/отсутствии настроек bool authenticated = false; try { authenticated = await _localAuth.authenticate( localizedReason: 'Подтвердите личность для сопряжения нового устройства', options: const AuthenticationOptions( biometricOnly: false, useErrorDialogs: true, stickyAuth: false, ), ); } catch (e) { print('Исключение local_auth, пробуем ручное подтверждение: $e'); authenticated = await _showManualConfirmationDialog(context); } if (!authenticated) { _resetScanner(); return; } // 1. Извлекаем локальный закрытый E2EE-ключ final privateKey = await _cryptoService.getPrivateKey(); if (privateKey == null) { throw Exception('Локальный закрытый ключ шифрования не найден'); } // 2. Генерируем свои временные ключи сопряжения final phoneKeyPair = await _cryptoService.generateTempKeyPair(); final phonePublicKey = await phoneKeyPair.extractPublicKey(); final phonePublicKeyBase64 = base64Encode(phonePublicKey.bytes); // 3. Шифруем приватный ключ на временном общем секрете final encPayload = await _cryptoService.encryptPrivateKeyWithEcdh( tempKeyPair: phoneKeyPair, tempRemotePublicKeyBase64: tempPublicKeyBase64, privateKeyToEncrypt: privateKey, ); // 4. Отправляем одобрение на сервер final apiService = ApiService(); final success = await apiService.approveQrLogin( roomId: roomId, phoneTempPub: phonePublicKeyBase64, encPrivateKeyPayload: encPayload, deviceName: connectingDeviceName ?? 'Новое устройство', ); if (success) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Новое устройство успешно авторизовано')), ); Navigator.pop(context); } } else { throw Exception('Сервер отклонил авторизацию сопряжения'); } } catch (e) { print(' Ошибка: $e'); setState(() { _statusMessage = 'Ошибка: $e'; }); _resetScanner(); } } void _resetScanner() { setState(() { _isProcessing = false; }); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { _mobileScannerController.start(); } }); } Future _showConfirmDialog(BuildContext context) async { return await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( title: const Text('Подтверждение входа'), content: const Text( 'Вы действительно хотите войти на новом устройстве? Это перенесет ваши зашифрованные ключи для доступа к чатам.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Войти'), ), ], ); }, ) ?? false; } Future _showManualConfirmationDialog(BuildContext context) async { return await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( title: const Text('Подтверждение сопряжения'), content: const Text( 'Биометрическая защита или блокировка экрана не настроены на этом телефоне.\n\nВы действительно подтверждаете вход на новом устройстве?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Подтвердить'), ), ], ); }, ) ?? false; } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: const Text('Сканировать QR-код'), backgroundColor: Colors.transparent, elevation: 0, ), body: Stack( children: [ // Сканер камеры MobileScanner( controller: _mobileScannerController, onDetect: _handleQrDetected, ), // Полупрозрачная рамка фокуса по центру Center( child: Container( width: 250, height: 250, decoration: BoxDecoration( border: Border.all(color: colorScheme.primary, width: 3), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.5), spreadRadius: 2000, ), ], ), ), ), // Текстовая подсказка Positioned( bottom: 60, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(16), ), child: Text( _statusMessage ?? 'Наведите камеру на QR-код на новом устройстве', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), ), ], ), ); } }