Prompt for wallet password on desktop
What changed, and why it matters
This commit adds a password prompt for the Skylight Wallet app when running on desktop computers (Linux, Windows, macOS). Previously, the app only used the phone's biometric/PIN lock, which isn't reliably available on desktop. The change makes desktop users create and enter a wallet password to protect their funds. It is a security improvement, not a vulnerability fix, though the password is held in memory while the app runs and is not persisted on desktop.
No urgent action needed; this is a defensive improvement. Reviewers should verify that the desktop password is never written to disk or logs, that the password field is cleared from memory on logout, and that the error handling in openExisting does not leak the password in logs (the log line shows '<hidden>'). Consider adding a password-strength indicator and ensuring the password is cleared from WalletModel when no longer needed.
Security signals we found
Adds password-based authentication for desktop platforms where biometric/PIN may be unavailable
Removes reliance on mobile secure storage for desktop wallet password
Password is held in memory only and not persisted on desktop
Adds minimum 8-character password validation during wallet creation
Changes initial route logic so desktop always requires unlock
Renames secure-storage helpers to clarify mobile-only scope
Fixes error handling in openExisting to check wallet errorString before assigning _w2Wallet
Evidence from the diff
The patch introduces platform-specific wallet unlock behavior. On mobile, the existing local_auth biometric/PIN flow remains. On desktop, a new CreateWalletPasswordScreen collects an 8+ character password, stored temporarily in WalletModel._desktopWalletPassword, used as the wallet file password during creation, and required via UnlockScreen to open an existing wallet. The previously auto-generated and secure-storage-backed mobile password is renamed to storeMobileWalletPassword/getMobileWalletPassword and is no longer stored or retrieved on desktop. The unlock route is now forced for desktop even when appLock is disabled. openExisting() now accepts an optional desktopWalletPassword and checks errorString() on the returned wallet object before assignment.
Changed components
lib/main.dartlib/models/wallet_model.dartlib/screens/unlock.dartlib/screens/create_wallet_password.dartlib/screens/connection_setup.dartlib/util/wallet_password.dartlib/l10n localization filesInspect captured patch +514 / −133
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index a41d2e7..52495c4 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -51,6 +51,11 @@
"unlockButton": "Unlock",
"unlockReason": "Unlock wallet",
"unlockUnableToAuthError": "Unable to authenticate.",
+ "unlockTitle": "Unlock Wallet",
+ "unlockDescription": "Enter your wallet password to unlock",
+ "unlockPasswordLabel": "Password",
+ "unlockPasswordHint": "Enter your password",
+ "unlockIncorrectPasswordError": "Incorrect password. Please try again.",
"homeConnecting": "Connecting",
"homeSyncing": "Syncing",
"homeHeight": "Height",
@@ -80,7 +85,7 @@
"settingsNotifyNewTxsLabel": "Notify New Transactions",
"settingsAppLockLabel": "App Lock",
"settingsAppLockUnlockReason": "Unlock wallet",
- "settingsAppLockUnableToAuthError": "Unable to authenticate.",
+ "settingsAppLockUnableToAuthError": "Unable to authenticate. Make sure you have device unlock set up.",
"settingsVerboseLoggingLabel": "Verbose Logging",
"settingsVerboseLoggingDescription": "Logs wallet operations to a text file in the app's data folder for debugging purposes.",
"settingsThemeLabel": "Theme",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 8f1915a..706b2ab 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -399,6 +399,36 @@ abstract class AppLocalizations {
/// **'Unable to authenticate.'**
String get unlockUnableToAuthError;
+ /// No description provided for @unlockTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Unlock Wallet'**
+ String get unlockTitle;
+
+ /// No description provided for @unlockDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your wallet password to unlock'**
+ String get unlockDescription;
+
+ /// No description provided for @unlockPasswordLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Password'**
+ String get unlockPasswordLabel;
+
+ /// No description provided for @unlockPasswordHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your password'**
+ String get unlockPasswordHint;
+
+ /// No description provided for @unlockIncorrectPasswordError.
+ ///
+ /// In en, this message translates to:
+ /// **'Incorrect password. Please try again.'**
+ String get unlockIncorrectPasswordError;
+
/// No description provided for @homeConnecting.
///
/// In en, this message translates to:
@@ -576,7 +606,7 @@ abstract class AppLocalizations {
/// No description provided for @settingsAppLockUnableToAuthError.
///
/// In en, this message translates to:
- /// **'Unable to authenticate.'**
+ /// **'Unable to authenticate. Make sure you have device unlock set up.'**
String get settingsAppLockUnableToAuthError;
/// No description provided for @settingsVerboseLoggingLabel.
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 912b61e..6fd2b84 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -167,6 +167,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get unlockUnableToAuthError => 'Unable to authenticate.';
+ @override
+ String get unlockTitle => 'Unlock Wallet';
+
+ @override
+ String get unlockDescription => 'Enter your wallet password to unlock';
+
+ @override
+ String get unlockPasswordLabel => 'Password';
+
+ @override
+ String get unlockPasswordHint => 'Enter your password';
+
+ @override
+ String get unlockIncorrectPasswordError => 'Incorrect password. Please try again.';
+
@override
String get homeConnecting => 'Connecting';
@@ -259,7 +274,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsAppLockUnlockReason => 'Unlock wallet';
@override
- String get settingsAppLockUnableToAuthError => 'Unable to authenticate.';
+ String get settingsAppLockUnableToAuthError =>
+ 'Unable to authenticate. Make sure you have device unlock set up.';
@override
String get settingsVerboseLoggingLabel => 'Verbose Logging';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index d8913f5..7a4b747 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -167,6 +167,21 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get unlockUnableToAuthError => 'Não foi possível autenticar.';
+ @override
+ String get unlockTitle => 'Desbloquear Carteira';
+
+ @override
+ String get unlockDescription => 'Digite a senha da sua carteira para desbloquear';
+
+ @override
+ String get unlockPasswordLabel => 'Senha';
+
+ @override
+ String get unlockPasswordHint => 'Digite sua senha';
+
+ @override
+ String get unlockIncorrectPasswordError => 'Senha incorreta. Tente novamente.';
+
@override
String get homeConnecting => 'Conectando';
@@ -259,7 +274,8 @@ class AppLocalizationsPt extends AppLocalizations {
String get settingsAppLockUnlockReason => 'Desbloquear carteira';
@override
- String get settingsAppLockUnableToAuthError => 'Não foi possível autenticar.';
+ String get settingsAppLockUnableToAuthError =>
+ 'Não foi possível autenticar. Verifique se o desbloqueio de tela está configurado.';
@override
String get settingsVerboseLoggingLabel => 'Logs Detalhados';
@@ -378,7 +394,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get confirmSendDescription =>
- 'As transações são irreversíveis, então certifique-se de que estes detalhes correspondem exatamente.';
+ 'As transações são irreversíveis, então verifique se estes detalhes correspondem exatamente.';
@override
String get addressBookTitle => 'Lista de Contatos';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 652d684..8fdabd7 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -51,6 +51,11 @@
"unlockButton": "Desbloquear",
"unlockReason": "Desbloquear carteira",
"unlockUnableToAuthError": "Não foi possível autenticar.",
+ "unlockTitle": "Desbloquear Carteira",
+ "unlockDescription": "Digite a senha da sua carteira para desbloquear",
+ "unlockPasswordLabel": "Senha",
+ "unlockPasswordHint": "Digite sua senha",
+ "unlockIncorrectPasswordError": "Senha incorreta. Tente novamente.",
"homeConnecting": "Conectando",
"homeSyncing": "Sincronizando",
"homeHeight": "Bloco",
@@ -80,7 +85,7 @@
"settingsNotifyNewTxsLabel": "Notificar Novas Transações",
"settingsAppLockLabel": "Desbloqueio com PIN/Biometria",
"settingsAppLockUnlockReason": "Desbloquear carteira",
- "settingsAppLockUnableToAuthError": "Não foi possível autenticar.",
+ "settingsAppLockUnableToAuthError": "Não foi possível autenticar. Verifique se o desbloqueio de tela está configurado.",
"settingsVerboseLoggingLabel": "Logs Detalhados",
"settingsVerboseLoggingDescription": "Registra operações da carteira em um arquivo de texto na pasta de dados do app para fins de depuração.",
"settingsThemeLabel": "Tema",
@@ -118,7 +123,7 @@
"secretKeysPublicViewKey": "Chave Pública de Visualização",
"scanQrTitle": "Escanear QR Code",
"confirmSendTitle": "Confirmar Envio",
- "confirmSendDescription": "As transações são irreversíveis, então certifique-se de que estes detalhes correspondem exatamente.",
+ "confirmSendDescription": "As transações são irreversíveis, então verifique se estes detalhes correspondem exatamente.",
"addressBookTitle": "Lista de Contatos",
"addressBookAddContact": "Adicionar Contato",
"addressBookEditContact": "Editar Contato",
diff --git a/lib/main.dart b/lib/main.dart
index a30c22e..43583a2 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/screens/privacy_policy.dart';
@@ -28,6 +30,7 @@ import 'package:skylight_wallet/screens/generate_seed.dart';
import 'package:skylight_wallet/screens/receive.dart';
import 'package:skylight_wallet/screens/send.dart';
import 'package:skylight_wallet/screens/create_wallet.dart';
+import 'package:skylight_wallet/screens/create_wallet_password.dart';
import 'package:skylight_wallet/screens/restore_wallet.dart';
import 'package:skylight_wallet/screens/restore_warning.dart';
import 'package:skylight_wallet/screens/wallet_home.dart';
@@ -35,6 +38,9 @@ import 'package:skylight_wallet/screens/welcome.dart';
import 'package:skylight_wallet/screens/address_book.dart';
import 'package:skylight_wallet/util/logging.dart';
+final isDesktop = Platform.isLinux || Platform.isWindows || Platform.isMacOS;
+final isMobile = Platform.isAndroid || Platform.isIOS;
+
void main() async {
WidgetsFlutterBinding.ensureInitialized();
timeago.setLocaleMessages('pt', timeago.PtBrMessages());
@@ -50,8 +56,12 @@ void main() async {
Future<bool> loadExistingWalletIfExists(WalletModel wallet) async {
if (await wallet.hasExistingWallet()) {
- await wallet.openExisting();
- await wallet.loadPersistedConnection();
+ if (isMobile) {
+ await wallet.openExisting();
+ await wallet.loadPersistedConnection();
+ wallet.load();
+ }
+
return true;
}
@@ -93,15 +103,11 @@ class MyApp extends StatelessWidget {
sharedPreferences.getBool(SharedPreferencesKeys.appLockEnabled) ?? false;
final initialRoute = walletExists
- ? appLockEnabled
+ ? appLockEnabled || isDesktop
? '/unlock'
: '/wallet_home'
: '/welcome';
- if (walletExists) {
- wallet.load();
- }
-
return MaterialApp(
title: 'Skylight Monero Wallet',
localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -123,6 +129,7 @@ class MyApp extends StatelessWidget {
routes: {
'/welcome': (context) => WelcomeScreen(),
'/connection_setup': (context) => ConnectionSetupScreen(),
+ '/create_wallet_password': (context) => CreateWalletPasswordScreen(),
'/create_wallet': (context) => CreateWalletScreen(),
'/generate_seed': (context) => GenerateSeedScreen(),
'/lws_details': (context) => LwsDetailsScreen(),
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 4ba2163..abe9767 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -148,6 +148,7 @@ class WalletModel with ChangeNotifier {
bool? _serverSupportsSubaddresses;
int? _unusedSubaddressIndex;
bool? _unusedSubaddressIndexIsSupported;
+ String? _desktopWalletPassword;
Wallet2Wallet? get w2Wallet => _w2Wallet;
bool get hasAttemptedConnection => _hasAttemptedConnection;
@@ -349,6 +350,10 @@ class WalletModel with ChangeNotifier {
notifyListeners();
}
+ void setWalletPassword(String password) {
+ _desktopWalletPassword = password;
+ }
+
Future<void> connectToDaemon() async {
if (_w2Wallet == null) throw Exception("w2wallet is null");
@@ -719,7 +724,7 @@ class WalletModel with ChangeNotifier {
isDummy: true,
);
- final walletPassword = genWalletPassword();
+ final walletPassword = _desktopWalletPassword ?? genWalletPassword();
if (legacyWallet.errorString() == '' && legacyWallet.status() == 0) {
_w2Wallet = await _getWalletFromLegacySeed(
@@ -746,7 +751,9 @@ class WalletModel with ChangeNotifier {
throw Exception('Something went wrong.');
}
- await storeWalletPassword(walletPassword);
+ if (Platform.isAndroid || Platform.isIOS) {
+ await storeMobileWalletPassword(walletPassword);
+ }
_w2TxHistory = _w2Wallet!.history();
@@ -754,10 +761,10 @@ class WalletModel with ChangeNotifier {
notifyListeners();
}
- Future<void> openExisting() async {
+ Future<void> openExisting({String? desktopWalletPassword}) async {
final path = await getWalletPath();
- final password = await getWalletPassword();
+ final password = desktopWalletPassword ?? await getMobileWalletPassword();
if (password == null) {
final errorMsg = 'Failed to open existing wallet: could not get password.';
@@ -769,18 +776,17 @@ class WalletModel with ChangeNotifier {
log(LogLevel.info, ' path: $path');
log(LogLevel.info, ' password: <hidden>');
- _w2Wallet = _w2WalletManager.openWallet(path: path, password: password);
-
- log(LogLevel.info, 'WalletManager_openWallet completed');
-
- final errorString = _w2WalletManager.errorString();
+ final w2Wallet = _w2WalletManager.openWallet(path: path, password: password);
- if (errorString != '') {
- final errorMsg = 'WalletManager_openWallet error: $errorString';
+ if (w2Wallet.errorString() != '') {
+ final errorMsg = 'WalletManager_openWallet error: ${w2Wallet.errorString()}';
log(LogLevel.error, errorMsg);
throw Exception(errorMsg);
}
+ log(LogLevel.info, 'WalletManager_openWallet completed');
+
+ _w2Wallet = w2Wallet;
_w2TxHistory = _w2Wallet!.history();
notifyListeners();
diff --git a/lib/screens/connection_setup.dart b/lib/screens/connection_setup.dart
index 1308c10..3d91524 100644
--- a/lib/screens/connection_setup.dart
+++ b/lib/screens/connection_setup.dart
@@ -205,7 +205,12 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
});
if (mounted) {
- Navigator.pushNamed(context, '/create_wallet');
+ // On desktop platforms, navigate to password screen first
+ if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
+ Navigator.pushNamed(context, '/create_wallet_password');
+ } else {
+ Navigator.pushNamed(context, '/create_wallet');
+ }
}
}
diff --git a/lib/screens/create_wallet_password.dart b/lib/screens/create_wallet_password.dart
new file mode 100644
index 0000000..0651679
--- /dev/null
+++ b/lib/screens/create_wallet_password.dart
@@ -0,0 +1,181 @@
+import 'package:flutter/material.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:provider/provider.dart';
+
+class CreateWalletPasswordScreen extends StatefulWidget {
+ const CreateWalletPasswordScreen({super.key});
+
+ @override
+ State<CreateWalletPasswordScreen> createState() => _CreateWalletPasswordScreenState();
+}
+
+class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen> {
+ final TextEditingController _passwordController = TextEditingController();
+ final TextEditingController _confirmPasswordController = TextEditingController();
+ final _formKey = GlobalKey<FormState>();
+ bool _obscurePassword = true;
+ bool _obscureConfirmPassword = true;
+ bool _isLoading = false;
+
+ @override
+ void dispose() {
+ _passwordController.dispose();
+ _confirmPasswordController.dispose();
+ super.dispose();
+ }
+
+ Future<void> _savePassword() async {
+ if (!_formKey.currentState!.validate()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ final password = _passwordController.text;
+
+ if (mounted) {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ wallet.setWalletPassword(password);
+ Navigator.pushNamed(context, '/create_wallet');
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('Failed to save password: $e')));
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ String? _validatePassword(String? value) {
+ if (value == null || value.isEmpty) {
+ return AppLocalizations.of(context)!.fieldEmptyError;
+ }
+ if (value.length < 8) {
+ return 'Password must be at least 8 characters long.';
+ }
+ return null;
+ }
+
+ String? _validateConfirmPassword(String? value) {
+ if (value == null || value.isEmpty) {
+ return AppLocalizations.of(context)!.fieldEmptyError;
+ }
+ if (value != _passwordController.text) {
+ return 'Passwords do not match.';
+ }
+ return null;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+
+ return Scaffold(
+ appBar: AppBar(title: Text('Skylight Monero Wallet')),
+ body: Center(
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 500),
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ Column(
+ spacing: 10,
+ children: [
+ Text(
+ 'Create Wallet Password',
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ Text(
+ 'Create a password to protect your wallet. This password will be required to unlock your wallet.',
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ ],
+ ),
+ Column(
+ spacing: 15,
+ children: [
+ TextFormField(
+ controller: _passwordController,
+ obscureText: _obscurePassword,
+ validator: _validatePassword,
+ decoration: InputDecoration(
+ labelText: 'Password',
+ hintText: 'Enter your password',
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ suffixIcon: IconButton(
+ icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
+ ),
+ enabled: !_isLoading,
+ ),
+ TextFormField(
+ controller: _confirmPasswordController,
+ obscureText: _obscureConfirmPassword,
+ validator: _validateConfirmPassword,
+ decoration: InputDecoration(
+ labelText: 'Confirm Password',
+ hintText: 'Confirm your password',
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscureConfirmPassword ? Icons.visibility : Icons.visibility_off,
+ ),
+ onPressed: () {
+ setState(() {
+ _obscureConfirmPassword = !_obscureConfirmPassword;
+ });
+ },
+ ),
+ ),
+ enabled: !_isLoading,
+ onFieldSubmitted: (_) => _savePassword(),
+ ),
+ FilledButton(
+ onPressed: _isLoading ? null : _savePassword,
+ child: _isLoading
+ ? SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: isDarkTheme
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.white,
+ ),
+ )
+ : Text(i18n.continueText),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/lws_details.dart b/lib/screens/lws_details.dart
index cb6108c..bc51f51 100644
--- a/lib/screens/lws_details.dart
+++ b/lib/screens/lws_details.dart
@@ -17,96 +17,85 @@ class LwsDetailsScreen extends StatelessWidget {
return Scaffold(
body: Center(
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(
- i18n.lwsDetailsTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- Text(
- i18n.lwsDetailsDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsPrimaryAddressLabel,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 480),
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ Text(i18n.lwsDetailsTitle, style: Theme.of(context).textTheme.headlineMedium),
+ Text(
+ i18n.lwsDetailsDescription,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ readOnly: true,
+ decoration: InputDecoration(
+ labelText: i18n.lwsDetailsPrimaryAddressLabel,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
+ controller: TextEditingController(text: primaryAddress),
),
- controller: TextEditingController(text: primaryAddress),
),
- ),
- IconButton(
- onPressed: () =>
- Clipboard.setData(ClipboardData(text: primaryAddress)),
- icon: Icon(Icons.copy),
- ),
- ],
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsSecretViewKeyLabel,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
+ IconButton(
+ onPressed: () => Clipboard.setData(ClipboardData(text: primaryAddress)),
+ icon: Icon(Icons.copy),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ readOnly: true,
+ decoration: InputDecoration(
+ labelText: i18n.lwsDetailsSecretViewKeyLabel,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
+ controller: TextEditingController(text: secretViewKey),
),
- controller: TextEditingController(text: secretViewKey),
),
- ),
- IconButton(
- onPressed: () =>
- Clipboard.setData(ClipboardData(text: secretViewKey)),
- icon: Icon(Icons.copy),
- ),
- ],
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsRestoreHeightLabel,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
+ IconButton(
+ onPressed: () => Clipboard.setData(ClipboardData(text: secretViewKey)),
+ icon: Icon(Icons.copy),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ readOnly: true,
+ decoration: InputDecoration(
+ labelText: i18n.lwsDetailsRestoreHeightLabel,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
- ),
- controller: TextEditingController(
- text: restoreHeight.toString(),
+ controller: TextEditingController(text: restoreHeight.toString()),
),
),
- ),
- IconButton(
- onPressed: () => Clipboard.setData(
- ClipboardData(text: restoreHeight.toString()),
+ IconButton(
+ onPressed: () =>
+ Clipboard.setData(ClipboardData(text: restoreHeight.toString())),
+ icon: Icon(Icons.copy),
),
- icon: Icon(Icons.copy),
+ ],
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/wallet_home',
+ (Route<dynamic> route) => false,
),
- ],
- ),
- FilledButton(
- onPressed: () => Navigator.pushNamedAndRemoveUntil(
- context,
- '/wallet_home',
- (Route<dynamic> route) => false,
+ child: Text(i18n.continueText),
),
- child: Text(i18n.continueText),
- ),
- ],
+ ],
+ ),
),
),
),
diff --git a/lib/screens/unlock.dart b/lib/screens/unlock.dart
index 81f3100..c1e81ba 100644
--- a/lib/screens/unlock.dart
+++ b/lib/screens/unlock.dart
@@ -1,8 +1,11 @@
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
+import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/models/wallet_model.dart';
class UnlockScreen extends StatefulWidget {
const UnlockScreen({super.key});
@@ -12,22 +15,34 @@ class UnlockScreen extends StatefulWidget {
}
class _UnlockScreenState extends State<UnlockScreen> {
+ final TextEditingController _passwordController = TextEditingController();
+ final _formKey = GlobalKey<FormState>();
+ bool _obscurePassword = true;
+ bool _isLoading = false;
+ String? _errorMessage;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (!Platform.isLinux && !Platform.isWindows && !Platform.isMacOS) {
+ _promptUnlock();
+ }
+ }
+
@override
- void initState() {
- super.initState();
- _promptUnlock();
+ void dispose() {
+ _passwordController.dispose();
+ super.dispose();
}
Future<void> _promptUnlock() async {
final auth = LocalAuthentication();
try {
+ final i18n = AppLocalizations.of(context)!;
final didAuthenticate = await auth.authenticate(
- localizedReason: 'Unlock wallet',
- options: AuthenticationOptions(
- useErrorDialogs: true,
- sensitiveTransaction: true,
- ),
+ localizedReason: i18n.unlockReason,
+ options: AuthenticationOptions(useErrorDialogs: true, sensitiveTransaction: true),
);
if (didAuthenticate) {
@@ -37,26 +52,142 @@ class _UnlockScreenState extends State<UnlockScreen> {
log(LogLevel.error, 'Unable to authenticate: ${error.toString()}');
if (mounted) {
+ final i18n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
- ).showSnackBar(SnackBar(content: Text('Unable to authenticate.')));
+ ).showSnackBar(SnackBar(content: Text(i18n.unlockUnableToAuthError)));
}
return;
}
}
+ Future<void> _unlockWithPassword() async {
+ if (!_formKey.currentState!.validate()) {
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ _errorMessage = null;
+ });
+
+ try {
+ final enteredPassword = _passwordController.text;
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+
+ await wallet.openExisting(desktopWalletPassword: enteredPassword);
+ await wallet.loadPersistedConnection();
+ await wallet.load();
+
+ if (mounted) {
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
+ }
+ } catch (e) {
+ if (mounted) {
+ final i18n = AppLocalizations.of(context)!;
+
+ setState(() {
+ _errorMessage = i18n.unlockIncorrectPasswordError;
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ String? _validatePasswordField(String? value) {
+ if (value == null || value.isEmpty) {
+ return AppLocalizations.of(context)!.fieldEmptyError;
+ }
+ return null;
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+ final isDesktop = Platform.isLinux || Platform.isWindows || Platform.isMacOS;
return Scaffold(
appBar: AppBar(title: Text('Skylight Monero Wallet')),
body: SafeArea(
child: Center(
- child: FilledButton.icon(
- onPressed: _promptUnlock,
- label: Text(i18n.unlockButton),
- icon: Icon(Icons.lock_open),
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 500),
+ padding: EdgeInsets.all(20),
+ child: isDesktop
+ ? Form(
+ key: _formKey,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ Column(
+ spacing: 10,
+ children: [
+ Text(
+ i18n.unlockTitle,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ Text(
+ i18n.unlockDescription,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ ],
+ ),
+ Column(
+ spacing: 15,
+ children: [
+ TextFormField(
+ controller: _passwordController,
+ obscureText: _obscurePassword,
+ validator: _validatePasswordField,
+ enabled: !_isLoading,
+ decoration: InputDecoration(
+ labelText: i18n.unlockPasswordLabel,
+ hintText: i18n.unlockPasswordHint,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility : Icons.visibility_off,
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
+ errorText: _errorMessage,
+ ),
+ onFieldSubmitted: (_) => _unlockWithPassword(),
+ ),
+ FilledButton(
+ onPressed: _isLoading ? null : _unlockWithPassword,
+ child: _isLoading
+ ? SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: isDarkTheme
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.white,
+ ),
+ )
+ : Text(i18n.unlockButton),
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+ : FilledButton.icon(
+ onPressed: _promptUnlock,
+ label: Text(i18n.unlockButton),
+ icon: Icon(Icons.lock_open),
+ ),
),
),
),
diff --git a/lib/screens/welcome.dart b/lib/screens/welcome.dart
index a3ee4c5..6db21be 100644
--- a/lib/screens/welcome.dart
+++ b/lib/screens/welcome.dart
@@ -27,11 +27,7 @@ class _WalletHomeScreenState extends State<WelcomeScreen> {
await wallet.loadPersistedConnection();
if (mounted) {
- Navigator.pushNamedAndRemoveUntil(
- context,
- '/wallet_home',
- (Route<dynamic> route) => false,
- );
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
}
}
}
@@ -47,10 +43,7 @@ class _WalletHomeScreenState extends State<WelcomeScreen> {
mainAxisAlignment: MainAxisAlignment.center,
spacing: 20,
children: [
- Text(
- i18n.welcomeTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
+ Text(i18n.welcomeTitle, style: Theme.of(context).textTheme.headlineMedium),
Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
@@ -60,8 +53,7 @@ class _WalletHomeScreenState extends State<WelcomeScreen> {
),
),
FilledButton(
- onPressed: () =>
- Navigator.pushNamed(context, '/connection_setup'),
+ onPressed: () => Navigator.pushNamed(context, '/connection_setup'),
child: Text(i18n.welcomeGetStarted),
),
],
diff --git a/lib/util/wallet_password.dart b/lib/util/wallet_password.dart
index 31e8efd..1beaced 100644
--- a/lib/util/wallet_password.dart
+++ b/lib/util/wallet_password.dart
@@ -7,9 +7,7 @@ import 'package:skylight_wallet/consts.dart';
String genWalletPassword() {
final byteLength = 16;
final rand = Random.secure();
- final bytes = Uint8List.fromList(
- List<int>.generate(byteLength, (_) => rand.nextInt(256)),
- );
+ final bytes = Uint8List.fromList(List<int>.generate(byteLength, (_) => rand.nextInt(256)));
final sb = StringBuffer();
for (final b in bytes) {
sb.write(b.toRadixString(16).padLeft(2, '0'));
@@ -17,12 +15,12 @@ String genWalletPassword() {
return sb.toString();
}
-Future<void> storeWalletPassword(String password) async {
+Future<void> storeMobileWalletPassword(String password) async {
final storage = FlutterSecureStorage();
await storage.write(key: walletPasswordStorageKey, value: password);
}
-Future<String?> getWalletPassword() async {
+Future<String?> getMobileWalletPassword() async {
final storage = FlutterSecureStorage();
return storage.read(key: walletPasswordStorageKey);
}
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.