import 'package:flutter/material.dart'; import '../core/app_controller.dart'; import '../core/failure.dart'; import '../l10n/strings.dart'; import '../services/ssh_service.dart'; import 'password_fields.dart'; /// Asks for the current password, then the new one twice, and changes the /// master password. The Vault stays open and unchanged on any failure. class ChangePasswordPage extends StatefulWidget { final AppController controller; const ChangePasswordPage(this.controller, {super.key}); @override State createState() => _ChangePasswordPageState(); } class _ChangePasswordPageState extends State { final current = TextEditingController(); final next = TextEditingController(); final repeated = TextEditingController(); bool busy = false; String? error; @override void dispose() { for (final c in [current, next, repeated]) { c.clear(); c.dispose(); } super.dispose(); } Future submit() async { FocusManager.instance.primaryFocus?.unfocus(); setState(() { busy = true; error = null; }); try { if (next.text != repeated.text) throw const Failure('passwordMismatch'); if (next.text == current.text) throw const Failure('samePassword'); await widget.controller.changePassword( current.text, next.text, context.tr('biometricReason'), ); if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(context.tr('passwordChanged')))); Navigator.pop(context, true); } catch (e) { if (!mounted) return; final code = failureCode(e); if (code == 'passwordChangedBiometricOff') { // The password itself was changed. ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.tr(code)), duration: const Duration(seconds: 10), ), ); Navigator.pop(context, true); return; } setState( () => error = code == 'unlockFailed' ? 'wrongCurrentPassword' : code, ); } finally { if (mounted) setState(() => busy = false); } } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(title: Text(context.tr('changePassword'))), body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 450), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PasswordField( controller: current, label: context.tr('currentPassword'), autofocus: true, action: TextInputAction.next, ), const SizedBox(height: 16), PasswordField( controller: next, label: context.tr('newPassword'), action: TextInputAction.next, onChanged: (_) => setState(() {}), ), const SizedBox(height: 8), PasswordAdvice(next.text), const SizedBox(height: 16), PasswordField( controller: repeated, label: context.tr('confirmNewPassword'), action: TextInputAction.done, onSubmitted: (_) { if (!busy) submit(); }, ), if (error != null) ...[ const SizedBox(height: 12), Text( context.tr(error!), style: TextStyle( color: Theme.of(context).colorScheme.error, ), ), ], const SizedBox(height: 20), FilledButton( onPressed: busy ? null : submit, child: Text(context.tr('changePassword')), ), if (busy) const LinearProgressIndicator(), ], ), ), ), ), ), ); }