diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 6a0ee60..13b4161 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -488,7 +488,7 @@ class AccountModule { final data = _requireMapPayload(packet, 'checkPassword'); if (data['error'] != null) { - throw Exception('checkPassword: неверный пароль'); + throw const WrongPasswordException(); } final tokenAttrs = data['tokenAttrs']; diff --git a/lib/backend/modules/account/account_models.dart b/lib/backend/modules/account/account_models.dart index a8bb7e3..540ff9e 100644 --- a/lib/backend/modules/account/account_models.dart +++ b/lib/backend/modules/account/account_models.dart @@ -290,6 +290,12 @@ class WrongDeviceTokenException implements Exception { String toString() => 'WrongDeviceTokenException'; } +class WrongPasswordException implements Exception { + const WrongPasswordException(); + @override + String toString() => 'WrongPasswordException'; +} + class RequestCodeResult { final String token; diff --git a/lib/backend/modules/account/two_factor_module.dart b/lib/backend/modules/account/two_factor_module.dart index e6f7b47..eb57144 100644 --- a/lib/backend/modules/account/two_factor_module.dart +++ b/lib/backend/modules/account/two_factor_module.dart @@ -124,7 +124,7 @@ class TwoFactorModule extends AccountApiBase { checkPacketError(packet, 'check2faPassword'); final data = packet.payload; if (data is Map && data['error'] != null) { - throw Exception('Неверный пароль'); + throw const WrongPasswordException(); } } diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index d817ed3..85bb4b5 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -1,3 +1,4 @@ +import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -14,10 +15,25 @@ class TokenStorage { mOptions: MacOsOptions(usesDataProtectionKeychain: false), ); - static Future writeSecure(String key, String value) async { - await _secure.write(key: key, value: value); + static const int _duplicateKeychainItem = -25299; + + static bool _isDuplicateItem(PlatformException error) => + error.details == _duplicateKeychainItem || + (error.message?.contains('$_duplicateKeychainItem') ?? false); + + static Future _write(String key, String value) async { + try { + await _secure.write(key: key, value: value); + } on PlatformException catch (e) { + if (!_isDuplicateItem(e)) rethrow; + await _secure.delete(key: key); + await _secure.write(key: key, value: value); + } } + static Future writeSecure(String key, String value) => + _write(key, value); + static Future readSecure(String key) async { return _secure.read(key: key); } @@ -31,9 +47,8 @@ class TokenStorage { return all.keys.where((key) => key.startsWith(prefix)).toList(); } - static Future saveToken(String token, int accountId) async { - await _secure.write(key: '$_tokenPrefix$accountId', value: token); - } + static Future saveToken(String token, int accountId) => + _write('$_tokenPrefix$accountId', token); static Future readToken(int accountId) async { final key = '$_tokenPrefix$accountId'; @@ -43,7 +58,7 @@ class TokenStorage { final prefs = await SharedPreferences.getInstance(); final legacy = prefs.getString(key); if (legacy != null) { - await _secure.write(key: key, value: legacy); + await _write(key, legacy); await prefs.remove(key); return legacy; } diff --git a/lib/frontend/screens/auth/password_2fa_screen.dart b/lib/frontend/screens/auth/password_2fa_screen.dart index 1ef6731..58cb650 100644 --- a/lib/frontend/screens/auth/password_2fa_screen.dart +++ b/lib/frontend/screens/auth/password_2fa_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../../../backend/modules/account/account_models.dart'; import '../../../core/protocol/packet.dart'; +import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../../widgets/animated_slash_icon.dart'; import '../../widgets/custom_notification.dart'; @@ -101,10 +103,13 @@ class _Password2FAScreenState extends State _isLoading = false; }); + final l10n = AppLocalizations.of(context)!; if (!passed && (isSessionStateError(e) || sessionStale)) { recoverStaleSession(); + } else if (e is WrongPasswordException) { + showCustomNotification(context, l10n.passwordEntryWrongPassword); } else { - showCustomNotification(context, 'Неверный пароль: $e'); + showCustomNotification(context, l10n.devicesGenericError('$e')); } } } diff --git a/lib/frontend/screens/profile/password_entry_screen.dart b/lib/frontend/screens/profile/password_entry_screen.dart index c5c93a9..b389ab9 100644 --- a/lib/frontend/screens/profile/password_entry_screen.dart +++ b/lib/frontend/screens/profile/password_entry_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../main.dart' show accountModule; -import '../../../backend/modules/account.dart' show TwoFactorDetails; import '../../../core/storage/app_database.dart'; import '../../../l10n/app_localizations.dart'; import '../../widgets/animated_slash_icon.dart'; @@ -11,6 +10,13 @@ import '../../widgets/primary_loading_button.dart'; import '../../widgets/small_spinner.dart'; import '../../../core/config/app_fonts.dart'; import '../../../core/config/app_shape.dart'; +import '../../../backend/modules/account/account_models.dart'; + + +String _passwordErrorText(Object error, AppLocalizations l10n) => + error is WrongPasswordException + ? l10n.passwordEntryWrongPassword + : l10n.devicesGenericError('$error'); class PasswordEntryScreen extends StatefulWidget { const PasswordEntryScreen({super.key}); @@ -56,12 +62,13 @@ class _PasswordEntryScreenState extends State { _details = details; }); _passwordController.clear(); - } catch (_) { + } catch (e) { if (mounted) { setState( - () => _errorMessage = AppLocalizations.of( - context, - )!.passwordEntryWrongPassword, + () => _errorMessage = _passwordErrorText( + e, + AppLocalizations.of(context)!, + ), ); } } finally { @@ -621,7 +628,14 @@ class _TwoFactorSetupScreenState extends State { break; } } catch (e) { - if (mounted) setState(() => _errorMessage = e.toString()); + if (mounted) { + setState( + () => _errorMessage = _passwordErrorText( + e, + AppLocalizations.of(context)!, + ), + ); + } } finally { if (mounted) { _isLoading.value = false; @@ -1011,7 +1025,14 @@ class _TwoFactorPasswordChangeScreenState Navigator.popUntil(context, ModalRoute.withName('SecurityScreen')); } } catch (e) { - if (mounted) setState(() => _errorMessage = e.toString()); + if (mounted) { + setState( + () => _errorMessage = _passwordErrorText( + e, + AppLocalizations.of(context)!, + ), + ); + } } finally { if (mounted) _isLoading.value = false; } @@ -1182,7 +1203,14 @@ class _TwoFactorEmailChangeScreenState break; } } catch (e) { - if (mounted) setState(() => _errorMessage = e.toString()); + if (mounted) { + setState( + () => _errorMessage = _passwordErrorText( + e, + AppLocalizations.of(context)!, + ), + ); + } } finally { if (mounted) _isLoading.value = false; } @@ -1335,7 +1363,14 @@ class _TwoFactorRemoveScreenState extends State { Navigator.popUntil(context, ModalRoute.withName('SecurityScreen')); } } catch (e) { - if (mounted) setState(() => _errorMessage = e.toString()); + if (mounted) { + setState( + () => _errorMessage = _passwordErrorText( + e, + AppLocalizations.of(context)!, + ), + ); + } } finally { if (mounted) _isLoading.value = false; }