Chepuhagram/lib/presentation/screens/key_recovery_screen.dart

467 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:chepuhagram/logic/auth_provider.dart';
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
import 'package:chepuhagram/domain/services/crypto_service.dart';
import 'package:chepuhagram/domain/services/api_service.dart';
import 'package:chepuhagram/presentation/screens/account_setup_screen.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:chepuhagram/core/constants.dart';
import 'package:chepuhagram/core/theme_manager.dart';
import 'dart:io';
class KeyRecoveryScreen extends StatefulWidget {
const KeyRecoveryScreen({super.key});
@override
State<KeyRecoveryScreen> createState() => _KeyRecoveryScreenState();
}
class _KeyRecoveryScreenState extends State<KeyRecoveryScreen>
with SingleTickerProviderStateMixin {
bool _isLoading = false;
String? _errorMessage;
final _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.fastOutSlowIn,
),
);
_animationController.forward();
}
Future<void> _startFresh() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = context.read<AuthProvider>();
try {
final api = ApiService();
await api.deleteAllMessages();
} catch (e) {
print('Ошибка при удалении сообщений: $e');
}
await authProvider.resetKeys();
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
);
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Ошибка: ${e.toString()}';
_isLoading = false;
});
}
}
}
Future<void> _recoverKeys() async {
if (!_formKey.currentState!.validate()) return;
FocusScope.of(context).unfocus();
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = context.read<AuthProvider>();
final apiService = ApiService();
final cryptoService = CryptoService();
final token = await apiService.getAccessToken();
if (token == null) throw Exception('Не авторизован');
final response = await http.get(
Uri.parse('${AppConstants.baseUrl}/users/me'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode != 200) {
throw Exception('Не удалось получить данные пользователя');
}
final data = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
final encryptedPrivateKey = data['encrypted_private_key'];
if (encryptedPrivateKey == null || encryptedPrivateKey.isEmpty) {
throw Exception('Зашифрованный ключ не найден на сервере');
}
final decryptedPrivateKey = await cryptoService.decryptPrivateKey(
encryptedPrivateKey,
_passwordController.text,
);
await cryptoService.savePrivateKey(decryptedPrivateKey);
await authProvider.tryAutoLogin();
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage =
'Ошибка восстановления: ${e.toString().replaceAll('Exception: ', '')}';
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 620),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
children: [
Icon(
Icons.security_outlined,
size: 80,
color: colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Восстановление ключей',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 16),
Text(
'Вы переустановили приложение или вошли на новом устройстве. Выберите один из вариантов:',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: colorScheme.outline,
),
),
],
),
),
),
const SizedBox(height: 32),
// Вариант 1: Начать с чистого листа
_buildOptionCard(
icon: Icons.restart_alt_outlined,
title: 'Начать с чистого листа',
description:
'Создаются новые ключи шифрования. Старые сообщения не будут расшифрованы.',
buttonText: 'Продолжить',
onPressed: _startFresh,
),
const SizedBox(height: 24),
// Вариант 2: Восстановить из облака
_buildRecoveryCard(),
const SizedBox(height: 24),
// Сообщение об ошибке
if (_errorMessage != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_errorMessage!,
style: TextStyle(
color: colorScheme.onErrorContainer,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
),
),
),
),
],
),
);
}
Widget _buildOptionCard({
required IconData icon,
required String title,
required String description,
required String buttonText,
required VoidCallback onPressed,
}) {
final colorScheme = Theme.of(context).colorScheme;
return ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: colorScheme.primary, size: 28),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
description,
style: TextStyle(color: colorScheme.outline, fontSize: 14),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
buttonText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
);
}
Widget _buildRecoveryCard() {
final colorScheme = Theme.of(context).colorScheme;
return ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
width: 1,
),
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.cloud_download_outlined,
color: colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Восстановить из облака',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
'Введите мастер-пароль для восстановления ключей шифрования',
style: TextStyle(color: colorScheme.outline, fontSize: 14),
),
const SizedBox(height: 16),
_buildTextField(
controller: _passwordController,
label: "Мастер-пароль",
icon: Icons.lock_outline,
obscureText: true,
validator: (value) =>
value!.isEmpty ? "Введите мастер-пароль" : null,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _recoverKeys,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Восстановить',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
bool obscureText = false,
String? Function(String?)? validator,
}) {
final colorScheme = Theme.of(context).colorScheme;
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
// No blur here, it's inside the card
decoration: BoxDecoration(
color: colorScheme.surface.withOpacity(0.5),
borderRadius: BorderRadius.circular(16),
),
child: TextFormField(
controller: controller,
obscureText: obscureText,
validator: validator,
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: colorScheme.outline),
prefixIcon: Icon(icon, color: colorScheme.outline),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 18,
),
),
),
),
);
}
@override
void dispose() {
_passwordController.dispose();
_animationController.dispose();
super.dispose();
}
}