Cw 1624 trezor passphrase on trezor device instead of app (#3601)

* feat: add support for Trezor device settings configuration and `AwaitingSettings` state in pairing flow

* fix ugly widgets

* trezor options ui

* feat: add support for Trezor on device passphrases

* chore: bump trezor_flutter deps

* fix: remove unused subtitle property in proceed_on_device_sheet [skip ci]

* chore: update trezor_flutter dependency to latest commit [skip ci]

* fix: set restore height and store wallet during initialization steps

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
This commit is contained in:
Konstantin Ullrich
2026-09-18 02:27:32 +02:00
committed by GitHub
co-authored by Robert Malikowski
parent dd58455ea6
commit 98f58c2898
50 changed files with 741 additions and 295 deletions
@@ -5,12 +5,16 @@ class ListItemToggle extends ListItem {
const ListItemToggle({
required super.keyValue,
required super.label,
this.subtitle,
this.iconPath,
required this.value,
required this.onChanged,
this.leadingEndWidget,
});
final String? subtitle;
final String? iconPath;
final ValueChanged<bool> onChanged;
final Widget? leadingEndWidget;
final bool value;
}
}
+54
View File
@@ -0,0 +1,54 @@
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class BorderedSvgIcon extends StatelessWidget {
const BorderedSvgIcon({
required this.iconPath,
required this.iconSize,
Color? iconColor,
Color? borderColor,
this.borderSize = 6,
super.key,
}) : _iconColor = iconColor,
_borderColor = borderColor;
final String iconPath;
final Color? _iconColor;
final double iconSize;
final Color? _borderColor;
final double borderSize;
@override
Widget build(BuildContext context) {
final iconColor = _iconColor ?? Theme.of(context).colorScheme.onSurface;
final borderColor = _borderColor ?? Theme.of(context).colorScheme.surface;
return SizedBox(
width: iconSize + borderSize * 2,
height: iconSize + borderSize * 2,
child: Stack(
alignment: Alignment.center,
children: [
CakeImageWidget(
imageUrl: iconPath,
width: iconSize + borderSize * 2,
height: iconSize + borderSize * 2,
colorFilter: ColorFilter.mode(
borderColor,
BlendMode.srcIn,
),
),
CakeImageWidget(
imageUrl: iconPath,
width: iconSize,
height: iconSize,
colorFilter: ColorFilter.mode(
iconColor,
BlendMode.srcIn,
),
),
],
),
);
}
}
@@ -1,25 +1,30 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/digit_input.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.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/new-ui/widgets/send_page/directional_switcher.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/bordered_svg.dart";
import "package:cake_wallet/new-ui/widgets/digit_input.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.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/new-ui/widgets/send_page/directional_switcher.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
import "package:cw_core/wallet_info.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:mobx/mobx.dart";
class HardwareWalletProceedOnDeviceSheet extends StatefulWidget {
const HardwareWalletProceedOnDeviceSheet({
super.key,
required this.hardwareWalletType,
required this.trezorConnectVM,
required this.onRetry,
super.key,
});
final HardwareWalletType hardwareWalletType;
@@ -51,7 +56,9 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
_paringState = widget.trezorConnectVM.paringState;
paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) {
if (paringState is SuccessTrezorParingState) {
if (mounted) Navigator.of(context).pop();
if (mounted) {
Navigator.of(context).pop();
}
return;
}
setState(() => _paringState = paringState);
@@ -67,111 +74,135 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
bool get _isAwaitingPin => _paringState == TrezorParingState.enterPin;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: SafeArea(
bottom: false,
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
Widget build(BuildContext context) => PopScope(
canPop: false,
child: SafeArea(
bottom: false,
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
children: [
ModalTopBar(
title: "",
leadingWidget: AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
duration: pinOpenDuration,
child: Text(
key: ValueKey(pageTitle),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
pageTitle,
),
),
trailingIcon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).are_you_sure_exit,
alertContent: S.of(context).hww_exit_desc,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).yes_exit,
rightAlertButtonStyle: AlertButtonStyle.error(context),
actionLeftButton: Navigator.of(context).pop,
actionRightButton: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
Expanded(
child: DirectionalAnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: content,
),
),
Padding(
padding: EdgeInsets.only(bottom: 24, right: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(),
AnimatedOpacity(
curve: Curves.easeOutQuad,
opacity: hasFullPin ? 1 : 0,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
children: [
ModalTopBar(
title: "",
leadingWidget: AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: pinOpenDuration,
child: ModernButton(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.onPrimary,
size: 36,
icon: Icon(Icons.arrow_forward),
semanticLabel: S.of(context).confirm,
onPressed: () =>
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
child: Text(
key: ValueKey(pageTitle),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
pageTitle,
),
),
],
),
)
],
trailingIcon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).are_you_sure_exit,
alertContent: S.of(context).hww_exit_desc,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).yes_exit,
rightAlertButtonStyle: AlertButtonStyle.error(context),
actionLeftButton: Navigator.of(context).pop,
actionRightButton: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
Expanded(
child: DirectionalAnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: content,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 24, right: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
AnimatedOpacity(
curve: Curves.easeOutQuad,
opacity: hasFullPin ? 1 : 0,
duration: pinOpenDuration,
child: ModernButton(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.onPrimary,
size: 36,
icon: const Icon(Icons.arrow_forward),
semanticLabel: S.of(context).confirm,
onPressed: () =>
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
),
),
],
),
),
],
),
),
),
)),
),
),
),
);
}
);
Widget get content {
if (_paringState is InitialTrezorParingState || _paringState is EnterPinTrezorParingState) {
return _enterPinCode();
}
if (_paringState is VerifyingPinTrezorParingState) return _verifyingCode();
if (_paringState is FailTrezorParingState)
return _errorBox((_paringState as FailTrezorParingState).message);
return SizedBox.shrink(key: ValueKey(3));
}
Widget get content => switch (_paringState) {
InitialTrezorParingState() || EnterPinTrezorParingState() => PinEntryWidget(
title: S.of(context).proceed_on_device,
description: S.of(context).proceed_on_device_description,
pinOpenDuration: pinOpenDuration,
pinLength: pinLength,
isAwaitingPin: _isAwaitingPin,
controller: _controller,
iconPath: hardwareWalletIcon ?? "",
key: const ValueKey(0),
),
VerifyingPinTrezorParingState() => const VerifyingProgressIndicator(
key: ValueKey(1),
),
FailTrezorParingState(:final message) => ConnectionErrorWidget(
error: message,
onRetryPressed: retry,
key: const ValueKey(2),
),
AwaitingSettingsTrezorParingState(:final isAutoPairingAvailable) => WalletOptionsScreen(
trezorConnectVM: widget.trezorConnectVM,
iconPath: hardwareWalletIcon ?? "",
isAutoPairingAvailable: isAutoPairingAvailable,
key: const ValueKey(3),
),
AwaitingPassphraseTrezorParingState() => PinEntryWidget(
title: S.of(context).proceed_on_device,
description: S.of(context).proceed_on_device_description,
pinOpenDuration: pinOpenDuration,
pinLength: pinLength,
isAwaitingPin: false,
controller: _controller,
iconPath: hardwareWalletIcon ?? "",
key: const ValueKey(4),
),
_ => const SizedBox.shrink(
key: ValueKey(5),
),
};
void retry() {
_controller.text = "";
@@ -179,8 +210,22 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
}
String get pageTitle {
if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation;
if (_isAwaitingPin) return S.of(context).pairing_code;
if (_paringState is InitialTrezorParingState) {
return S.of(context).device_confirmation;
}
if (_isAwaitingPin) {
return S.of(context).pairing_code;
}
if (_paringState is AwaitingSettingsTrezorParingState) {
return S.of(context).other_device_settings;
}
if (_paringState is AwaitingPassphraseTrezorParingState) {
return S.of(context).passphrase_entry;
}
return "";
}
@@ -202,34 +247,17 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
return "assets/images/hardware_wallet/device_qr.svg";
}
}
}
Widget _verifyingCode() => Container(
key: ValueKey(1),
width: MediaQuery.of(context).size.width,
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CupertinoActivityIndicator(radius: 36),
const SizedBox(),
Text(
"${S.of(context).verifying_code}...",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
Text(
S.of(context).this_can_take_few_seconds,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
);
class ConnectionErrorWidget extends StatelessWidget {
const ConnectionErrorWidget({required this.error, required this.onRetryPressed, super.key});
Widget _errorBox(String errorText) => Container(
key: ValueKey(2),
final VoidCallback onRetryPressed;
final String error;
@override
Widget build(BuildContext context) => SizedBox(
key: key,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
@@ -253,7 +281,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
textAlign: TextAlign.center,
),
Text(
errorText,
error,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
@@ -263,28 +291,80 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
),
const Spacer(),
NewPrimaryButton(
onPressed: retry,
onPressed: onRetryPressed,
text: S.of(context).try_again,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
)
),
],
),
),
);
}
Widget _enterPinCode() => Column(
key: ValueKey(0),
class VerifyingProgressIndicator extends StatelessWidget {
const VerifyingProgressIndicator({super.key});
@override
Widget build(BuildContext context) => SizedBox(
key: key,
width: MediaQuery.of(context).size.width,
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CupertinoActivityIndicator(radius: 36),
const SizedBox(),
Text(
"${S.of(context).verifying_code}...",
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
Text(
S.of(context).this_can_take_few_seconds,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
class PinEntryWidget extends StatelessWidget {
const PinEntryWidget({
required this.pinOpenDuration,
required this.pinLength,
required this.controller,
required this.title,
required this.description,
required this.iconPath,
required this.isAwaitingPin,
super.key,
});
final DigitInputController controller;
final String iconPath;
final bool isAwaitingPin;
final int pinLength;
final Duration pinOpenDuration;
final String title;
final String description;
@override
Widget build(BuildContext context) => Column(
key: key,
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: pinOpenDuration,
curve: Curves.easeOutCubic,
width: _isAwaitingPin ? 40 : 100,
height: _isAwaitingPin ? 40 : 100,
width: isAwaitingPin ? 40 : 100,
height: isAwaitingPin ? 40 : 100,
child: CakeImageWidget(
imageUrl: hardwareWalletIcon,
imageUrl: iconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
@@ -295,46 +375,191 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
duration: pinOpenDuration,
switchInCurve: Curves.easeInCubic,
switchOutCurve: Curves.easeOutCubic,
child: _isAwaitingPin
child: isAwaitingPin
? Column(
spacing: 24,
key: ValueKey(1),
key: const ValueKey(1),
children: [
Text(
textAlign: TextAlign.center,
"${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}",
),
DigitInput(
controller: _controller,
controller: controller,
desiredLength: pinLength,
)
),
],
)
: Column(
key: ValueKey(0),
key: const ValueKey(0),
spacing: 12,
children: [
Text(
S.of(context).proceed_on_device,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
),
title,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
S.of(context).proceed_on_device_description,
description,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
),
],
),
)
),
],
);
}
class WalletOptionsScreen extends StatefulWidget {
const WalletOptionsScreen({
required this.trezorConnectVM,
required this.isAutoPairingAvailable,
required this.iconPath,
super.key,
});
final String iconPath;
final TrezorConnectViewModelBase trezorConnectVM;
final bool isAutoPairingAvailable;
@override
State<WalletOptionsScreen> createState() => _WalletOptionsScreenState();
}
class _WalletOptionsScreenState extends State<WalletOptionsScreen> {
bool _autoConnect = true;
bool _usePassphrase = false;
bool _setPassphraseOnDevice = false;
final _passphraseController = TextEditingController();
bool get anythingSelected => _autoConnect || _usePassphrase;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 32,
children: [
Column(
spacing: 12,
children: [
SizedBox(
width: 100,
height: 116,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: CakeImageWidget(
imageUrl: widget.iconPath,
width: 100,
height: 100,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
),
const Positioned(
right: 0,
bottom: 0,
child: BorderedSvgIcon(
iconPath: "assets/new-ui/cog.svg",
iconSize: 36,
),
),
],
),
),
const SizedBox.shrink(),
Text(
S.of(context).almost_ready,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
S.of(context).hww_options_desc,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
),
NewListSections(
sections: {
if (widget.isAutoPairingAvailable)
"auto": [
ListItemToggle(
value: _autoConnect,
onChanged: (val) => setState(() => _autoConnect = val),
keyValue: "autoconnect",
label: S.of(context).auto_connect,
),
],
"pass": [
ListItemToggle(
value: _usePassphrase,
onChanged: (val) => setState(() => _usePassphrase = val),
keyValue: "passphrase",
label: S.of(context).passphrase,
subtitle: S.of(context).wallet_has_passphrase,
),
if (_usePassphrase) ...[
ListItemToggle(
value: _setPassphraseOnDevice,
onChanged: (val) => setState(() => _setPassphraseOnDevice = val),
keyValue: "passphrase on device",
label: S.of(context).enter_passphrase_on_device,
),
if (!_setPassphraseOnDevice)
ListItemRegularRow(
keyValue: "passphrase on app",
label: S.of(context).passphrase_raw,
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => MoneroHardwareWalletPassphraseInputModal(
controller: _passphraseController,
),
);
},
),
]
],
},
),
],
),
),
NewPrimaryButton(
onPressed: () => widget.trezorConnectVM.setDeviceSettings(
TrezorDeviceSettings(
enableAutoParing: _autoConnect,
passphraseOnDevice: _usePassphrase && _setPassphraseOnDevice,
passphrase: _usePassphrase && !_setPassphraseOnDevice ? _passphraseController.text : null,
),
),
text: anythingSelected ? S.of(context).continue_text : S.of(context).skip,
color: anythingSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainerHigh,
textColor: anythingSelected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
),
],
),
);
}
@@ -3,7 +3,6 @@ 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";
@@ -136,21 +135,6 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
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,
@@ -1,3 +1,4 @@
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:flutter/material.dart';
@@ -7,6 +8,8 @@ class ListItemToggleWidget extends StatefulWidget {
super.key,
required this.keyValue,
required this.label,
this.subtitle,
this.iconPath,
required this.value,
required this.onChanged,
this.leadingEndWidget,
@@ -15,7 +18,9 @@ class ListItemToggleWidget extends StatefulWidget {
});
final String keyValue;
final String? iconPath;
final String label;
final String? subtitle;
final bool value;
final ValueChanged<bool> onChanged;
final Widget? leadingEndWidget;
@@ -48,8 +53,24 @@ class _ListItemToggleWidgetState extends State<ListItemToggleWidget> {
child: Row(
spacing: 8,
children: [
if(widget.iconPath != null && widget.iconPath!.isNotEmpty)
CakeImageWidget(imageUrl: widget.iconPath, width: 24, height: 24,),
Flexible(
child: Text(widget.label, style: textStyle, softWrap: true),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.label, style: textStyle, softWrap: true),
if (widget.subtitle != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
widget.subtitle!,
style: labelStyle.copyWith(fontSize: 12),
),
),
],
),
),
if (widget.leadingEndWidget != null) widget.leadingEndWidget!
],
@@ -65,4 +86,4 @@ class _ListItemToggleWidgetState extends State<ListItemToggleWidget> {
);
});
}
}
}
@@ -16,12 +16,12 @@ import 'package:flutter/material.dart';
class NewListSections extends StatelessWidget {
const NewListSections(
{super.key,
required this.sections,
this.controllers = const {},
this.tapHandlers = const {},
this.getCheckboxValue,
this.updateCheckboxValue,
this.showHeader = false});
required this.sections,
this.controllers = const {},
this.tapHandlers = const {},
this.getCheckboxValue,
this.updateCheckboxValue,
this.showHeader = false});
final Map<String, List<ListItem>> sections;
final Map<String, TextEditingController> controllers;
@@ -71,8 +71,8 @@ class NewListSections extends StatelessWidget {
final controller = controllers[item.keyValue];
assert(
controller != null,
'No controller provided for key ${item.keyValue}. '
controller != null,
'No controller provided for key ${item.keyValue}. '
'Please provide a TextEditingController for this key.');
return ListItemTextFieldWidget(
@@ -120,7 +120,9 @@ class NewListSections extends StatelessWidget {
return ListItemToggleWidget(
keyValue: item.keyValue,
label: item.label,
subtitle: item.subtitle,
leadingEndWidget: item.leadingEndWidget,
iconPath: item.iconPath,
value: item.value,
onChanged: item.onChanged,
isFirstInSection: isFirst,
@@ -170,4 +172,4 @@ class NewListSections extends StatelessWidget {
return SizedBox.shrink();
}
}
}
@@ -138,12 +138,15 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
Completer<TrezorDeviceSettings>? _settingsCompleter;
@observable
TrezorParingState paringState = TrezorParingState.initial;
void setParingPin(String pin) => _pinCompleter?.complete(pin);
void setDeviceSettings(TrezorDeviceSettings settings) => _settingsCompleter?.complete(settings);
@override
@action
Future<bool> connectDevice(
@@ -207,7 +210,17 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
await _client!.createChannel();
if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) {
final hasAutoPairingCredentials =
_state!.pairingCredentials.any((c) => c.autoconnect == true);
final isAutoPairingAvailable = _client is sdk.TrezorClientV2 && !hasAutoPairingCredentials;
paringState =
TrezorParingState.awaitingSettings(isAutoPairingAvailable: isAutoPairingAvailable);
_settingsCompleter = Completer<TrezorDeviceSettings>();
final settings = await _settingsCompleter!.future;
paringState = TrezorParingState.awaitingPassphrase;
if (settings.enableAutoParing && isAutoPairingAvailable) {
if (_client case final sdk.TrezorClientV2 clientV2) {
try {
final auto = await clientV2.getAutoPairingCredentials();
@@ -217,6 +230,13 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
}
}
if ((settings.passphrase ?? "").isNotEmpty || settings.passphraseOnDevice) {
final passphrase = settings.passphraseOnDevice
? const sdk.TrezorPassphrase.onDevice()
: sdk.TrezorPassphrase.value(settings.passphrase ?? "");
await _client!.createSession(passphrase);
}
paringState = TrezorParingState.success;
return true;
} catch (e) {
@@ -337,8 +357,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
abstract class TrezorParingState {
static TrezorParingState initial = InitialTrezorParingState();
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState awaitingPassphrase = AwaitingPassphraseTrezorParingState();
static TrezorParingState awaitingSettings({required bool isAutoPairingAvailable}) =>
AwaitingSettingsTrezorParingState(isAutoPairingAvailable: isAutoPairingAvailable);
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
@@ -349,6 +373,14 @@ class EnterPinTrezorParingState extends TrezorParingState {}
class VerifyingPinTrezorParingState extends TrezorParingState {}
class AwaitingSettingsTrezorParingState extends TrezorParingState {
AwaitingSettingsTrezorParingState({required this.isAutoPairingAvailable});
final bool isAutoPairingAvailable;
}
class AwaitingPassphraseTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
@@ -356,3 +388,15 @@ class FailTrezorParingState extends TrezorParingState {
final String message;
}
class TrezorDeviceSettings {
const TrezorDeviceSettings({
required this.enableAutoParing,
required this.passphraseOnDevice,
this.passphrase,
});
final bool enableAutoParing;
final bool passphraseOnDevice;
final String? passphrase;
}
@@ -114,8 +114,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
WalletRestoreMode mode;
@computed
bool get passphraseAvailable =>
mode == WalletRestoreMode.seed || hardwareWalletType == HardwareWalletType.trezor;
bool get passphraseAvailable => mode == WalletRestoreMode.seed;
@observable
bool hasPassphrase;