CW-1135-add-support-for-bitbox-hww (#2476)

* chore: add ffi and ffigen dependencies to pubspec.yaml

* refactor: replace method channel with FFI implementation for MWeb module

* fix: correct `cd` command in build_mwebd.sh script

* chore: add go.sum file with dependency checksums

* fix: ensure ANDROID_NDK_VERSION is set before building and dynamically update NDK_BIN path

* fix: update NDK_BIN path to dynamically detect platform in build_mwebd.sh script

* refactor: remove unused mwebd dependencies and clean up CwMwebPlugin code

* Update scripts/android/build_mwebd.sh

Co-authored-by: cyan <cyjan@mrcyjanek.net>

* refactor: replace iOS MoneroWallet framework with Mwebd.xcframework and clean up plugin implementation [skip-ci]

* refactor: extract hardware wallet service abstraction

* refactor: extend BitcoinHardwareWalletService from HardwareWalletService and override account retrieval method

* feat: add BitBox hardware wallet support and implement manufacturer selection page with custom UI components

* refactor: update manufacturer selection UI and integrate custom gradient styles for ManufacturerOptionTile widget

* fix: mwebd ffi ios

* feat: add BitBox hardware wallet support, update derivation paths, and refactor hardware wallet credential implementations

* feat: add bitbox support and refactor the old ledger implementation

* feat: add BitBox hardware wallet support and refactor hardware wallet service implementations

* feat: refactor hardware wallet support to introduce abstraction for BitBox and Ledger; update device scanning, initialization, and UI integration

* fix: BitBox hardware wallet ethereum transaction signing

* chore: update BitBox Flutter path references and include build steps in CI workflows

* chore: update CI workflows to include `--dont-install` flag for BitBox Flutter builds

* refactor: replace if-else logic with switch-case for HardwareWalletViewModel factory

* feat: refactor hardware wallet services to introduce shared interface, improve ledger and bitbox implementations, and support additional transaction signing operations

* feat: add SVG assets for hardware wallet manufacturers (Foundation, Keystone, SeedSigner)

* feat: redesign hardware wallet restoration flow with improved routing, UI updates, and manufacturer-specific device selection handling

* fix: validate required BIP353 fields in transaction builder to prevent potential null value errors

* feat: improve BitBox transaction signing logs with detailed PSBT information

* refactor: update MWebFfi class to use non-nullable `lib` and improve address debug logging

* feat: add BitBox coin type constant and improve hardware wallet service extensibility

* chore: remove unused iOS/Android build scripts for mwebd

* feat: add BitBox SVG asset and update localization for hardware wallet connection instructions

* feat: update manufacturer tags and add Toast Easter egg for device selection interactions

* refactor: replace LedgerViewModel with HardwareWalletViewModel across providers and improve hardware wallet handling

* refactor: replace mweb.so with libmweb.so, use sync methods in MWebFfi, and update iOS/Android frameworks/scripts accordingly

* feat: add Ledger Nano X SVG asset to hardware wallet resources

* chore: unify mwebd build process in workflows, remove unused gomobile init command

* refactor: remove redundant print statement and unnecessary whitespace

* chore: add `gomobile init` to Android PR test build workflow

* chore: update Android PR test build workflow and add file existence check in `build_bitbox_flutter.sh`

* chore: override `ledger_usb_plus` dependency in pubspec_base.yaml

* feat: add verbose logging for BitBox connection and initialization process

* Update lib/view_model/hardware_wallet/bitbox_view_model.dart [skip ci]

* CW-1206-refactor-hardware-wallets-code (#2507)

* feat: add support for new hardware wallet types (Cupcake, Coldcard, SeedSigner) and improve QR Code handling

- Introduced new hardware wallet types (Cupcake, Coldcard, SeedSigner) with corresponding assets and localization updates.
- Updated QR Code format selection for hardware wallets with new formats (BCUR, BBQR) specific to devices.
- Improved hardware wallet restoration flow to include airgapped wallet handling via QR Code scanning.
- Refactored routing, view models, and dependency injection to accommodate new hardware wallet types.
- Enhanced UI for device manufacturer selection and QR Code format dialogs.

* feat: update hardware wallet assets and restoration flow

* feat: add changing airgapped wallet type

* feat: add Cupcake Man SVG asset to hardware wallet resources

* refactor: update Litecoin receive options to require wallet parameter and handle hardware wallets

* fix: debugging keystone integration
feat: support for zpub view only restore

* fix: seed signer restore

* fix: disable bitbox restoring (for now) [skip-ci]

* refactor: rename `isCupcake` to `isBitcoinViewOnly` for clarity [skip ci]

---------

Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor: standardize hardware wallet connection handling across wallet types

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Konstantin Ullrich
2025-10-01 18:20:40 +03:00
committed by GitHub
co-authored by Czarek Nakamoto Omar Hatem
parent e1bb0e4a69
commit e4672c75b7
117 changed files with 2048 additions and 723 deletions
@@ -1,6 +1,7 @@
import 'dart:async';
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/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
@@ -9,23 +10,25 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_wid
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';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
typedef OnConnectDevice = void Function(BuildContext, LedgerViewModel);
typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
class ConnectDevicePageParams {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final HardwareWalletType hardwareWalletType;
ConnectDevicePageParams({
required this.walletType,
required this.hardwareWalletType,
required this.onConnectDevice,
this.allowChangeWallet = false,
this.isReconnect = true,
@@ -37,9 +40,9 @@ class ConnectDevicePage extends BasePage {
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final LedgerViewModel ledgerVM;
final HardwareWalletViewModel hardwareWalletVM;
ConnectDevicePage(ConnectDevicePageParams params, this.ledgerVM)
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
: walletType = params.walletType,
onConnectDevice = params.onConnectDevice,
allowChangeWallet = params.allowChangeWallet,
@@ -51,8 +54,7 @@ class ConnectDevicePage extends BasePage {
: S.current.restore_title_from_hardware_wallet;
@override
Widget? leading(BuildContext context) =>
!isReconnect ? super.leading(context) : null;
Widget? leading(BuildContext context) => !isReconnect ? super.leading(context) : null;
@override
Widget body(BuildContext context) => PopScope(
@@ -61,7 +63,7 @@ class ConnectDevicePage extends BasePage {
walletType,
onConnectDevice,
allowChangeWallet,
ledgerVM,
hardwareWalletVM,
currentTheme,
));
}
@@ -70,14 +72,14 @@ class ConnectDevicePageBody extends StatefulWidget {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final LedgerViewModel ledgerVM;
final HardwareWalletViewModel hardwareWalletVM;
final MaterialThemeBase currentTheme;
const ConnectDevicePageBody(
this.walletType,
this.onConnectDevice,
this.allowChangeWallet,
this.ledgerVM,
this.hardwareWalletVM,
this.currentTheme,
);
@@ -86,13 +88,13 @@ class ConnectDevicePageBody extends StatefulWidget {
}
class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
var bleDevices = <LedgerDevice>[];
var usbDevices = <LedgerDevice>[];
var bleDevices = <HardwareWalletDevice>[];
var usbDevices = <HardwareWalletDevice>[];
late Timer? _usbRefreshTimer = null;
late Timer? _bleRefreshTimer = null;
late Timer? _bleStateTimer = null;
late StreamSubscription<LedgerDevice>? _bleRefresh = null;
late StreamSubscription<HardwareWalletDevice>? _bleRefresh = null;
bool longWait = false;
Timer? _longWaitTimer;
@@ -101,21 +103,21 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_bleStateTimer = Timer.periodic(
Duration(seconds: 1), (_) => widget.ledgerVM.updateBleState());
_bleStateTimer =
Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState());
_bleRefreshTimer =
Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
_bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
if (Platform.isAndroid) {
_usbRefreshTimer =
Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
_usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
}
_longWaitTimer = Timer(Duration(seconds: 10), () {
if (widget.ledgerVM.bleIsEnabled && bleDevices.isEmpty)
setState(() => longWait = true);
});
if (widget.hardwareWalletVM.hasBluetooth) {
_longWaitTimer = Timer(Duration(seconds: 10), () {
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty)
setState(() => longWait = true);
});
}
});
}
@@ -127,35 +129,25 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
_bleRefresh?.cancel();
_longWaitTimer?.cancel();
widget.ledgerVM.stopScanning();
widget.hardwareWalletVM.stopScanning();
super.dispose();
}
Future<void> _refreshUsbDevices() async {
final dev = await widget.ledgerVM.ledgerPlusUSB.devices;
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) setState(() => usbDevices = dev);
// _usbRefresh = widget.ledgerVM
// .scanForUsbDevices()
// .listen((device) => setState(() => usbDevices.add(device)))
// ..onError((e) {
// throw e.toString();
// });
// Keep polling until the lfp lib gets updated
// _usbRefreshTimer?.cancel();
// _usbRefreshTimer = null;
}
Future<void> _refreshBleDevices() async {
try {
if (widget.ledgerVM.bleIsEnabled) {
_bleRefresh =
widget.ledgerVM.scanForBleDevices().listen((device) => setState(() {
bleDevices.add(device);
if (longWait) longWait = false;
}))
..onError((e) {
throw e.toString();
});
if (widget.hardwareWalletVM.isBleEnabled) {
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() {
bleDevices.add(device);
if (longWait) longWait = false;
}))
..onError((e) {
throw e.toString();
});
_bleRefreshTimer?.cancel();
_bleRefreshTimer = null;
}
@@ -164,25 +156,35 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
}
}
Future<void> _connectToDevice(LedgerDevice device) async {
final isConnected =
await widget.ledgerVM.connectLedger(device, widget.walletType);
if (isConnected) widget.onConnectDevice(context, widget.ledgerVM);
Future<void> _connectToDevice(HardwareWalletDevice device) async {
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
if (isConnected) widget.onConnectDevice(context, widget.hardwareWalletVM);
}
String _getDeviceTileLeading(LedgerDeviceType deviceInfo) {
switch (deviceInfo) {
case LedgerDeviceType.nanoX:
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
switch (deviceType) {
case HardwareWalletDeviceType.ledgerNanoX:
return 'assets/images/hardware_wallet/ledger_nano_x.png';
case LedgerDeviceType.stax:
case HardwareWalletDeviceType.ledgerStax:
return 'assets/images/hardware_wallet/ledger_stax.png';
case LedgerDeviceType.flex:
case HardwareWalletDeviceType.ledgerFlex:
return 'assets/images/hardware_wallet/ledger_flex.png';
case HardwareWalletDeviceType.BitBox02:
return 'assets/images/hardware_wallet/device_bitbox.svg';
default:
return 'assets/images/hardware_wallet/ledger_nano_x.png';
}
}
String get description {
if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble;
if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet;
return S.of(context).connect_your_hardware_wallet_usb;
}
@override
Widget build(BuildContext context) {
return Container(
@@ -197,11 +199,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
Platform.isAndroid
? S.of(context).connect_your_hardware_wallet
: S.of(context).connect_your_hardware_wallet_ios,
style: Theme.of(context)
.textTheme.titleMedium,
description,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
@@ -211,22 +210,20 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
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,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
),
Observer(
builder: (_) => Offstage(
offstage: widget.ledgerVM.bleIsEnabled,
offstage: widget.hardwareWalletVM.isBleEnabled ||
!widget.hardwareWalletVM.hasBluetooth,
child: Padding(
padding:
EdgeInsets.only(left: 20, right: 20, bottom: 20),
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
S.of(context).ledger_please_enable_bluetooth,
style: Theme.of(context)
.textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
@@ -239,8 +236,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
width: double.infinity,
child: Text(
S.of(context).bluetooth,
style: Theme.of(context)
.textTheme.bodyMedium,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
@@ -251,7 +247,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
child: DeviceTile(
onPressed: () => _connectToDevice(device),
title: device.name,
leading: _getDeviceTileLeading(device.deviceInfo),
leading: _getDeviceTileLeading(device.type),
connectionType: device.connectionType,
),
),
@@ -265,8 +261,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
width: double.infinity,
child: Text(
S.of(context).usb,
style: Theme.of(context)
.textTheme.bodyMedium,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
@@ -277,7 +272,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
child: DeviceTile(
onPressed: () => _connectToDevice(device),
title: device.name,
leading: _getDeviceTileLeading(device.deviceInfo),
leading: _getDeviceTileLeading(device.type),
connectionType: device.connectionType,
),
),
@@ -287,10 +282,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
if (widget.allowChangeWallet) ...[
PrimaryButton(
text: S.of(context).wallets,
color: Theme.of(context)
.colorScheme.primary,
textColor: Theme.of(context)
.colorScheme.onPrimary,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: _onChangeWallet,
)
],
@@ -312,8 +305,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
void _onChangeWallet() {
Navigator.of(context).pushNamed(
Routes.walletList,
arguments: (BuildContext context) => Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
arguments: (BuildContext context) =>
Navigator.of(context).pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
);
}
@@ -0,0 +1,183 @@
import 'package:cake_wallet/generated/i18n.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/dotted_divider.dart';
import 'package:cake_wallet/src/screens/connect_device/widgets/manufacturer_option_tile.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:permission_handler/permission_handler.dart';
class SelectDeviceManufacturerPage extends BasePage {
SelectDeviceManufacturerPage({
this.showUnavailable = true,
this.onSelect,
this.availableHardwareWalletTypes,
});
final bool showUnavailable;
final void Function(BuildContext, HardwareWalletType)? onSelect;
final List<HardwareWalletType>? availableHardwareWalletTypes;
@override
String get title => S.current.select_manufacturer_title;
@override
AppBarStyle get appBarStyle => AppBarStyle.regular;
List<_DeviceManufacturer> get availableManufacturers => [
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/ledger_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.ledger,
),
// _DeviceManufacturer(
// image: SvgPicture.asset('assets/images/hardware_wallet/bitbox_man.svg', height: 25),
// hardwareWalletType: HardwareWalletType.bitbox,
// tag: S.current.new_tag,
// ),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/cupcake_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.cupcake,
tag: S.current.new_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/coldcard_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.coldcard,
tag: S.current.new_tag,
),
// _DeviceManufacturer(
// image: SvgPicture.asset('assets/images/hardware_wallet/seedsigner_man.svg', height: 25),
// hardwareWalletType: HardwareWalletType.seedsigner,
// tag: S.current.new_tag,
// ),
]
.where((e) => availableHardwareWalletTypes == null
? ![HardwareWalletType.cupcake].contains(e.hardwareWalletType)
: availableHardwareWalletTypes!.contains(e.hardwareWalletType))
.toList();
List<_DeviceManufacturer> get comingManufacturers => [
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/trezor_man.svg', height: 25),
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/bitbox_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.bitbox,
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/seedsigner_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.seedsigner,
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/foundation_man.svg', height: 25),
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/keystone_man.svg', height: 25),
tag: S.current.coming_soon_tag,
),
];
@override
Widget body(BuildContext context) => Container(
child: Center(
child: Container(
padding: EdgeInsets.only(left: 24, right: 24),
child: Column(
children: [
...availableManufacturers.map(
(manufacturer) => Padding(
padding: EdgeInsets.only(top: 10),
child: ManufacturerOptionTile(
image: manufacturer.image,
tag: manufacturer.tag,
onPressed: () {
if (onSelect != null)
return onSelect!.call(context, manufacturer.hardwareWalletType!);
if (isAirgappedWallet(manufacturer.hardwareWalletType)) {
_onScanQRCode(context, manufacturer.hardwareWalletType!);
} else if (manufacturer.hardwareWalletType != null) {
Navigator.pushNamed(context, Routes.connectHardwareWallet,
arguments: [manufacturer.hardwareWalletType]);
}
},
isDarkTheme: currentTheme.isDark,
),
),
),
if (showUnavailable) ...[
Padding(
padding: EdgeInsets.only(top: 20, bottom: 10),
child: DottedDivider(color: Theme.of(context).colorScheme.surfaceContainer),
),
...comingManufacturers.map(
(manufacturer) => Padding(
padding: EdgeInsets.only(top: 10),
child: ManufacturerOptionTile(
image: manufacturer.image,
tag: manufacturer.tag,
onPressed: () =>
Fluttertoast.showToast(msg: 'One more tap and it might work'),
// Ester egg
isDarkTheme: currentTheme.isDark,
isUnavailable: true,
),
),
),
]
],
),
),
),
);
bool isAirgappedWallet(HardwareWalletType? type) => [
HardwareWalletType.cupcake,
HardwareWalletType.coldcard,
HardwareWalletType.seedsigner,
HardwareWalletType.keystone,
].contains(type);
bool isRestoring = false;
Future<void> _onScanQRCode(BuildContext context, HardwareWalletType type) async {
final isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
try {
if (isRestoring) return;
isRestoring = true;
final restoredWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context);
final params = {
'walletType': restoredWallet.type,
'restoredWallet': restoredWallet,
'hardwareWalletType': type,
};
Navigator.pushNamed(context, Routes.restoreWallet, arguments: params)
.then((_) => isRestoring = false);
} catch (e) {
printV(e.toString());
}
}
}
class _DeviceManufacturer {
final SvgPicture image;
final HardwareWalletType? hardwareWalletType;
final String? tag;
_DeviceManufacturer({required this.image, this.hardwareWalletType, this.tag});
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:flutter/material.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DeviceTile extends StatelessWidget {
const DeviceTile({
@@ -12,15 +13,15 @@ class DeviceTile extends StatelessWidget {
final VoidCallback onPressed;
final String title;
final String? leading;
final ConnectionType? connectionType;
final HardwareWalletConnectionType? connectionType;
String? get connectionTypeIcon {
switch (connectionType) {
case ConnectionType.ble:
case HardwareWalletConnectionType.ble:
return 'assets/images/bluetooth.png';
case ConnectionType.usb:
case HardwareWalletConnectionType.usb:
return 'assets/images/usb.png';
case null:
default:
return null;
}
}
@@ -42,12 +43,14 @@ class DeviceTile extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (leading != null)
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),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 16),
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class DottedDivider extends StatelessWidget {
final Color color;
const DottedDivider({super.key, required this.color});
@override
Widget build(BuildContext context) => SizedBox(
width: double.infinity,
child: CustomPaint(
painter: _DashedLinePainter(color: color),
),
);
}
class _DashedLinePainter extends CustomPainter {
final Color color;
const _DashedLinePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
double dashWidth = 9, dashSpace = 5, startX = 0;
final paint = Paint()
..color = color
..strokeWidth = 1;
while (startX < size.width) {
canvas.drawLine(Offset(startX, 0), Offset(startX + dashWidth, 0), paint);
startX += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,80 @@
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:flutter/material.dart';
class ManufacturerOptionTile extends StatelessWidget {
const ManufacturerOptionTile({
required this.onPressed,
required this.image,
required this.isDarkTheme,
this.tag,
this.isUnavailable = false,
});
final VoidCallback onPressed;
final Widget image;
final String? tag;
final bool isUnavailable;
final bool isDarkTheme;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onPressed,
child: Opacity(
opacity: isUnavailable ? 0.5 : 1,
child: Container(
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
isDarkTheme
? CustomThemeColors.cardGradientColorPrimaryDark
: CustomThemeColors.cardGradientColorPrimaryLight,
isDarkTheme
? CustomThemeColors.cardGradientColorSecondaryDark
: CustomThemeColors.cardGradientColorSecondaryLight,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
padding: EdgeInsets.all(24),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
Container(height: 25, child: image),
if (tag != null)
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.surfaceContainerLow),
padding: EdgeInsets.symmetric(horizontal: 5),
margin: EdgeInsets.only(left: 5),
child: Text(tag!))
],
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context).colorScheme.onSurface,
)
],
),
),
),
);
}
@@ -12,6 +12,7 @@ import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -27,6 +28,23 @@ class CryptoBalanceWidget extends StatelessWidget {
final btcLockLight = 'assets/images/btc_lock_light.png';
final btcLockDark = 'assets/images/btc_lock_dark.png';
String? get hardwareWalletIcon {
switch (dashboardViewModel.wallet.hardwareWalletType) {
case null:
return null;
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
case HardwareWalletType.bitbox:
return "assets/images/hardware_wallet/device_bitbox.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";
}
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
@@ -86,10 +104,10 @@ class CryptoBalanceWidget extends StatelessWidget {
maxLines: 1,
textAlign: TextAlign.center,
),
if (dashboardViewModel.wallet.isHardwareWallet)
if (hardwareWalletIcon != null)
Container(
child: Image.asset(
'assets/images/hardware_wallet/ledger_nano_x.png',
child: SvgPicture.asset(
hardwareWalletIcon!,
width: 24,
color: Theme.of(context).colorScheme.onSurface,
),
+39
View File
@@ -0,0 +1,39 @@
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/view_model/dev/qr_tools_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class DevQRToolsPage extends BasePage {
@override
String? get title => "[dev] *QR tools";
final QRToolsViewModel viewModel = QRToolsViewModel();
late final textCtrl = TextEditingController(text: viewModel.input);
@override
Widget body(BuildContext context) {
return Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: textCtrl,
maxLines: 8,
onChanged: (value) {
viewModel.input = value;
}
),
Observer(
builder: (_) {
return Expanded(
child: JsonExplorer(data: viewModel.data, title: "result"),
);
},
),
],
),
);
}
}
@@ -1,7 +1,7 @@
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'dart:io';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/routes.dart';
@@ -11,12 +11,12 @@ import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_aler
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@@ -51,7 +51,7 @@ class NewWalletTypePage extends BasePage {
isCreate: newWalletTypeArguments.isCreate,
newWalletTypeViewModel: newWalletTypeViewModel,
onTypeSelected: newWalletTypeArguments.onTypeSelected,
isHardwareWallet: newWalletTypeArguments.isHardwareWallet,
hardwareWalletType: newWalletTypeArguments.hardwareWalletType,
);
}
@@ -61,14 +61,16 @@ class WalletTypeForm extends StatefulWidget {
required this.isCreate,
required this.newWalletTypeViewModel,
this.onTypeSelected,
required this.isHardwareWallet,
this.hardwareWalletType,
});
final bool isCreate;
final Image walletImage;
final NewWalletTypeViewModel newWalletTypeViewModel;
final void Function(BuildContext, WalletType)? onTypeSelected;
final bool isHardwareWallet;
final HardwareWalletType? hardwareWalletType;
bool get isHardwareWallet => hardwareWalletType != null;
@override
WalletTypeFormState createState() => WalletTypeFormState();
@@ -90,7 +92,9 @@ class WalletTypeFormState extends State<WalletTypeForm> {
types = filteredTypes = availableWalletTypes
.where((element) =>
!widget.isHardwareWallet ||
DeviceConnectionType.supportedConnectionTypes(element, Platform.isIOS).isNotEmpty)
DeviceConnectionType.supportedConnectionTypes(
element, widget.hardwareWalletType!, Platform.isIOS)
.isNotEmpty)
.toList();
super.initState();
@@ -148,7 +152,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
isSelected: selected == type,
onTap: () => setState(() => selected = type),
deviceConnectionTypes: widget.isHardwareWallet
? DeviceConnectionType.supportedConnectionTypes(type, Platform.isIOS)
? DeviceConnectionType.supportedConnectionTypes(
type, widget.hardwareWalletType!, Platform.isIOS)
: [],
),
),
@@ -111,7 +111,7 @@ class QRWidget extends StatelessWidget {
),
if (addressListViewModel.isPayjoinUnavailable &&
!addressListViewModel.isSilentPayments &&
!addressListViewModel.isCupcake) ...[
!addressListViewModel.isBitcoinViewOnly) ...[
GestureDetector(
onTap: () => _onPayjoinInactivePressed(context),
child: Row(
@@ -1,10 +1,9 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
@@ -12,11 +11,9 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
class RestoreOptionsPage extends BasePage {
RestoreOptionsPage({required this.isNewInstall});
@@ -27,9 +24,8 @@ class RestoreOptionsPage extends BasePage {
final bool isNewInstall;
@override
Widget body(BuildContext context) {
return _RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type);
}
Widget body(BuildContext context) =>
_RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type);
}
class _RestoreOptionsBody extends StatefulWidget {
@@ -45,41 +41,25 @@ class _RestoreOptionsBody extends StatefulWidget {
class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
bool isRestoring = false;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS)
.isNotEmpty;
}
String get imageRestoreHWPath => widget.themeType == ThemeType.dark
? 'assets/images/restore_hw_dark.png'
: 'assets/images/restore_hw.png';
return true;
}
String get imageRestoreCupcakePath => widget.themeType == ThemeType.dark
? 'assets/images/restore_cupcake_dark.png'
: 'assets/images/restore_cupcake.png';
String get imageRestoreHWPath =>
widget.themeType == ThemeType.dark
? 'assets/images/restore_hw_dark.png'
: 'assets/images/restore_hw.png';
String get imageRestoreQRPath => widget.themeType == ThemeType.dark
? 'assets/images/restore_qr_dark.png'
: 'assets/images/restore_qr.png';
String get imageRestoreCupcakePath =>
widget.themeType == ThemeType.dark
? 'assets/images/restore_cupcake_dark.png'
: 'assets/images/restore_cupcake.png';
String get imageRestoreHotWalletPath => widget.themeType == ThemeType.dark
? 'assets/images/restore_hot_wallet_dark.png'
: 'assets/images/restore_hot_wallet.png';
String get imageRestoreQRPath =>
widget.themeType == ThemeType.dark
? 'assets/images/restore_qr_dark.png'
: 'assets/images/restore_qr.png';
String get imageRestoreHotWalletPath =>
widget.themeType == ThemeType.dark
? 'assets/images/restore_hot_wallet_dark.png'
: 'assets/images/restore_hot_wallet.png';
String get imageRestoreBackupPath =>
widget.themeType == ThemeType.dark
? 'assets/images/restore_backup_dark.png'
: 'assets/images/restore_backup.png';
String get imageRestoreBackupPath => widget.themeType == ThemeType.dark
? 'assets/images/restore_backup_dark.png'
: 'assets/images/restore_backup.png';
@override
Widget build(BuildContext context) {
@@ -116,18 +96,20 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
tag: S.of(context).new_tag,
),
),
if (_doesSupportHardwareWallets)
Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
key: ValueKey('restore_options_from_hardware_wallet_button_key'),
onPressed: () =>
Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet),
image: imageRestoreHW,
title: S.of(context).restore_title_from_hardware_wallet,
description: S.of(context).restore_description_from_hardware_wallet,
),
Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
key: ValueKey('restore_options_from_hardware_wallet_button_key'),
onPressed: () => Navigator.pushNamed(
context,
isSingleCoin
? Routes.connectHardwareWallet
: Routes.restoreWalletFromHardwareWallet),
image: imageRestoreHW,
title: S.of(context).restore_title_from_hardware_wallet,
description: S.of(context).restore_description_from_hardware_wallet,
),
),
if (widget.isNewInstall)
Padding(
padding: EdgeInsets.only(top: 24),
@@ -143,11 +125,11 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
key: ValueKey('restore_options_from_qr_button_key'),
onPressed: () => _onScanQRCode(context),
image: imageRestoreQR,
title: S.of(context).scan_qr_code,
description: S.of(context).cold_or_recover_wallet,
key: ValueKey('restore_options_from_qr_button_key'),
onPressed: () => _onScanQRCode(context),
image: imageRestoreQR,
title: S.of(context).scan_qr_code,
description: S.of(context).cold_or_recover_wallet,
),
),
],
@@ -184,7 +166,11 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
final restoredWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context);
final params = {'walletType': restoredWallet.type, 'restoredWallet': restoredWallet};
final params = {
'walletType': restoredWallet.type,
'restoredWallet': restoredWallet,
'hardwareWalletType': HardwareWalletType.cupcake,
};
Navigator.pushNamed(context, Routes.restoreWallet, arguments: params).then((_) {
if (mounted) setState(() => isRestoring = false);
+7 -4
View File
@@ -440,18 +440,21 @@ class SendPage extends BasePage {
}
if (sendViewModel.wallet.isHardwareWallet) {
if (!sendViewModel.ledgerViewModel!.isConnected) {
if (!sendViewModel.hardwareWalletViewModel!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: sendViewModel.walletType,
hardwareWalletType:
sendViewModel.wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (BuildContext context, _) {
sendViewModel.ledgerViewModel!
.setLedger(sendViewModel.wallet);
sendViewModel.hardwareWalletViewModel!
.initWallet(sendViewModel.wallet);
Navigator.of(context).pop();
},
));
} else {
sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet);
sendViewModel.hardwareWalletViewModel!
.initWallet(sendViewModel.wallet);
}
}
@@ -9,6 +9,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.da
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -26,16 +27,14 @@ class OtherSettingsPage extends BasePage {
final OtherSettingsViewModel _otherSettingsViewModel;
@override
Widget body(BuildContext context) {
return Observer(
builder: (_) {
return Container(
Widget body(BuildContext context) => Observer(
builder: (_) => Container(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
if (_otherSettingsViewModel.displayTransactionPriority)
_otherSettingsViewModel.walletType == WalletType.bitcoin ?
SettingsPriorityPickerCell(
_otherSettingsViewModel.walletType == WalletType.bitcoin
? SettingsPriorityPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority,
@@ -44,8 +43,8 @@ class OtherSettingsPage extends BasePage {
onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected,
customValue: _otherSettingsViewModel.customBitcoinFeeRate,
maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(),
) :
SettingsPickerCell(
)
: SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayPriority,
@@ -55,75 +54,96 @@ class OtherSettingsPage extends BasePage {
if (_otherSettingsViewModel.changeRepresentativeEnabled)
SettingsCellWithArrow(
title: S.current.change_rep,
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.changeRep),
handler: (context) => Navigator.of(context).pushNamed(Routes.changeRep),
),
if (_otherSettingsViewModel.changeHardwareWalletTypeEnabled)
SettingsCellWithArrow(
title: "Hardware wallet manufacturer",
handler: (context) => Navigator.of(context)
.pushNamed(Routes.restoreWalletFromHardwareWallet, arguments: {
"showUnavailable": false,
"availableHardwareWalletTypes": [
HardwareWalletType.cupcake,
HardwareWalletType.coldcard,
HardwareWalletType.seedsigner,
],
"onSelect": (BuildContext context, HardwareWalletType hwType) async {
await _otherSettingsViewModel.onHardwareWalletTypeChanged(hwType);
Navigator.pop(context);
},
}),
),
SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.readDisclaimer),
handler: (context) => Navigator.of(context).pushNamed(Routes.readDisclaimer),
),
if (FeatureFlag.hasDevOptions && _otherSettingsViewModel.walletType == WalletType.monero)
if (FeatureFlag.hasDevOptions &&
_otherSettingsViewModel.walletType == WalletType.monero)
SettingsCellWithArrow(
title: '[dev] monero background sync',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devMoneroBackgroundSync),
),
if (FeatureFlag.hasDevOptions && [WalletType.monero, WalletType.wownero, WalletType.zano].contains(_otherSettingsViewModel.walletType))
if (FeatureFlag.hasDevOptions &&
[WalletType.monero, WalletType.wownero, WalletType.zano]
.contains(_otherSettingsViewModel.walletType))
SettingsCellWithArrow(
title: '[dev] xmr call profiler',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
),
if (FeatureFlag.hasDevOptions && [WalletType.monero].contains(_otherSettingsViewModel.walletType))
if (FeatureFlag.hasDevOptions &&
[WalletType.monero].contains(_otherSettingsViewModel.walletType))
SettingsCellWithArrow(
title: '[dev] xmr wallet cache debug',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devMoneroWalletCacheDebug),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] shared preferences',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devSharedPreferences),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] secure storage preferences',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devSecurePreferences),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] background sync logs',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] socket health logs',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devSocketHealthLogs),
handler: (context) => Navigator.of(context).pushNamed(Routes.devSocketHealthLogs),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] network requests logs',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devNetworkRequests),
handler: (context) => Navigator.of(context).pushNamed(Routes.devNetworkRequests),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] exchange provider logs',
handler: (BuildContext context) =>
handler: (context) =>
Navigator.of(context).pushNamed(Routes.devExchangeProviderLogs),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] *QR tools',
handler: (context) => Navigator.of(context).pushNamed(Routes.devQRTools),
),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
title: S.of(context).version(_otherSettingsViewModel.currentVersion),
),
],
),
);
},
);
}
),
);
}
+2 -1
View File
@@ -61,6 +61,7 @@ class AnimatedURPage extends BasePage {
child: URQR(
urqr: urQr,
walletType: animatedURmodel.wallet.type,
hardwareWalletType: animatedURmodel.wallet.hardwareWalletType,
),
),
if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header]
@@ -125,4 +126,4 @@ class AnimatedURPage extends BasePage {
});
}
}
}
}
+83 -101
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
@@ -7,67 +6,65 @@ import 'package:cake_wallet/src/screens/ur/widgets/qr_selection_dialog.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
class URQR extends StatefulWidget {
URQR({super.key, required this.urqr, required this.walletType});
const URQR({super.key, required this.urqr, required this.walletType, this.hardwareWalletType});
final Map<String, String> urqr;
final WalletType walletType;
final HardwareWalletType? hardwareWalletType;
@override
// ignore: library_private_types_in_public_api
_URQRState createState() => _URQRState();
}
const urFrameTime = 1000 ~/ 5;
class _URQRState extends State<URQR> {
Timer? t;
Timer? _timer;
int frame = 0;
int selectedInt = 0;
@override
void initState() {
super.initState();
setState(() {
t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) {
_nextFrame();
});
});
}
final keys = widget.urqr.keys.toList();
if (widget.hardwareWalletType == HardwareWalletType.coldcard) {
selectedInt = 1;
selected = keys[(selectedInt) % keys.length];
} else if (widget.hardwareWalletType == HardwareWalletType.seedsigner) {
selectedInt = 2;
selected = keys[(selectedInt) % keys.length];
}
void _nextFrame() {
setState(() {
frame++;
});
_timer = Timer.periodic(const Duration(milliseconds: urFrameTime), (_) => _nextFrame());
super.initState();
}
@override
void dispose() {
t?.cancel();
_timer?.cancel();
super.dispose();
}
late String selected =
(widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first;
int selectedInt = 0;
List<String> get frames {
return widget.urqr[selected]?.split("\n") ?? [];
}
late String nextLabel =
widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length];
void next() {
String get nextLabel => widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length];
void next() => setState(() {
final keys = widget.urqr.keys.toList();
setState(() {
selectedInt++;
nextLabel = keys[(selectedInt + 1) % keys.length];
selected = keys[(selectedInt) % keys.length];
});
}
selectedInt++;
selected = keys[(selectedInt) % keys.length];
});
late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first;
List<String> get frames => widget.urqr[selected]?.split("\n") ?? [];
void _nextFrame() => setState(() => frame++);
@override
Widget build(BuildContext context) {
@@ -107,45 +104,44 @@ All frames:
);
}
Widget _legacySwitch(BuildContext context) {
return SizedBox(
width: double.maxFinite,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: next,
text: nextLabel,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
Widget _legacySwitch(BuildContext context) => SizedBox(
width: double.maxFinite,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: next,
text: nextLabel,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
);
}
);
Widget _newSwitch(BuildContext context) {
return Column(
children: [
SizedBox(height: 32),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.qr_code,
color: Theme.of(context).colorScheme.primary, size: 24),
SizedBox(width: 4),
Text("QR Code Format",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
)),
Icon(Icons.qr_code, color: Theme.of(context).colorScheme.primary, size: 24),
const SizedBox(width: 4),
Text(
"QR Code Format",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
SizedBox(height: 16),
const SizedBox(height: 16),
Container(
margin: EdgeInsets.symmetric(horizontal: 16),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(12),
@@ -176,7 +172,7 @@ All frames:
),
),
),
SizedBox(width: 16),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -193,10 +189,7 @@ All frames:
_getCurrentFormatSubtitle(),
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
@@ -204,10 +197,7 @@ All frames:
),
Icon(
Icons.chevron_right,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.4),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
],
@@ -216,7 +206,7 @@ All frames:
),
),
),
SizedBox(height: 16),
const SizedBox(height: 16),
GestureDetector(
onTap: () => _showInfoDialog(context),
child: Row(
@@ -226,35 +216,32 @@ All frames:
"What's this for?",
style: TextStyle(
fontSize: 14,
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
SizedBox(width: 4),
const SizedBox(width: 4),
Icon(
Icons.info_outline,
size: 16,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
],
),
),
SizedBox(height: 32),
const SizedBox(height: 32),
],
);
}
String _getCurrentFormatName() {
if (widget.urqr.isEmpty) return "Unknown";
final currentKey =
widget.urqr.keys.elementAt(selectedInt % widget.urqr.length);
final currentKey = widget.urqr.keys.elementAt(selectedInt % widget.urqr.length);
return currentKey.split(' ')[0];
}
String _getCurrentFormatSubtitle() {
if (widget.urqr.isEmpty) return "";
final currentKey =
widget.urqr.keys.elementAt(selectedInt % widget.urqr.length);
final currentKey = widget.urqr.keys.elementAt(selectedInt % widget.urqr.length);
return currentKey.substring(currentKey.indexOf(' ') + 1);
}
@@ -264,30 +251,25 @@ All frames:
final keys = widget.urqr.keys.toList();
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return QRFormatSelectionDialog(
formats: keys,
currentIndex: selectedInt,
onFormatSelected: (index) {
Navigator.pop(context);
setState(() {
selectedInt = index;
selected = keys[index];
nextLabel = keys[(index + 1) % keys.length];
});
},
);
});
context: context,
builder: (context) => QRFormatSelectionDialog(
formats: keys,
currentIndex: selectedInt,
onFormatSelected: (index) {
Navigator.pop(context);
setState(() {
selectedInt = index;
selected = keys[index];
});
},
),
);
}
Future<void> _showInfoDialog(BuildContext context) async {
await showModalBottomSheet(
Future<void> _showInfoDialog(BuildContext context) => showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return QRFormatInfoBottomSheet();
});
}
builder: (_) => QRFormatInfoBottomSheet(),
);
}
@@ -21,9 +21,11 @@ import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -477,8 +479,9 @@ class WalletListBodyState extends State<WalletListBody> {
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: WalletType.monero,
hardwareWalletType: HardwareWalletType.ledger,
onConnectDevice: (context, ledgerVM) async {
monero!.setGlobalLedgerConnection(ledgerVM.connection);
if (ledgerVM is LedgerViewModel) monero!.setGlobalLedgerConnection(ledgerVM.connection);
didConnect = true;
Navigator.of(context).pop();
},