mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
co-authored by
Konstantin Ullrich
Czarek Nakamoto
Robert Malikowski
Konstantin Ullrich
Claude Opus 4.8
parent
6c1ee16513
commit
5b2a8f6755
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user