mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
CW-1581-trezor-fixes-enhancements (#3462)
* feat: add Trezor auto connect * chore: update `trezor_flutter` * fix: correct method name for retrieving Trezor auto-pairing credentials * fix: update device connection prompt text across all languages [skip ci] * feat: add Trezor key image synchronization ui and improve hardware wallet UX * feat: add sync-balance icon asset for settings row visuals [skip ci] * feat: add sync-balance icon asset for settings row visuals [skip ci] * fix: remove unused Monero and sync key images settings actions from menus [skip ci] * fix: re-enable routing for UR QR Animated Page [skip ci]
This commit is contained in:
@@ -19,6 +19,7 @@ import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/picker.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
@@ -42,7 +43,6 @@ import "package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_shee
|
||||
import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/src/widgets/standard_checkbox.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/utils/payment_request.dart";
|
||||
@@ -618,9 +618,9 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
selectedDot: _selectedOutput,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
builder: (_) => NewFuturePrimaryButton(
|
||||
key: const ValueKey("send_page_send_button_key"),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
//Request dummy node to get the focus out of the text fields
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
|
||||
@@ -629,11 +629,11 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
}
|
||||
|
||||
if (widget.mode == SendPageModes.normal) {
|
||||
_handleSend();
|
||||
await _handleSend();
|
||||
} else if (widget.mode ==
|
||||
SendPageModes.lightningDeposit ||
|
||||
widget.mode == SendPageModes.mwebDeposit) {
|
||||
Navigator.of(context).push(
|
||||
await Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
child: L2ActionWalletSelector(
|
||||
@@ -652,7 +652,7 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
} else if (widget.mode ==
|
||||
SendPageModes.lightningWithdrawal ||
|
||||
widget.mode == SendPageModes.mwebWithdrawal) {
|
||||
Navigator.of(context).push(
|
||||
await Navigator.of(context).push(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => Material(
|
||||
child: L2ActionWalletSelector(
|
||||
@@ -673,13 +673,7 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: widget.sendViewModel.state is IsExecutingState ||
|
||||
widget.sendViewModel.state is TransactionCommitting ||
|
||||
widget.sendViewModel.state
|
||||
is IsAwaitingDeviceResponseState ||
|
||||
widget.sendViewModel.state
|
||||
is LoadingTemplateExecutingState,
|
||||
isDisabled: !widget.sendViewModel.isReadyForSend ||
|
||||
disabled: !widget.sendViewModel.isReadyForSend ||
|
||||
widget.sendViewModel.state is ExecutedSuccessfullyState,
|
||||
),
|
||||
),
|
||||
@@ -858,17 +852,11 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
for (final item in widget.sendViewModel.outputs) {
|
||||
amount += item.cryptoAmountMoney;
|
||||
}
|
||||
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
||||
if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) {
|
||||
await monero!.syncTrezor(widget.sendViewModel.wallet);
|
||||
} else {
|
||||
if (mounted) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.urqrAnimatedPage,
|
||||
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (monero!.hasUnknownKeyImages(widget.sendViewModel.wallet) && mounted) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.syncKeyImagesDevices,
|
||||
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet),
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1)); // wait for monero to refresh the state
|
||||
}
|
||||
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
||||
|
||||
@@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
bool _trueFunc(DashboardViewModel _) => true;
|
||||
|
||||
@@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
|
||||
|
||||
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
|
||||
|
||||
bool _isCupcake(DashboardViewModel vm) =>
|
||||
vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
|
||||
bool _requiresKeyImageSync(DashboardViewModel vm) =>
|
||||
vm.wallet.type == WalletType.monero &&
|
||||
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
|
||||
|
||||
class SettingsListItem {
|
||||
const SettingsListItem(
|
||||
this.iconPath,
|
||||
this.title,
|
||||
this.route, {
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc,
|
||||
this.routeArgs,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String route;
|
||||
@@ -37,21 +47,15 @@ class SettingsListItem {
|
||||
final bool requireAuth;
|
||||
final bool Function(DashboardViewModel) use2fa;
|
||||
final bool Function(DashboardViewModel) condition;
|
||||
|
||||
const SettingsListItem(this.iconPath, this.title, this.route,
|
||||
{this.routeArgs = null,
|
||||
this.requireAuth = false,
|
||||
this.use2fa = _falseFunc,
|
||||
this.condition = _trueFunc});
|
||||
}
|
||||
|
||||
class SettingsSectionData {
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
|
||||
final String title;
|
||||
final String titleIconPath;
|
||||
final List<SettingsListItem> items;
|
||||
|
||||
const SettingsSectionData(this.title, this.titleIconPath, this.items);
|
||||
|
||||
static SettingsSectionData walletSettings =
|
||||
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
|
||||
SettingsListItem(
|
||||
@@ -71,9 +75,13 @@ class SettingsSectionData {
|
||||
condition: _hasWalletConnect),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
|
||||
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
|
||||
SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs,
|
||||
Routes.urqrAnimatedPage,
|
||||
routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/sync-balance.svg",
|
||||
S.current.resync_device,
|
||||
Routes.syncKeyImagesDevices,
|
||||
routeArgs: {'export-outputs': 'export-outputs'},
|
||||
condition: _requiresKeyImageSync,
|
||||
),
|
||||
SettingsListItem(
|
||||
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import "package:cake_wallet/generated/i18n.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/routes.dart";
|
||||
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/store/app_store.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_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SyncKeyImagesSheet extends StatefulWidget {
|
||||
const SyncKeyImagesSheet({
|
||||
required this.appStore,
|
||||
required this.trezorConnectVM,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppStore appStore;
|
||||
final TrezorConnectViewModelBase trezorConnectVM;
|
||||
|
||||
WalletBase get wallet => appStore.wallet!;
|
||||
|
||||
@override
|
||||
State<SyncKeyImagesSheet> createState() => _HardwareWalletProceedOnDeviceSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletProceedOnDeviceSheetState extends State<SyncKeyImagesSheet> {
|
||||
_KeyImageSyncState _state = _KeyImageSyncState.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PopScope(
|
||||
canPop: false,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).resync_device,
|
||||
leadingIcon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onLeadingPressed: () => showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).are_you_sure_exit,
|
||||
alertContent: S.of(context).resync_device_cancel_warning_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: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: hardwareWalletIcon,
|
||||
width: 100,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
DirectionalAnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: content,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.easeOutQuad,
|
||||
opacity: _state == _KeyImageSyncState.initial ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: PrimaryButton(
|
||||
text: S.of(context).continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onContinuePressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get content {
|
||||
if (_state == _KeyImageSyncState.syncing) {
|
||||
return _syncingKeyImages();
|
||||
}
|
||||
|
||||
return _initial();
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (widget.wallet.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:
|
||||
case null:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _initial() => Column(
|
||||
key: const ValueKey(0),
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).resync_device_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _syncingKeyImages() => Column(
|
||||
key: const ValueKey(1),
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> _onContinuePressed() async {
|
||||
setState(() => _state = _KeyImageSyncState.syncing);
|
||||
|
||||
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: widget.wallet.type,
|
||||
hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (_, __) {
|
||||
widget.trezorConnectVM.initWallet(widget.wallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isReconnect: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Recheck to handle tap-backs
|
||||
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
|
||||
setState(() => _state = _KeyImageSyncState.initial);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await widget.trezorConnectVM.initWallet(widget.wallet);
|
||||
}
|
||||
|
||||
final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet);
|
||||
if (result && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _KeyImageSyncState {
|
||||
initial,
|
||||
syncing;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_svg/flutter_svg.dart";
|
||||
|
||||
class NewFuturePrimaryButton extends StatefulWidget {
|
||||
const NewFuturePrimaryButton({
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.disabled = false,
|
||||
this.image,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function() onPressed;
|
||||
final bool disabled;
|
||||
final SvgPicture? image;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final Color borderColor;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _NewFuturePrimaryButtonState();
|
||||
}
|
||||
|
||||
class _NewFuturePrimaryButtonState extends State<NewFuturePrimaryButton> {
|
||||
bool isLoading = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setState(() => isLoading = true);
|
||||
try {
|
||||
await widget.onPressed.call();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewPrimaryButton(
|
||||
onPressed: _onPressed,
|
||||
image: widget.image,
|
||||
text: widget.text,
|
||||
color: widget.color,
|
||||
textColor: widget.textColor,
|
||||
isLoading: isLoading,
|
||||
borderColor: widget.borderColor,
|
||||
disabled: widget.disabled,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user