trezor-monero-passphrase-modal (#3337)

* trezor monero passphrase modal

* feat: add Trezor passphrase support for wallet restoration process

* revert: send page changes

* chore: format code

* chore: format code

* chore: reorganize imports and improve code formatting in send_page.dart [skip ci]

* auto-reformat

* fix: trezor usb device refresh race condition

* fix: ui bugs in relation to trezor

* fix: only show passphrase option for trezor devices

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Konstantin Ullrich
2026-07-17 17:52:52 +03:00
committed by GitHub
co-authored by Robert Malikowski Omar Hatem
parent 03edd658a5
commit 9d685d4645
17 changed files with 319 additions and 172 deletions
@@ -142,12 +142,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
super.dispose();
}
var _isRefreshingUsb = false;
Future<void> _refreshUsbDevices() async {
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) {
setState(() => usbDevices = dev);
if (_isRefreshingUsb) {
return;
}
_isRefreshingUsb = true;
try {
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) {
setState(() => usbDevices = dev);
}
} catch(e) {
printV(e);
}
_isRefreshingUsb = false;
}
Future<void> _refreshBleDevices() async {
@@ -1,19 +1,22 @@
import 'package:cake_wallet/core/wallet_name_validator.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import "dart:async";
import "package:cake_wallet/core/wallet_name_validator.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/screens/base_page.dart";
import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/base_text_form_field.dart";
import "package:cake_wallet/src/widgets/blockchain_height_widget.dart";
import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart";
import "package:cake_wallet/utils/responsive_layout_util.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart";
import "package:cw_core/generate_name.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
class MoneroHardwareWalletOptionsPage extends BasePage {
MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM);
@@ -42,13 +45,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
: _formKey = GlobalKey<FormState>(),
_blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
_blockHeightFocusNode = FocusNode(),
_controller = TextEditingController();
_walletNameController = TextEditingController(),
_passphraseController = TextEditingController();
final GlobalKey<FormState> _formKey;
final GlobalKey<BlockchainHeightState> _blockchainHeightKey;
final FocusNode _blockHeightFocusNode;
final WalletHardwareRestoreViewModel _walletHardwareRestoreVM;
final TextEditingController _controller;
final TextEditingController _walletNameController;
final TextEditingController _passphraseController;
@override
void initState() {
@@ -57,131 +62,159 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Center(
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(top: 0),
child: Form(
key: _formKey,
child: Stack(
alignment: Alignment.centerRight,
children: [
BaseTextFormField(
onChanged: (value) => _walletHardwareRestoreVM.name = value,
controller: _controller,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 20.0,
fontWeight: FontWeight.w600,
),
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
hintText: S.of(context).wallet_name,
suffixIcon: Semantics(
label: S.of(context).generate_name,
child: IconButton(
onPressed: _onGenerateName,
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection(
contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.zero,
child: Form(
key: _formKey,
child: Stack(
alignment: Alignment.centerRight,
children: [
BaseTextFormField(
onChanged: (value) => _walletHardwareRestoreVM.name = value,
controller: _walletNameController,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
),
width: 34,
height: 34,
child: Image.asset(
'assets/images/refresh_icon.png',
color: Theme.of(context).colorScheme.primary,
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
hintText: S.of(context).wallet_name,
suffixIcon: Semantics(
label: S.of(context).generate_name,
child: IconButton(
onPressed: _onGenerateName,
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
width: 34,
height: 34,
child: Image.asset(
"assets/images/refresh_icon.png",
color: Theme.of(context).colorScheme.primary,
),
),
),
),
validator: WalletNameValidator(),
),
validator: WalletNameValidator(),
),
],
],
),
),
),
),
Padding(
padding: EdgeInsets.only(top: 20),
child: BlockchainHeightWidget(
focusNode: _blockHeightFocusNode,
key: _blockchainHeightKey,
hasDatePicker: true,
walletType: WalletType.monero,
Padding(
padding: const EdgeInsets.only(top: 20),
child: BlockchainHeightWidget(
focusNode: _blockHeightFocusNode,
key: _blockchainHeightKey,
hasDatePicker: true,
walletType: WalletType.monero,
),
),
],
),
),
),
bottomSectionPadding: const EdgeInsets.all(24),
bottomSection: Observer(
builder: (_) => Column(
children: [
if (_walletHardwareRestoreVM.passphraseAvailable) ...[
TextButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => MoneroHardwareWalletPassphraseInputModal(
controller: _passphraseController,
),
);
},
child: Text(S.of(context).add_a_passphrase),
),
],
LoadingPrimaryButton(
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
),
],
),
),
),
bottomSectionPadding: EdgeInsets.all(24),
bottomSection: Observer(
builder: (context) => LoadingPrimaryButton(
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
),
),
),
);
}
);
Future<void> _onGenerateName() async {
final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_controller.text = rName;
_walletNameController.text = rName;
_walletHardwareRestoreVM.name = rName;
_controller.selection =
TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
_walletNameController.selection =
TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length));
});
}
Future<void> _confirmForm() async {
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
alertBarrierDismissible: false,
buttonAction: () => Navigator.of(context).pop(),
unawaited(
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
alertBarrierDismissible: false,
buttonAction: () => Navigator.of(context).pop(),
),
),
);
final options = {'height': _blockchainHeightKey.currentState?.height ?? -1};
final options = <String, dynamic>{"height": _blockchainHeightKey.currentState?.height ?? -1};
if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) {
options["passphrase"] = _passphraseController.text;
}
await _walletHardwareRestoreVM.create(options: options);
}
bool _effectsInstalled = false;
void _setEffects(BuildContext context) {
if (_effectsInstalled) return;
if (_effectsInstalled) {
return;
}
reaction((_) => _walletHardwareRestoreVM.error, (String? error) {
reaction((_) => _walletHardwareRestoreVM.error, (error) {
if (error != null) {
if (error == S.current.ledger_connection_error) Navigator.of(context).pop();
if (error == S.current.ledger_connection_error) {
Navigator.of(context).pop();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: error,
buttonText: S.of(context).ok,
@@ -0,0 +1,54 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/base_text_form_field.dart";
import "package:flutter/material.dart";
class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key});
final TextEditingController controller;
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
color: Theme.of(context).colorScheme.surface,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: S.of(context).passphrase_entry,
leadingIcon: const Icon(Icons.close),
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 12,
children: [
BaseTextFormField(
controller: controller,
hintText: S.of(context).passphrase_raw,
),
const SizedBox(),
NewPrimaryButton(
onPressed: Navigator.of(context).pop,
text: S.of(context).restore_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(),
],
),
),
],
),
),
),
);
}
@@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage {
children: [
Observer(
builder: (context) {
return walletRestoreViewModel.mode == WalletRestoreMode.seed
return walletRestoreViewModel.passphraseAvailable
? StandardCheckbox(
captionColor: Theme.of(context).colorScheme.onSecondaryContainer,
value: walletRestoreViewModel.hasPassphrase,