mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
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:
co-authored by
Robert Malikowski
parent
dd58455ea6
commit
98f58c2898
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user