Cw 1379 add monero support to trezor rebase dev (#3273)

* refactor: migrate hardware wallet resources to `new-ui`, add Trezor model support, and streamline hardware wallet handling across services

* refactor: migrate hardware wallet resources to `new-ui`, add Trezor model support, and streamline hardware wallet handling across services

* fix: correctly check for Ledger hardware wallet type in `isHardwareWallet` logic

* refactor: unify `isConnected` method for hardware wallets, add Trezor-specific support, and update Monero sync logic

* refactor: add Trezor transaction signing support, streamline hardware wallet handling, and update Monero dependency

* refactor: improve Trezor transaction handling, adjust hardware wallet state logic, and update Monero dependency references

* fix: non-hww tx commit() in xmr
fix: prod monero_c
fix: keyImage import workaround for Trezor/Cupcake
chore: add kotlin's .salive to .gitignore

* fix: universal_ble builds on iOS
chore: remove prints
fix: proper CI prebuilt for linux

* new trezor connection ui

* fix page title

* typo

* fix page title anim

* refactor: enhance Trezor pairing flow, update state management, and simplify UI logic in hardware wallet integration

* fix: correct widget indentation in Trezor pairing failure state UI

* fix: extract Trezor pairing failure state UI to `_errorBox` widget and update Trezor dependency reference

* refactor: simplify hardware wallet pairing error UI, optimize `_errorBox` layout, and streamline widget logic

* Update cw_monero/lib/pending_monero_transaction.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update lib/buy/robinhood/robinhood_buy_provider.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* feat: add USB intent filters, device filter for hardware wallets, and update Trezor dependency reference

* Update cw_monero/pubspec.yaml [skip ci]

* Update cw_wownero/pubspec.yaml [skip ci]

* Update cw_zano/pubspec.yaml [skip ci]

* fix: use CryptoCurrency.title instead of amount-refactor-only .symbol getter

new_wallet_type_page used curr.symbol, which only exists on the
amount-refactor branch (String get symbol => title). On dev the
equivalent is curr.title. Adapts the Trezor feature to dev's API
without pulling in amount-refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix wallet list screen not scrollable
minor fixes

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Konstantin Ullrich <konstantin@cakewallet.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Omar Hatem
2026-06-02 02:33:17 +02:00
committed by GitHub
co-authored by Konstantin Ullrich Czarek Nakamoto Robert Malikowski Konstantin Ullrich Claude Opus 4.8
parent 6c1ee16513
commit 5b2a8f6755
73 changed files with 1851 additions and 451 deletions
+1 -1
View File
@@ -336,7 +336,7 @@ class DFXBuyProvider extends BuyProvider {
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
@@ -173,7 +173,7 @@ class RobinhoodBuyProvider extends BuyProvider {
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
+3 -3
View File
@@ -240,7 +240,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
@@ -414,7 +414,7 @@ Future<void> setup({
switch(type) {
case HardwareWalletType.bitbox: return getIt<BitboxViewModel>();
case HardwareWalletType.ledger: return getIt<LedgerViewModel>();
case HardwareWalletType.trezor: return getIt<TrezorViewModel>();
case HardwareWalletType.trezor: return getIt<TrezorConnectViewModel>();
case HardwareWalletType.cupcake:
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
@@ -429,7 +429,7 @@ Future<void> setup({
getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect",
appName: "Cake Wallet"));
getIt.registerLazySingleton(() => TrezorViewModel(getIt<TrezorConnect>()));
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
@@ -1,5 +1,6 @@
import 'package:bitbox_flutter/usb/bitbox_device.dart' as bitbox;
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
import 'package:trezor_flutter/trezor_flutter.dart' as trezor;
abstract class HardwareWalletDevice {
String get name;
@@ -41,6 +42,21 @@ class BitboxHardwareWalletDevice extends HardwareWalletDevice {
HardwareWalletConnectionType get connectionType => HardwareWalletConnectionType.usb;
}
class TrezorHardwareWalletDevice extends HardwareWalletDevice {
final trezor.TrezorDevice device;
TrezorHardwareWalletDevice(this.device);
@override
String get name => device.name;
@override
HardwareWalletDeviceType get type => device.deviceInfo.toGeneric();
@override
HardwareWalletConnectionType get connectionType => device.connectionType.toGeneric();
}
enum HardwareWalletDeviceType {
ledgerBlue,
ledgerNanoS,
@@ -50,7 +66,12 @@ enum HardwareWalletDeviceType {
ledgerStax,
ledgerFlex,
BitBox02,
BitBox02Nova;
BitBox02Nova,
trezorModelOne,
trezorModelT,
trezorSafe3,
trezorSafe5,
trezorSafe7;
}
enum HardwareWalletConnectionType {
@@ -60,7 +81,7 @@ enum HardwareWalletConnectionType {
qr;
}
extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
extension LedgerToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
HardwareWalletDeviceType toGeneric() {
switch (this) {
case ledger.LedgerDeviceType.blue:
@@ -81,7 +102,24 @@ extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
}
}
extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
extension TrezorToGenericHardwareWalletDeviceType on trezor.TrezorDeviceType {
HardwareWalletDeviceType toGeneric() {
switch (this) {
case trezor.TrezorDeviceType.modelOne:
return HardwareWalletDeviceType.trezorModelOne;
case trezor.TrezorDeviceType.modelT:
return HardwareWalletDeviceType.trezorModelT;
case trezor.TrezorDeviceType.safe3:
return HardwareWalletDeviceType.trezorSafe3;
case trezor.TrezorDeviceType.safe5:
return HardwareWalletDeviceType.trezorSafe5;
case trezor.TrezorDeviceType.safe7:
return HardwareWalletDeviceType.trezorSafe7;
}
}
}
extension LedgerToGenericHardwareWalletConnectionType on ledger.ConnectionType {
HardwareWalletConnectionType toGeneric() {
switch (this) {
case ledger.ConnectionType.usb:
@@ -91,3 +129,14 @@ extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
}
}
}
extension TrezorToGenericHardwareWalletConnectionType on trezor.ConnectionType {
HardwareWalletConnectionType toGeneric() {
switch (this) {
case trezor.ConnectionType.usb:
return HardwareWalletConnectionType.usb;
case trezor.ConnectionType.ble:
return HardwareWalletConnectionType.ble;
}
}
}
+33 -22
View File
@@ -236,13 +236,14 @@ class CWMonero extends Monero {
required String name,
required String password,
required int height,
required ledger.LedgerConnection ledgerConnection,
required HardwareWalletService hardwareWalletService,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name,
password: password,
height: height,
ledgerConnection: ledgerConnection);
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
);
@override
WalletCredentials createMoneroRestoreWalletFromSeedCredentials(
@@ -416,9 +417,7 @@ class CWMonero extends Monero {
}
@override
void monerocCheck() {
checkIfMoneroCIsFine();
}
void monerocCheck() => checkIfMoneroCIsFine();
@override
Future<void> setLedgerConnection(Object wallet, ledger.LedgerConnection connection) async {
@@ -427,29 +426,41 @@ class CWMonero extends Monero {
}
@override
void resetLedgerConnection() {
disableLedgerExchange();
}
void resetLedgerConnection() => disableLedgerExchange();
@override
void setGlobalLedgerConnection(ledger.LedgerConnection connection) {
gLedger = connection;
}
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
@override
String? getLastLedgerCommand() => latestLedgerCommand;
bool isViewOnly() {
return isViewOnlyBySpendKey(null);
@override
void setHardwareWalletService(Object wallet, HardwareWalletService service) {
final moneroWallet = wallet as MoneroWallet;
moneroWallet.trezorService = service as MoneroTrezorService;
}
@override
Map<String, List<int>> debugCallLength() {
return monero_wallet_api.debugCallLength();
}
@override
Map<String, dynamic> getWalletCacheDebug() {
return monero_wallet_api.getWalletCacheDebug();
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) =>
MoneroLedgerService(connection);
@override
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorClient client) =>
MoneroTrezorService(client);
@override
Future<void> syncTrezor(Object wallet) async {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.syncTrezor();
}
bool isViewOnly() => isViewOnlyBySpendKey(null);
@override
Map<String, List<int>> debugCallLength() => monero_wallet_api.debugCallLength();
@override
Map<String, dynamic> getWalletCacheDebug() => monero_wallet_api.getWalletCacheDebug();
}
+9 -4
View File
@@ -726,12 +726,13 @@ class _NewSendPageState extends State<NewSendPage> {
}
if (widget.sendViewModel.wallet.isHardwareWallet) {
if (!widget.sendViewModel.hardwareWalletViewModel!.isConnected) {
if (!widget.sendViewModel.hardwareWalletViewModel!
.isConnected(widget.sendViewModel.walletType)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: widget.sendViewModel.walletType,
hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (BuildContext context, _) {
onConnectDevice: (_, __) {
widget.sendViewModel.hardwareWalletViewModel!
.initWallet(widget.sendViewModel.wallet);
Navigator.of(context).pop();
@@ -748,8 +749,12 @@ class _NewSendPageState extends State<NewSendPage> {
amount += item.formattedCryptoAmount;
}
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet));
if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) {
await monero!.syncTrezor(widget.sendViewModel.wallet);
} else {
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet));
}
await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state
}
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
@@ -86,11 +86,11 @@ class WalletInfoBar extends StatelessWidget {
case null:
return null;
case HardwareWalletType.bitbox:
return "assets/images/hardware_wallet/device_bitbox.svg";
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
+182
View File
@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DigitInputController {
String _text = "";
String get text => _text;
set text(String newText) {
_text = newText;
for (final listener in _listeners) {
listener.call();
}
}
final List<void Function()> _listeners = [];
void addListener(void Function() listener) => _listeners.add(listener);
}
class DigitInputPill extends StatelessWidget {
const DigitInputPill({super.key, this.digit, required this.highlighted});
final String? digit;
final bool highlighted;
@override
Widget build(BuildContext context) {
return Container(
width: 51,
height: 84,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border:
highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
),
),
);
}
}
class DigitInput extends StatefulWidget {
const DigitInput({
super.key,
required this.controller,
required this.desiredLength,
this.breakAt = 3,
});
final DigitInputController controller;
final int desiredLength;
final int breakAt;
@override
State<DigitInput> createState() => _DigitInputState();
}
class _DigitInputState extends State<DigitInput> implements TextInputClient {
TextInputConnection? _textInputConnection;
@override
void initState() {
super.initState();
_openVirtualKeyboard();
}
void _openVirtualKeyboard() {
if (_textInputConnection == null || !_textInputConnection!.attached) {
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
inputType: TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
inputAction: TextInputAction.none,
),
);
}
_textInputConnection!.setEditingState(
TextEditingValue(
text: widget.controller.text,
selection: TextSelection.collapsed(offset: widget.controller.text.length),
),
);
_textInputConnection!.show();
}
void _closeVirtualKeyboard() => _textInputConnection?.close();
@override
void dispose() {
_closeVirtualKeyboard();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _openVirtualKeyboard,
child: SizedBox(
height: 84,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => DigitInputPill(
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
highlighted: widget.controller.text.length == index,
),
separatorBuilder: (context, index) =>
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
itemCount: widget.desiredLength,
),
),
);
}
@override
void updateEditingValue(TextEditingValue value) {
final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
if (numberRegExp.hasMatch(value.text)) {
setState(() => widget.controller.text = value.text);
} else {
// flutter keeps the text editing state regardless of whether you actually used it or not.
// so, if validation fails, we gotta set it back to what it was explicitly
_textInputConnection?.setEditingState(
TextEditingValue(
text: widget.controller.text,
selection: TextSelection.collapsed(offset: widget.controller.text.length),
),
);
}
}
@override
void performAction(TextInputAction action) {}
@override
TextEditingValue? get currentTextEditingValue => TextEditingValue(text: widget.controller.text);
@override
void connectionClosed() => _textInputConnection = null;
@override
AutofillScope? get currentAutofillScope => null;
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {}
@override
void showAutocorrectionPromptRect(int start, int end) {}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {}
@override
void insertTextPlaceholder(Size size) {}
@override
void removeTextPlaceholder() {}
@override
void showToolbar() {}
@override
void performSelector(String selectorName) {}
@override
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
@override
void insertContent(KeyboardInsertedContent content) {}
}
@@ -0,0 +1,60 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
class HardwareWalletProceedOnDeviceMessage extends StatelessWidget {
const HardwareWalletProceedOnDeviceMessage({super.key, required this.hardwareWalletType});
final HardwareWalletType hardwareWalletType;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(spacing: 8, children: [
if (hardwareWalletIcon != null)
CakeImageWidget(
imageUrl: hardwareWalletIcon!,
width: 36,
height: 36,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Text(
S.of(context).proceed_on_device,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
)
]),
),
);
}
String? get hardwareWalletIcon {
switch (hardwareWalletType) {
case HardwareWalletType.bitbox:
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
return "assets/images/hardware_wallet/device_qr.svg";
}
}
}
@@ -0,0 +1,338 @@
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';
class HardwareWalletProceedOnDeviceSheet extends StatefulWidget {
const HardwareWalletProceedOnDeviceSheet({
super.key,
required this.hardwareWalletType,
required this.trezorConnectVM,
required this.onRetry,
});
final HardwareWalletType hardwareWalletType;
final TrezorConnectViewModelBase trezorConnectVM;
final void Function() onRetry;
@override
State<HardwareWalletProceedOnDeviceSheet> createState() =>
_HardwareWalletProceedOnDeviceSheetState();
}
class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProceedOnDeviceSheet> {
TrezorParingState _paringState = TrezorParingState.initial;
final DigitInputController _controller = DigitInputController();
static const pinOpenDuration = Duration(milliseconds: 300);
// you should probably check if there can be other pin lengths
static const pinLength = 6;
ReactionDisposer? paringStateReaction;
@override
void initState() {
super.initState();
_controller.addListener(() => setState(() {}));
_paringState = widget.trezorConnectVM.paringState;
paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) {
if (paringState is SuccessTrezorParingState) {
if (mounted) Navigator.of(context).pop();
return;
}
setState(() => _paringState = paringState);
});
}
@override
void dispose() {
super.dispose();
paringStateReaction?.reaction.dispose();
}
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(
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,
),
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,
duration: pinOpenDuration,
child: ModernButton(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.onPrimary,
size: 36,
icon: Icon(Icons.arrow_forward),
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));
}
void retry() {
_controller.text = "";
widget.onRetry();
}
String get pageTitle {
if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation;
if (_isAwaitingPin) return S.of(context).pairing_code;
return "";
}
bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin;
String? get hardwareWalletIcon {
switch (widget.hardwareWalletType) {
case HardwareWalletType.bitbox:
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
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,
),
)
],
),
);
Widget _errorBox(String errorText) => Container(
key: ValueKey(2),
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
const Spacer(),
Icon(
Icons.error_outline,
size: 80,
color: Theme.of(context).colorScheme.error,
),
Text(
S.of(context).pairing_error,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
Text(
errorText,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const Spacer(),
NewPrimaryButton(
onPressed: retry,
text: S.of(context).try_again,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
)
],
),
),
);
Widget _enterPinCode() => Column(
key: ValueKey(0),
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: pinOpenDuration,
curve: Curves.easeOutCubic,
width: _isAwaitingPin ? 40 : 100,
height: _isAwaitingPin ? 40 : 100,
child: CakeImageWidget(
imageUrl: hardwareWalletIcon,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: pinOpenDuration,
switchInCurve: Curves.easeInCubic,
switchOutCurve: Curves.easeOutCubic,
child: _isAwaitingPin
? Column(
spacing: 24,
key: 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,
desiredLength: pinLength,
)
],
)
: Column(
key: ValueKey(0),
spacing: 12,
children: [
Text(
S.of(context).proceed_on_device,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(
S.of(context).proceed_on_device_description,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
],
),
)
],
);
}
@@ -0,0 +1,106 @@
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:flutter/material.dart';
class HardwareWalletTrezorParingSheet extends StatefulWidget {
const HardwareWalletTrezorParingSheet({super.key});
@override
State<HardwareWalletTrezorParingSheet> createState() => _HardwareWalletTrezorParingSheetState();
}
class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorParingSheet> {
final TextEditingController _paringCodeController = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: SafeArea(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
color: Theme.of(context).colorScheme.surface,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ModalTopBar(
title: "Paring code",
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {},
leadingIcon: Icon(Icons.close),
),
Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
SizedBox(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 75,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 60,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(16),
),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainer,
width: 2,
),
),
child: TextField(
textAlign: TextAlign.left,
textAlignVertical: TextAlignVertical.center,
controller: _paringCodeController,
keyboardType:
TextInputType.numberWithOptions(signed: false, decimal: false),
decoration: InputDecoration(
hint: Text(
"000 000",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
border: InputBorder.none,
filled: true,
fillColor: Colors.transparent,
),
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
),
),
],
),
SizedBox(),
NewPrimaryButton(
text: S.of(context).continue_text,
onPressed: () => Navigator.of(context).pop(_paringCodeController.text),
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
)
],
),
)
],
),
),
),
);
}
}
@@ -1,16 +1,14 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vector_graphics/vector_graphics.dart';
class SendConfirmBottomWidget extends StatelessWidget {
const SendConfirmBottomWidget({super.key, required this.sendViewModel});
@@ -61,7 +59,7 @@ class SendConfirmBottomWidget extends StatelessWidget {
text: "${S.of(context).signing_transaction}...",
);
case IsAwaitingDeviceResponseState:
return HardwareWalletConfirmationMessage(
return HardwareWalletProceedOnDeviceMessage(
hardwareWalletType: sendViewModel.wallet.hardwareWalletType!);
case TransactionCommitting:
return LoadingBottomWidget(
@@ -173,58 +171,3 @@ class TransactionErrorActions extends StatelessWidget {
);
}
}
class HardwareWalletConfirmationMessage extends StatelessWidget {
const HardwareWalletConfirmationMessage({super.key, required this.hardwareWalletType});
final HardwareWalletType hardwareWalletType;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(spacing: 8, children: [
if (hardwareWalletIcon != null)
CakeImageWidget(imageUrl:
hardwareWalletIcon!,
width: 36,
height: 36,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Text(
S.of(context).proceed_on_device,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface),
)
]),
),
);
}
String? get hardwareWalletIcon {
switch (hardwareWalletType) {
case HardwareWalletType.bitbox:
return "assets/images/hardware_wallet/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
return "assets/images/hardware_wallet/device_qr.svg";
}
}
}
@@ -41,7 +41,7 @@ class _SwapConfirmSheetState extends State<SwapConfirmSheet> {
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
if (sendVM.wallet.isHardwareWallet) {
if (!sendVM.hardwareWalletViewModel!.isConnected) {
if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendVM.walletType,
+1 -1
View File
@@ -371,7 +371,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
(_) => getIt.get<NewWalletTypePage>(
param1: NewWalletTypeArguments(
onTypeSelected: (BuildContext context, WalletType type) {
if (hardwareWalletType == HardwareWalletType.trezor) {
if (hardwareWalletType == HardwareWalletType.trezor && type != WalletType.monero) {
Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]);
return;
@@ -3,10 +3,13 @@ import 'dart:io';
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
@@ -91,6 +94,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
var bleDevices = <HardwareWalletDevice>[];
var usbDevices = <HardwareWalletDevice>[];
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
late Timer? _usbRefreshTimer = null;
late Timer? _bleRefreshTimer = null;
late Timer? _bleStateTimer = null;
@@ -158,24 +163,38 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Future<void> _connectToDevice(HardwareWalletDevice device) async {
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
if (isConnected) widget.onConnectDevice(context, widget.hardwareWalletVM);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isConnected) widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
});
}
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
switch (deviceType) {
case HardwareWalletDeviceType.ledgerNanoX:
return 'assets/images/hardware_wallet/ledger_nano_x.png';
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
case HardwareWalletDeviceType.ledgerNanoGen5:
return 'assets/images/hardware_wallet/device_ledger_nano_gen_5.svg';
return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg';
case HardwareWalletDeviceType.ledgerStax:
return 'assets/images/hardware_wallet/ledger_stax.png';
return 'assets/new-ui/hardware_wallets/ledger_stax.svg';
case HardwareWalletDeviceType.ledgerFlex:
return 'assets/images/hardware_wallet/ledger_flex.png';
return 'assets/new-ui/hardware_wallets/ledger_flex.svg';
case HardwareWalletDeviceType.BitBox02:
return 'assets/images/hardware_wallet/device_bitbox.svg';
case HardwareWalletDeviceType.BitBox02Nova:
return 'assets/new-ui/hardware_wallets/device_bitbox.svg';
case HardwareWalletDeviceType.trezorModelOne:
return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg';
case HardwareWalletDeviceType.trezorModelT:
return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg';
case HardwareWalletDeviceType.trezorSafe3:
return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg';
case HardwareWalletDeviceType.trezorSafe5:
return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg';
case HardwareWalletDeviceType.trezorSafe7:
return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg';
default:
return 'assets/images/hardware_wallet/ledger_nano_x.png';
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
}
}
@@ -197,25 +216,15 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Expanded(
child: SingleChildScrollView(
child: Column(
spacing: 24,
children: [
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
description,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
CakeImageWidget(
imageUrl: "assets/new-ui/hardware_wallet.svg",
),
Offstage(
offstage: !longWait,
child: Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
S.of(context).if_you_dont_see_your_device,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
Text(
description,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
Observer(
builder: (_) => Offstage(
@@ -231,56 +240,69 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
),
),
),
if (bleDevices.length > 0) ...[
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Container(
width: double.infinity,
child: Text(
S.of(context).bluetooth,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
...bleDevices
.map(
(device) => Padding(
padding: EdgeInsets.only(bottom: 20),
child: DeviceTile(
onPressed: () => _connectToDevice(device),
title: device.name,
leading: _getDeviceTileLeading(device.type),
connectionType: device.connectionType,
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18)),
child: ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) {
final item = allDevices[index];
return GestureDetector(
onTap: () => _connectToDevice(item),
child: SizedBox(
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 12,
children: [
CakeImageWidget(
imageUrl: _getDeviceTileLeading(item.type),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn),
),
Text(item.name)
],
),
Row(
spacing: 12,
children: [
if (item.connectionType ==
HardwareWalletConnectionType.ble)
CakeImageWidget(
imageUrl: "assets/new-ui/bluetooth.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn),
),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_forward.svg",
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn))
],
)
],
),
),
),
),
)
.toList()
],
if (usbDevices.length > 0) ...[
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Container(
width: double.infinity,
child: Text(
S.of(context).usb,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
...usbDevices
.map(
(device) => Padding(
padding: EdgeInsets.only(bottom: 20),
child: DeviceTile(
onPressed: () => _connectToDevice(device),
title: device.name,
leading: _getDeviceTileLeading(device.type),
connectionType: device.connectionType,
),
),
)
.toList(),
],
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh),
itemCount: allDevices.length),
),
if (widget.allowChangeWallet) ...[
PrimaryButton(
text: S.of(context).wallets,
@@ -293,10 +315,10 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
),
),
),
PrimaryButton(
NewPrimaryButton(
text: S.of(context).how_to_connect,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
textColor: Theme.of(context).colorScheme.primary,
onPressed: () => _onHowToConnect(context),
)
],
@@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -43,14 +44,12 @@ class DeviceTile extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (leading != null && !leading!.endsWith(".svg"))
Image.asset(
leading!,
height: 30,
color: Theme.of(context).colorScheme.onSurface,
),
if (leading != null && leading!.endsWith(".svg"))
SvgPicture.asset(leading!, height: 30),
CakeImageWidget(
height: 30,
imageUrl: leading,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurface, BlendMode.srcIn),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 16),
@@ -34,11 +34,11 @@ class CryptoBalanceWidget extends StatelessWidget {
case null:
return null;
case HardwareWalletType.bitbox:
return "assets/images/hardware_wallet/device_bitbox.svg";
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
@@ -213,7 +213,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
if (sendVM.wallet.isHardwareWallet) {
if (!sendVM.hardwareWalletViewModel!.isConnected) {
if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendVM.walletType,
@@ -160,7 +160,7 @@ class DEuroSavingsPage extends BasePage {
Future<void> _requireHardwareWallet(BuildContext context) async {
if (_dEuroViewModel.wallet.isHardwareWallet) {
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected) {
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected(_dEuroViewModel.wallet.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: _dEuroViewModel.wallet.type,
@@ -6,14 +6,8 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart';
import 'package:cake_wallet/src/widgets/cake_image_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/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
@@ -21,10 +15,38 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
class NewWalletTypePageHeader extends StatelessWidget {
const NewWalletTypePageHeader({super.key});
@override
Widget build(BuildContext context) => const SizedBox(
width: 138,
height: 75,
child: Stack(
children: [
Positioned(
left: 0,
child: CakeImageWidget(
imageUrl: "assets/new-ui/hardware_wallet.svg",
width: 75,
height: 75,
),
),
Positioned(
left: 63,
child: CakeImageWidget(
imageUrl: "assets/new-ui/cake_coin.svg",
width: 75,
height: 75,
),
),
],
),
);
}
class NewWalletTypePage extends BasePage {
NewWalletTypePage({
required this.newWalletTypeArguments,
});
NewWalletTypePage({required this.newWalletTypeArguments});
final NewWalletTypeArguments newWalletTypeArguments;
@@ -38,23 +60,21 @@ class NewWalletTypePage extends BasePage {
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
final currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) currentFocus.focusedChild?.unfocus();
};
@override
Widget body(BuildContext context) => WalletTypeForm(
walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage,
isCreate: newWalletTypeArguments.isCreate,
onTypeSelected: newWalletTypeArguments.onTypeSelected,
hardwareWalletType: newWalletTypeArguments.hardwareWalletType,
);
walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage,
isCreate: newWalletTypeArguments.isCreate,
onTypeSelected: newWalletTypeArguments.onTypeSelected,
hardwareWalletType: newWalletTypeArguments.hardwareWalletType,
);
}
class WalletTypeForm extends StatefulWidget {
WalletTypeForm({
const WalletTypeForm({
required this.walletImage,
required this.isCreate,
this.onTypeSelected,
@@ -77,9 +97,6 @@ class WalletTypeFormState extends State<WalletTypeForm> {
static const aspectRatioImage = 1.22;
final TextEditingController searchController = TextEditingController();
WalletType? selected;
List<WalletType> types;
List<WalletType> filteredTypes = [];
@@ -87,21 +104,12 @@ class WalletTypeFormState extends State<WalletTypeForm> {
void initState() {
types = filteredTypes = availableWalletTypes
.where((element) =>
!widget.isHardwareWallet ||
DeviceConnectionType.supportedConnectionTypes(
element, widget.hardwareWalletType!, Platform.isIOS)
.isNotEmpty)
!widget.isHardwareWallet ||
DeviceConnectionType.supportedConnectionTypes(
element, widget.hardwareWalletType!, Platform.isIOS)
.isNotEmpty)
.toList();
super.initState();
searchController.addListener(() {
setState(() {
filteredTypes = List.from(types.where((type) => walletTypeToDisplayName(type)
.toLowerCase()
.contains(searchController.text.toLowerCase())));
return;
});
});
}
@override
@@ -110,96 +118,102 @@ class WalletTypeFormState extends State<WalletTypeForm> {
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
spacing: 24,
children: [
Padding(
padding: EdgeInsets.only(top: 48),
child: Text(
S.of(context).choose_wallet_currency,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const NewWalletTypePageHeader(),
Text(
S.of(context).choose_wallet_currency,
textAlign: TextAlign.center,
),
Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 12),
child: SearchBarWidget(searchController: searchController),
),
Expanded(
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
scrollableKey: ValueKey('new_wallet_type_scrollable_key'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
...filteredTypes.map(
(type) => Padding(
padding: EdgeInsets.only(top: 12),
child: SelectButton(
key: ValueKey('new_wallet_type_${type.name}_button_key'),
image: CakeImageWidget(
imageUrl: getCryptoCurrencyIconForWalletListItem(type),
height: 24,
width: 24,
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: ListView.separated(
key: const ValueKey('new_wallet_type_scrollable_key'),
itemCount: filteredTypes.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final item = filteredTypes[index];
final curr = getCryptoCurrencyForWalletListItem(item);
return GestureDetector(
key: ValueKey('new_wallet_type_${item.name}_button_key'),
behavior: HitTestBehavior.opaque,
onTap: () {
onTypeSelected(item);
},
child: SizedBox(
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CakeImageWidget(
height: 24,
width: 24,
imageUrl: curr.iconPath,
),
const SizedBox(width: 12),
Text(curr.fullName ?? curr.name),
const SizedBox(width: 4),
Text(
curr.title,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_forward.svg",
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
)
],
),
),
text: walletTypeToDisplayName(type),
showTrailingIcon: false,
height: 54,
isSelected: selected == type,
onTap: () => setState(() => selected = type),
deviceConnectionTypes: widget.isHardwareWallet
? DeviceConnectionType.supportedConnectionTypes(
type, widget.hardwareWalletType!, Platform.isIOS)
: [],
),
),
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
],
),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: PrimaryButton(
key: ValueKey('new_wallet_type_next_button_key'),
onPressed: () => onTypeSelected(),
text: S.of(context).seed_language_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: selected == null,
),
),
),
),
)
],
),
),
);
}
Future<void> onTypeSelected() async {
if (selected == null) throw Exception('Wallet Type is not selected yet.');
if (selected == WalletType.haven && widget.isCreate) {
return await showPopUp<void>(
context: context,
builder: (BuildContext context) => PopUpCancellableAlertDialog(
contentText: S.of(context).pause_wallet_creation,
actionButtonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
Future<void> onTypeSelected(WalletType selected) async {
// If it's a restore flow, trigger the external callback
// If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page
// Any other scenario, route to pre-existing seed page
final walletList = await WalletInfo.getAll();
if (!widget.isCreate) {
widget.onTypeSelected!(context, selected!);
} else if (!isBIP39Wallet(selected!) || walletList.isEmpty) {
widget.onTypeSelected!(context, selected);
} else if (!isBIP39Wallet(selected) || walletList.isEmpty) {
Navigator.of(context).pushNamed(
Routes.newWallet,
arguments: NewWalletArguments(type: selected!),
arguments: NewWalletArguments(type: selected),
);
} else {
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!);
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected);
}
}
}
}
+1 -1
View File
@@ -426,7 +426,7 @@ class SendPage extends BasePage {
}
if (sendViewModel.wallet.isHardwareWallet) {
if (!sendViewModel.hardwareWalletViewModel!.isConnected) {
if (!sendViewModel.hardwareWalletViewModel!.isConnected(sendViewModel.walletType)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendViewModel.walletType,
@@ -78,7 +78,7 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
bool _isConnecting = false;
@override
bool get isConnected => false;
bool isConnected(WalletType type) => false;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
@@ -6,10 +6,11 @@ import 'package:cw_core/wallet_type.dart';
abstract class HardwareWalletViewModel {
HardwareWalletType get hardwareWalletType;
bool get isConnected;
bool get isBleEnabled;
bool get hasBluetooth;
bool isConnected(WalletType type);
Future<void> updateBleState();
Stream<HardwareWalletDevice> scanForBleDevices();
@@ -114,7 +114,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (_isConnecting) return false;
_isConnecting = true;
_connectingWalletType = type;
if (isConnected) {
if (isConnected(type)) {
try {
await _connection!.disconnect().catchError((_) {});
} catch (_) {}
@@ -126,7 +126,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (_connectionChangeSubscription == null) {
_connectionChangeSubscription = ledger
.deviceStateChanges
.deviceStateChanges(device.device.id)
.listen(_connectionChangeListener);
}
@@ -172,7 +172,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
@override
bool get isConnected => _connection != null && !(_connection!.isDisconnected);
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
sdk.LedgerConnection get connection => _connection!;
@@ -196,6 +196,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getLedgerHardwareWalletService(connection);
case WalletType.bitcoin:
return bitcoin!.getLedgerHardwareWalletService(connection, true);
case WalletType.litecoin:
@@ -0,0 +1,258 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:trezor_connect/trezor_connect.dart' as connect_sdk;
import 'package:trezor_flutter/trezor_flutter.dart' as sdk;
part 'trezor_connect_view_model.g.dart';
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
final connect_sdk.TrezorConnect trezorConnect;
late final sdk.TrezorInterface trezorBLE;
late final sdk.TrezorInterface trezorUSB;
sdk.ThpState _state = sdk.ThpState();
TrezorConnectViewModelBase(this.trezorConnect) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
});
updateBleState();
if (!Platform.isIOS) {
trezorUSB = sdk.TrezorInterface.usb();
}
}
}
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.trezor, Platform.isIOS)
.isNotEmpty;
}
return true;
}
bool _bleIsInitialized = false;
Future<void> _initBLE() async {
if (isBleEnabled && !_bleIsInitialized) {
trezorBLE = sdk.TrezorInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
].request();
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => true;
@override
Future<void> updateBleState() async {
final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d));
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => trezorUSB.devices
.then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await trezorBLE.stopScanning();
if (!Platform.isIOS) await trezorUSB.stopScanning();
}
bool _isConnecting = false;
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
@observable
TrezorParingState paringState = TrezorParingState.initial;
void setParingPin(String pin) => _pinCompleter?.complete(pin);
@override
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
[bool isRetry = false]) async {
if (!(device is TrezorHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
paringState = TrezorParingState.initial;
try {
final trezorInterface =
device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB;
final connection = await trezorInterface.connect(device.device);
if (!isRetry) {
showModalBottomSheet(
context: navigatorKey.currentContext!,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
useSafeArea: true,
builder: (context) =>
HardwareWalletProceedOnDeviceSheet(
hardwareWalletType: hardwareWalletType,
trezorConnectVM: this,
onRetry: () => connectDevice(device, type, true)
),
);
}
Future<String> onPinCode() async {
_pinCompleter = Completer<String?>();
paringState = TrezorParingState.enterPin;
final res = await _pinCompleter!.future;
paringState = TrezorParingState.verifyingPin;
if (res == null) throw Exception();
return res;
}
final deviceInfo = await _deviceName;
_client = sdk.TrezorClient.getClientForConnection(
connection,
_state,
"Cake Wallet",
deviceInfo,
onPinCode,
);
await _client!.createChannel();
paringState = TrezorParingState.success;
return true;
} catch (e) {
await _client?.connection.disconnect();
_client = null;
_state = sdk.ThpState();
// rethrow;
paringState = TrezorParingState.fail(e.toString());
printV(e);
return false;
} finally {
_isConnecting = false;
}
}
Future<String> get _deviceName async {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) return (await deviceInfo.androidInfo).model;
if (Platform.isIOS) return (await deviceInfo.iosInfo).name;
return "Computer";
}
@override
bool isConnected(WalletType type) => type == WalletType.monero
? _client != null && _client?.connection.isDisconnected == false
: true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getTrezorHardwareWalletService(_client!);
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
switch (wallet.type) {
case WalletType.monero:
return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
}
}
}
abstract class TrezorParingState {
static TrezorParingState initial = InitialTrezorParingState();
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
class InitialTrezorParingState extends TrezorParingState {}
class EnterPinTrezorParingState extends TrezorParingState {}
class VerifyingPinTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
final String message;
FailTrezorParingState(this.message);
}
@@ -1,79 +0,0 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:trezor_connect/trezor_connect.dart' as sdk;
import 'package:mobx/mobx.dart';
part 'trezor_view_model.g.dart';
class TrezorViewModel = TrezorViewModelBase with _$TrezorViewModel;
abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store {
final sdk.TrezorConnect trezorConnect;
TrezorViewModelBase(this.trezorConnect);
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => false;
@override
Future<void> updateBleState() async {}
@override
Stream<HardwareWalletDevice> scanForBleDevices() => throw UnimplementedError();
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() async => [];
@override
Future<void> stopScanning() async {}
@override
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async => true;
@override
bool get isConnected => true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
}
}
}
+6 -2
View File
@@ -619,7 +619,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (!(state is IsExecutingState)) state = IsExecutingState();
if (wallet.isHardwareWallet) {
state = IsAwaitingDeviceResponseState();
if (walletType == WalletType.monero) {
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
@@ -627,6 +626,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = IsDeviceSigningResponseState();
}
});
} else {
state = IsAwaitingDeviceResponseState();
}
}
@@ -970,8 +971,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
throw Exception("Pending transaction doesn't exist. It should not be happened.");
}
try {
state = TransactionCommitting();
state = wallet.isHardwareWallet && walletType == WalletType.monero
? IsAwaitingDeviceResponseState()
: TransactionCommitting();
if (ocpRequest != null) {
await _handleOcpRequest();
@@ -89,7 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials(
name: name,
ledgerConnection: (hardwareWalletVM as LedgerViewModel).connection,
hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
password: password,
height: _options['height'] as int? ?? 0,
);