// Haveno App extends the features of Haveno, supporting mobile devices and more. // Copyright (C) 2024 Kewbit (https://kewbit.org) // Source Code: https://git.haveno.com/haveno/haveno-app.git // // Author: Kewbit // Website: https://kewbit.org // Contact Email: me@kewbit.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import 'dart:io'; import 'package:flutter/material.dart'; import 'package:haveno_app/services/secure_storage_service.dart'; import 'package:haveno_app/services/security.dart'; import 'package:haveno_app/views/screens/establish_connection_screen.dart'; import 'package:haveno_app/views/screens/link_to_desktop_screen.dart'; import 'package:haveno_app/views/screens/onboarding_screen.dart'; class PasswordScreen extends StatefulWidget { const PasswordScreen({super.key}); @override _PasswordScreenState createState() => _PasswordScreenState(); } class _PasswordScreenState extends State with SingleTickerProviderStateMixin { final SecureStorageService _secureStorage = SecureStorageService(); final TextEditingController _passwordController = TextEditingController(); final SecurityService _securityService = SecurityService(); final _formKey = GlobalKey(); bool _isPasswordSet = false; bool _isSettingPassword = false; bool _isObscured = true; bool _isHovered = false; bool _isUnlocked = false; Color _lockColor = Colors.white; IconData _lockIcon = Icons.lock; // Animation controller for shaking the padlock late AnimationController _shakeController; late Animation _shakeAnimation; @override void initState() { super.initState(); _checkPasswordStatus(); // Initialize shake animation _shakeController = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); _shakeAnimation = Tween(begin: 0, end: 10).chain( CurveTween(curve: Curves.elasticIn), ).animate(_shakeController) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _shakeController.reverse(); } }); } @override void dispose() { _shakeController.dispose(); super.dispose(); } Future _checkPasswordStatus() async { String? storedPassword = await _secureStorage.readUserPassword(); setState(() { _isPasswordSet = storedPassword != null; _isSettingPassword = storedPassword == null; }); } Future _setPassword(String password) async { await _securityService.setupUserPassword(password); setState(() { _isPasswordSet = true; _isSettingPassword = false; }); } Future _verifyPassword(String password) async { if (await _securityService.authenticateUserPassword(password)) { setState(() { _lockColor = Colors.green; _lockIcon = Icons.lock_open; _isUnlocked = true; }); await Future.delayed(const Duration(seconds: 2)); _decideNextScreen(); } else { _shakeController.forward(from: 0); // Start the shake animation setState(() { _lockColor = Colors.red; _lockIcon = Icons.lock; }); await Future.delayed(const Duration(seconds: 2)); setState(() { _lockColor = Colors.white; }); } } void _toggleObscured() { setState(() { _isObscured = !_isObscured; }); } void _decideNextScreen() { // Check if the user is new and still part of the onboarding process and send him to monero screen print("Navigating to the next screen..."); if (Platform.isIOS || Platform.isAndroid) { _secureStorage.readHavenoDaemonConfig().then((config) => { if (config == null) { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => LinkToDesktopScreen(), ), ) } else { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => EstablishConnectionScreen(), ), ) } }); } else { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => EstablishConnectionScreen(), ), ); } } void _showResetConfirmation() { showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { bool isResetting = false; bool resetComplete = false; String? errorMessage; Future startReset() async { setState(() { isResetting = true; }); try { await _securityService.resetAppData(); setState(() { resetComplete = true; }); await Future.delayed(const Duration(seconds: 1)); // Wait for the tick to show Navigator.of(context).pop(); // Close the dialog box Navigator.pushReplacement( context, PageRouteBuilder( pageBuilder: (context, animation1, animation2) => OnboardingScreen(), transitionsBuilder: (context, animation1, animation2, child) { return SlideTransition( position: Tween( begin: const Offset(-1, 0), // Start from left end: Offset.zero, ).animate(animation1), child: child, ); }, ), ); } catch (e) { setState(() { isResetting = false; errorMessage = "Failed to reset app data. Please try again."; }); print("Reset error: $e"); } } return AlertDialog( title: isResetting ? null : const Text('Confirm Reset'), content: isResetting ? resetComplete ? const Icon(Icons.check_circle, color: Colors.green, size: 64) : const SizedBox( height: 64, width: 64, child: CircularProgressIndicator(), ) : errorMessage != null ? Text(errorMessage!) : const Text('Are you sure you want to reset the device? This action cannot be undone.'), actions: isResetting || resetComplete ? null : [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Cancel'), ), TextButton( onPressed: () { startReset(); }, child: const Text('Reset'), ), ], ); }, ); }, ); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( children: [ Image.asset( 'assets/haveno-logo.png', height: 100, ), const SizedBox(height: 16), AnimatedBuilder( animation: _shakeAnimation, builder: (context, child) { return Transform.translate( offset: Offset(_shakeAnimation.value, 0), child: Icon( _lockIcon, size: 48, color: _lockColor, ), ); }, ), const SizedBox(height: 16), ], ), Text( _isSettingPassword ? 'Set Your Password' : 'Enter Your Password', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 24), Form( key: _formKey, child: Column( children: [ TextFormField( controller: _passwordController, obscureText: _isObscured, decoration: InputDecoration( labelText: 'Password', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), ), suffixIcon: IconButton( icon: Icon( _isObscured ? Icons.visibility : Icons.visibility_off, ), onPressed: _toggleObscured, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a password'; } if (_isSettingPassword && value.length < 8) { return 'Password must be at least 8 characters'; } return null; }, ), const SizedBox(height: 24), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { if (_isSettingPassword) { _setPassword(_passwordController.text); } else { _verifyPassword(_passwordController.text); } } }, child: Text( _isSettingPassword ? 'Set Password' : 'Verify Password', ), ), ], ), ), ], ), ), ), Positioned( bottom: 16, right: 16, child: MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) { setState(() { _isHovered = true; }); }, onExit: (_) { setState(() { _isHovered = false; }); }, child: GestureDetector( onTap: _showResetConfirmation, child: Tooltip( message: 'This will reset the device to its factory settings.', child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.info_outline, color: _isHovered ? Colors.orange : Colors.grey.withOpacity(0.46), ), const SizedBox(width: 4), SizedBox( height: 20, child: Stack( children: [ Text( 'Reset Device', style: TextStyle( color: _isHovered ? Colors.white : Colors.grey.withOpacity(0.46), fontSize: 14, decoration: TextDecoration.none, ), ), Positioned( bottom: -2, left: 0, right: 0, child: Container( height: 2, color: _isHovered ? Colors.orange : Colors.transparent, ), ), ], ), ), ], ), ), ), ), ), ], ), ); } }