Fix create password screen showing up on mobile; format
What changed, and why it matters
This commit fixes a UI bug where mobile users were incorrectly shown a 'create wallet password' screen that should only appear on desktop. On mobile, the app now skips that screen and creates or restores the wallet directly, relying on the device's own lock instead. Most of the changes are just code formatting.
Treat as a normal bug-fix/UI-flow patch. Review whether removing the in-app password on mobile matches the product's security model and ensure the device lock requirement is enforced/documented. No urgent security action required based on the diff alone.
Security signals we found
Flow change: mobile wallet creation/restoration bypasses app-level password screen
Mobile now relies on device app lock instead of an in-app password
Duplicate-submission guard added via _committing flag
No cryptographic, authentication, or input-validation changes observed
Evidence from the diff
The patch changes wallet creation and restoration flows so that on mobile (non-desktop) the app no longer pushes /create_wallet_password. Instead it calls commitGeneratedWallet()/restoreWallet() directly and then navigates to /wallet_home or /lws_details. A loading flag prevents duplicate submissions. The remaining diffs are formatting-only line wrapping.
Changed components
lib/screens/generate_seed.dartlib/screens/restore_wallet.dartlib/screens/lws_details.dartlib/screens/send.dartlib/screens/settings.dartlib/screens/tor_settings.dartlib/screens/wallet_home.dartlib/screens/address_book.dartlib/models/fiat_rate_model.dartInspect captured patch +163 / −72
### lib/models/fiat_rate_model.dart
@@ -3,4 +3,5 @@
// sites are unchanged; the app supplies the Tor proxy via FiatRates.install and
// attaches the WalletManager via attachFiatWalletManager in wallet_core_glue.dart.
// The fiatAutoDisabledByTor auto-disable/restore stays app-side (tor_settings_form).
-export 'package:wallet_fiat/wallet_fiat.dart' show FiatRateModel, FiatApiMode, FiatCurrency, FiatQuote;
+export 'package:wallet_fiat/wallet_fiat.dart'
+ show FiatRateModel, FiatApiMode, FiatCurrency, FiatQuote;
### lib/screens/address_book.dart
@@ -61,39 +61,44 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
return SafeArea(
top: false,
child: Padding(
- padding: EdgeInsets.fromLTRB(hpad, isDesktopModal ? 0 : 8, hpad, isDesktopModal ? 0 : 12),
+ padding: EdgeInsets.fromLTRB(
+ hpad,
+ isDesktopModal ? 0 : 8,
+ hpad,
+ isDesktopModal ? 0 : 12,
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SheetHandle(),
- Row(
- children: [
- SheetIcon(
- icon: Icons.delete_outline,
- bg: BrandColors.errorBg,
- color: BrandColors.error,
- ),
- const SizedBox(width: 11),
- Text(i18n.addressBookDeleteContact, style: BrandText.sheetTitle),
- ],
- ),
- const SizedBox(height: 7),
- Text(
- i18n.addressBookDeleteContactConfirmation(contact.name),
- style: BrandText.bodyMuted.copyWith(fontSize: 13, height: 1.5),
- ),
- const SizedBox(height: 18),
- BrandButton(label: i18n.cancel, onPressed: () => Navigator.pop(sheetContext)),
- const SizedBox(height: 4),
- BrandButton.ghost(
- label: i18n.addressBookDelete,
- color: BrandColors.error,
- onPressed: () {
- Provider.of<ContactModel>(context, listen: false).deleteContact(contact.id);
- Navigator.pop(sheetContext);
- },
- ),
+ Row(
+ children: [
+ SheetIcon(
+ icon: Icons.delete_outline,
+ bg: BrandColors.errorBg,
+ color: BrandColors.error,
+ ),
+ const SizedBox(width: 11),
+ Text(i18n.addressBookDeleteContact, style: BrandText.sheetTitle),
+ ],
+ ),
+ const SizedBox(height: 7),
+ Text(
+ i18n.addressBookDeleteContactConfirmation(contact.name),
+ style: BrandText.bodyMuted.copyWith(fontSize: 13, height: 1.5),
+ ),
+ const SizedBox(height: 18),
+ BrandButton(label: i18n.cancel, onPressed: () => Navigator.pop(sheetContext)),
+ const SizedBox(height: 4),
+ BrandButton.ghost(
+ label: i18n.addressBookDelete,
+ color: BrandColors.error,
+ onPressed: () {
+ Provider.of<ContactModel>(context, listen: false).deleteContact(contact.id);
+ Navigator.pop(sheetContext);
+ },
+ ),
],
),
),
@@ -798,11 +803,7 @@ class _ContactSheetState extends State<_ContactSheet> {
Expanded(
child: Text(
shortenMiddle(address, head: 12, tail: 12),
- style: TextStyle(
- fontFamily: 'Ubuntu Mono',
- fontSize: 12.5,
- color: BrandColors.ink,
- ),
+ style: TextStyle(fontFamily: 'Ubuntu Mono', fontSize: 12.5, color: BrandColors.ink),
),
),
const SizedBox(width: 11),
### lib/screens/generate_seed.dart
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
import 'package:wallet_domain/wallet_domain.dart' show SeedSource;
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/screens/create_wallet_password.dart';
+import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
@@ -20,6 +23,7 @@ class GenerateSeedScreen extends StatefulWidget {
class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScreenMixin {
List<String>? _seed;
({SeedSource seed, DateTime restoreDate})? _generated;
+ bool _committing = false;
@override
void initState() {
@@ -31,19 +35,58 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScre
_seed = generated.seed.mnemonic.split(' ');
}
- /// Carry the generated seed to the password step, which sets the password and
- /// writes the wallet.
+ /// Desktop adds a password step (password-last flow). Mobile has no password
+ /// screen — it's guarded by the device app lock — so it commits the wallet
+ /// directly from here.
void _continue() {
final generated = _generated;
if (generated == null) return;
- Navigator.pushNamed(
- context,
- '/create_wallet_password',
- arguments: CreateWalletPasswordArgs(
- commit: (ctx) =>
- commitGeneratedWallet(ctx, seed: generated.seed, restoreDate: generated.restoreDate),
- ),
- );
+ if (isDesktop) {
+ Navigator.pushNamed(
+ context,
+ '/create_wallet_password',
+ arguments: CreateWalletPasswordArgs(
+ commit: (ctx) =>
+ commitGeneratedWallet(ctx, seed: generated.seed, restoreDate: generated.restoreDate),
+ ),
+ );
+ return;
+ }
+ _commitOnMobile(generated);
+ }
+
+ Future<void> _commitOnMobile(({SeedSource seed, DateTime restoreDate}) generated) async {
+ if (_committing) return;
+ setState(() => _committing = true);
+ try {
+ final restoreHeight = await commitGeneratedWallet(
+ context,
+ seed: generated.seed,
+ restoreDate: generated.restoreDate,
+ );
+ if (!mounted) return;
+ Provider.of<FiatRateModel>(context, listen: false).startService();
+ if (appWalletOf(context).isNodeMode) {
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (route) => false);
+ } else {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/lws_details',
+ (route) => false,
+ arguments: restoreHeight,
+ );
+ }
+ } catch (error) {
+ if (!mounted) return;
+ setState(() => _committing = false);
+ var errorMsg = 'Sorry, something went wrong.';
+ if (error.toString().contains('failedToLoadHeight')) {
+ errorMsg = 'Check your internet connection.';
+ } else {
+ log(LogLevel.error, error.toString());
+ }
+ showBrandToast(context, errorMsg);
+ }
}
@override
@@ -80,6 +123,7 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScre
seedWords: _seed,
birthdayCard: null,
onContinue: _continue,
+ continueLoading: _committing,
labels: GenerateSeedLabels(
titleCovered: i18n.generateSeedTitleCovered,
titleRevealed: i18n.generateSeedTitle,
### lib/screens/lws_details.dart
@@ -53,11 +53,8 @@ class _LwsDetailsScreenState extends State<LwsDetailsScreen> with SecureScreenMi
warning: i18n.lwsKeysWarning,
);
- void goHome() => Navigator.pushNamedAndRemoveUntil(
- context,
- '/wallet_home',
- (Route<dynamic> route) => false,
- );
+ void goHome() =>
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
// Desktop: an unnumbered onboarding step — the two-pane chrome carries the
// title/description/warning, the content slot shows just the value cards.
### lib/screens/restore_wallet.dart
@@ -5,10 +5,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
import 'package:polyseed/polyseed.dart';
+import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/screens/create_wallet_password.dart';
import 'package:skylight_wallet/util/get_height_by_date.dart';
+import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
@@ -33,6 +36,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
bool _scanChosen = false;
bool _heightManuallySet = false; // date picked / QR height
String _seedTypeId = 'polyseed'; // the view's default (first seed type)
+ bool _committing = false; // mobile: restoring the wallet directly (no password step)
bool _validWord(String w) => _wordSet.contains(w);
@@ -119,16 +123,59 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
}
final restoreHeight = _restoreHeight;
- Navigator.pushNamed(
- context,
- '/create_wallet_password',
- arguments: CreateWalletPasswordArgs(
- commit: (ctx) async {
- await restoreWallet(ctx, mnemonic: mnemonic, restoreHeight: restoreHeight);
- return restoreHeight;
- },
- ),
- );
+ // Desktop adds a password step; mobile has no password screen (the device
+ // app lock guards it), so it restores the wallet directly.
+ if (isDesktop) {
+ Navigator.pushNamed(
+ context,
+ '/create_wallet_password',
+ arguments: CreateWalletPasswordArgs(
+ commit: (ctx) async {
+ await restoreWallet(ctx, mnemonic: mnemonic, restoreHeight: restoreHeight);
+ return restoreHeight;
+ },
+ ),
+ );
+ return;
+ }
+ _restoreOnMobile(mnemonic, restoreHeight);
+ }
+
+ Future<void> _restoreOnMobile(String mnemonic, int restoreHeight) async {
+ if (_committing) return;
+ final i18n = AppLocalizations.of(context)!;
+ setState(() => _committing = true);
+ try {
+ await restoreWallet(context, mnemonic: mnemonic, restoreHeight: restoreHeight);
+ } on Exception catch (error) {
+ if (!mounted) return;
+ setState(() => _committing = false);
+ final errorMsg = error.toString().replaceFirst('Exception: ', '');
+ showBrandToast(
+ context,
+ errorMsg == 'Invalid mnemonic.' ? i18n.restoreWalletInvalidMnemonic : i18n.unknownError,
+ );
+ return;
+ } catch (error) {
+ if (!mounted) return;
+ log(LogLevel.error, error.toString());
+ setState(() => _committing = false);
+ showBrandToast(context, i18n.unknownError);
+ return;
+ }
+ if (!mounted) return;
+ setState(() => _committing = false);
+ Provider.of<FiatRateModel>(context, listen: false).startService();
+ if (appWalletOf(context).isNodeMode) {
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (route) => false);
+ } else {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/lws_details',
+ (route) => false,
+ arguments: restoreHeight,
+ );
+ }
}
@override
@@ -150,6 +197,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
final view = RestoreWalletView(
controller: _restoreWalletController,
+ restoring: _committing,
embedded: isDesktop,
stepCount: isDesktop ? null : 6,
stepIndex: isDesktop ? null : 4,
### lib/screens/send.dart
@@ -63,6 +63,7 @@ class _SendScreenState extends State<SendScreen> {
bool _isLoading = false;
bool _isLoadingFees = false;
final _destinationAddressController = TextEditingController(text: '');
+
/// The amount, typed in XMR or fiat. Read what is spent from
/// `_amount.baseUnits` / `_amount.coinText`, never from its field.
late final AmountEntryController _amount;
@@ -775,7 +776,11 @@ class _SendScreenState extends State<SendScreen> {
),
),
const SizedBox(height: 18),
- BrandButton(label: i18n.sendSendButton, loading: _isLoading, onPressed: canSend ? _send : null),
+ BrandButton(
+ label: i18n.sendSendButton,
+ loading: _isLoading,
+ onPressed: canSend ? _send : null,
+ ),
const SizedBox(height: 9),
BrandButton.ghost(label: i18n.cancel, onPressed: () => Navigator.pop(context)),
const SizedBox(height: 16),
@@ -915,10 +920,7 @@ class _SendScreenState extends State<SendScreen> {
);
}
- Widget _dAmountCard(
- AppLocalizations i18n,
- double available,
- ) {
+ Widget _dAmountCard(AppLocalizations i18n, double available) {
return _dCard(
child: ListenableBuilder(
listenable: _amount,
### lib/screens/settings.dart
@@ -615,10 +615,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (widget.asModal) return _modalBody(i18n, groups, versionFooter);
final tiles = <Widget>[
- for (var i = 0; i < groups.length; i++) ...[
- if (i > 0) const SizedBox(height: 18),
- groups[i],
- ],
+ for (var i = 0; i < groups.length; i++) ...[if (i > 0) const SizedBox(height: 18), groups[i]],
const SizedBox(height: 16),
versionFooter,
];
### lib/screens/tor_settings.dart
@@ -61,9 +61,7 @@ class TorSettingsScreen extends StatelessWidget {
noTor: i18n.torSettingsModeDisabled,
noTorDesc: i18n.torChoiceNoTorDesc,
socksPortLabel: i18n.torSettingsSocksPortLabel,
- orbotLabel: Platform.isIOS
- ? i18n.torSettingsUseOrbotLabelIos
- : i18n.torSettingsUseOrbotLabel,
+ orbotLabel: Platform.isIOS ? i18n.torSettingsUseOrbotLabelIos : i18n.torSettingsUseOrbotLabel,
testButton: i18n.torSettingsTestConnectionButton,
connected: i18n.torChoiceConnected,
testFailed: i18n.torChoiceTestFailed,
### lib/screens/wallet_home.dart
@@ -251,7 +251,10 @@ class _BalanceHero extends StatelessWidget {
// Fiat leads when available; otherwise the coin amount is the hero,
// with a skeleton while the rate is still loading.
if (showFiat)
- BalanceText.split(formatFiat(unlockedBalanceFiat!, fiatRate.fiatCurrency), style: _balanceStyle)
+ BalanceText.split(
+ formatFiat(unlockedBalanceFiat!, fiatRate.fiatCurrency),
+ style: _balanceStyle,
+ )
else if (!fiatRate.isDisabled && !fiatRate.hasFailed)
Skeletonizer(child: Text('0.0000', style: _balanceStyle))
elseWhy this scored 18/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.