Merge branch 'dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	cw_core/lib/format_fixed.dart
#	cw_solana/lib/solana_client.dart
#	lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart
#	lib/di.dart
#	lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart
#	lib/new-ui/widgets/receive_page/receive_top_bar.dart
#	lib/solana/cw_solana.dart
#	lib/src/screens/buy/buy_sell_page.dart
#	lib/src/screens/connect_device/connect_device_page.dart
#	lib/view_model/buy/buy_sell_view_model.dart
#	lib/view_model/hardware_wallet/hardware_wallet_view_model.dart
#	lib/view_model/hardware_wallet/ledger_view_model.dart
#	lib/view_model/send/send_view_model.dart
This commit is contained in:
Robert Malikowski
2026-07-17 13:25:45 +02:00
99 changed files with 5021 additions and 4178 deletions
@@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
@@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage {
),
bottomCardChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: card.denominationItems.isNotEmpty
? DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominationItems,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
: EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
child: Column(children: [
if (card.prepaidRange.isNotEmpty)
PrepaidRangeAmountWidget(
fiatCurrency: card.fiatCurrency.title,
prepaidRanges: card.prepaidRange,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
)
else if (card.denominationItems.isNotEmpty)
DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominationItems,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
)
else
EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
),
]))),
Expanded(
flex: 2,
child: Padding(
+108 -21
View File
@@ -1,8 +1,21 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
enum CakePayCardType {
prepaid('Prepaid Cards'),
gift('gift'),
onDemand('on_demand'),
custom('custom');
const CakePayCardType(this.apiValue);
final String apiValue;
}
class CakePayCard {
final int id;
final String name;
final CakePayCardType? type;
final String? description;
final String? termsAndConditions;
final String? howToUse;
@@ -15,26 +28,77 @@ class CakePayCard {
final String? minValue;
final String? maxValue;
final List<Denomination> denominationItems;
final List<PrepaidRange> prepaidRange;
CakePayCard({
required this.id,
required this.name,
this.description,
this.termsAndConditions,
this.howToUse,
this.expiryAndValidity,
this.cardImageUrl,
this.country,
required this.fiatCurrency,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
CakePayCard(
{required this.id,
required this.name,
this.type,
this.description,
this.termsAndConditions,
this.howToUse,
this.expiryAndValidity,
this.cardImageUrl,
this.country,
required this.fiatCurrency,
this.minValueUsd,
this.maxValueUsd,
this.minValue,
this.maxValue,
List<Denomination>? denominationItems,
List<PrepaidRange>? prepaidRange})
: denominationItems = denominationItems ?? const [],
prepaidRange = prepaidRange ?? const [];
CakePayCard copyWith({
int? id,
String? name,
CakePayCardType? type,
String? description,
String? termsAndConditions,
String? howToUse,
String? expiryAndValidity,
String? cardImageUrl,
String? country,
FiatCurrency? fiatCurrency,
String? minValueUsd,
String? maxValueUsd,
String? minValue,
String? maxValue,
List<Denomination>? denominationItems,
}) : denominationItems = denominationItems ?? const [];
List<PrepaidRange>? prepaidRange,
}) {
return CakePayCard(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
description: description ?? this.description,
termsAndConditions: termsAndConditions ?? this.termsAndConditions,
howToUse: howToUse ?? this.howToUse,
expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity,
cardImageUrl: cardImageUrl ?? this.cardImageUrl,
country: country ?? this.country,
fiatCurrency: fiatCurrency ?? this.fiatCurrency,
minValueUsd: minValueUsd ?? this.minValueUsd,
maxValueUsd: maxValueUsd ?? this.maxValueUsd,
minValue: minValue ?? this.minValue,
maxValue: maxValue ?? this.maxValue,
denominationItems: denominationItems ?? this.denominationItems,
prepaidRange: prepaidRange ?? this.prepaidRange,
);
}
static CakePayCardType? cakePayCardTypeFromApi(String? value) {
if (value == null) return null;
final normalized = value.trim().toLowerCase();
return CakePayCardType.values.firstWhereOrNull(
(e) => e.apiValue.toLowerCase() == normalized,
);
}
factory CakePayCard.fromJson(Map<String, dynamic> json) {
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
final typeString = json['type'] as String? ?? '';
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
@@ -63,9 +127,12 @@ class CakePayCard {
}
}
final cakePayCardType = cakePayCardTypeFromApi(typeString);
return CakePayCard(
id: json['id'] as int? ?? 0,
name: name,
type: cakePayCardType,
description: description,
termsAndConditions: termsAndConditions,
howToUse: howToUse,
@@ -103,12 +170,6 @@ class Denomination {
this.usdValue,
});
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
factory Denomination.fromJson(Map<String, dynamic> json) {
return Denomination(
value: _toDouble(json['value']) ?? 0,
@@ -117,3 +178,29 @@ class Denomination {
);
}
}
class PrepaidRange {
final double minValue;
final double maxValue;
final String rangeId;
PrepaidRange({
required this.minValue,
required this.maxValue,
required this.rangeId,
});
factory PrepaidRange.fromCard(CakePayCard card) {
return PrepaidRange(
minValue: _toDouble(card.minValue) ?? 0,
maxValue: _toDouble(card.maxValue) ?? 0,
rangeId: card.id.toString(),
);
}
}
double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
+20 -3
View File
@@ -31,9 +31,26 @@ class CakePayVendor {
if (cardsJson != null && cardsJson.isNotEmpty) {
try {
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
return Country.normalizeName(card['country'] as String) == country;
}) as Map<String, dynamic>);
final cards = cardsJson
.map((cardJson) => CakePayCard.fromJson(cardJson as Map<String, dynamic>))
.toList();
final cardsForSelectedCountry =
cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList();
if (cardsForSelectedCountry.isNotEmpty) {
final isPrepaid =
cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid);
if (isPrepaid) {
final firstCard = cardsForSelectedCountry.first;
final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList()
..sort((a, b) => a.minValue.compareTo(b.minValue));
cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges);
} else {
cardForVendor = cardsForSelectedCountry.first;
}
}
} catch (e) {
printV('Error parsing card for vendor: $e');
}
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
class CakePayApi {
static const testBaseUri = FeatureFlag.hasDevOptions;
static const testBaseUri = false; //FeatureFlag.hasDevOptions;
static const baseTestCakePayUri = 'api-stg.cakepay.com';
static const baseProdCakePayUri = 'api-prod.cakepay.com';
@@ -0,0 +1,134 @@
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:flutter/material.dart';
import 'enter_amount_widget.dart';
class PrepaidRangeAmountWidget extends StatefulWidget {
const PrepaidRangeAmountWidget({
required this.fiatCurrency,
required this.prepaidRanges,
required this.amountFieldFocus,
required this.amountController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
});
final String fiatCurrency;
final List<PrepaidRange> prepaidRanges;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
@override
State<PrepaidRangeAmountWidget> createState() => _PrepaidRangeAmountWidgetState();
}
class _PrepaidRangeAmountWidgetState extends State<PrepaidRangeAmountWidget> {
late PrepaidRange _selectedRange;
@override
void initState() {
super.initState();
_selectedRange = widget.prepaidRanges.first;
_applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select range:',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<PrepaidRange>(
value: _selectedRange,
isExpanded: true,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
filled: true,
fillColor: Theme.of(context).cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.onSurface.withAlpha(40),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
items: widget.prepaidRanges
.map(
(range) => DropdownMenuItem<PrepaidRange>(
value: range,
child: Text(_rangeLabel(range)),
),
)
.toList(),
onChanged: (range) {
if (range == null) return;
setState(() => _selectedRange = range);
widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range);
_applySelectedRange(forceAmountUpdate: true);
},
),
const SizedBox(height: 12),
EnterAmountWidget(
minValue: _formatAmount(_selectedRange.minValue),
maxValue: _formatAmount(_selectedRange.maxValue),
fiatCurrency: widget.fiatCurrency,
amountFieldFocus: widget.amountFieldFocus,
amountController: widget.amountController,
onAmountChanged: (value) {
widget.onAmountChanged(value);
widget.cakePayBuyCardViewModel.selectedDenomination = (
value.replaceAll(',', '.'),
int.tryParse(_selectedRange.rangeId),
);
},
),
],
);
}
void _applySelectedRange({required bool forceAmountUpdate}) {
final currentAmount = double.tryParse(widget.amountController.text);
final isCurrentAmountInRange = currentAmount != null &&
currentAmount >= _selectedRange.minValue &&
currentAmount <= _selectedRange.maxValue;
final nextAmount = forceAmountUpdate || !isCurrentAmountInRange
? _formatAmount(_selectedRange.minValue)
: widget.amountController.text;
widget.amountController.text = nextAmount;
widget.onAmountChanged(nextAmount);
widget.cakePayBuyCardViewModel.selectedDenomination = (
nextAmount.replaceAll(',', '.'),
int.tryParse(_selectedRange.rangeId),
);
}
String _rangeLabel(PrepaidRange range) {
return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}';
}
String _formatAmount(double value) {
return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2);
}
}
@@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
height: screenHeight * 0.50,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
borderRadius: constBorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
Flexible(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25.0),
bottomRight: Radius.circular(25.0),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
color: Theme.of(context).colorScheme.surfaceContainerLow,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
height: screenHeight * 0.38,
width: double.infinity,
child: topCardChild)),
bottomCardChild,
color: Theme.of(context).colorScheme.surfaceContainerLow,
),
constraints: BoxConstraints(
maxHeight: screenHeight * 0.38,
),
width: double.infinity,
child: topCardChild,
),
),
),
Flexible(
child: bottomCardChild,
),
],
),
),
+7 -4
View File
@@ -423,16 +423,19 @@ Future<void> setup({
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
throw Exception(
"This should not have happened, because airgapped Wallets don't need View Models");
"This should not have happened, because airgapped Wallets don't need View Models",
);
}
});
getIt.registerLazySingleton(() => LedgerViewModel());
getIt.registerLazySingleton(() => LedgerViewModel(getIt<AppStore>()));
getIt.registerLazySingleton(() => BitboxViewModel());
getIt.registerLazySingleton(BitboxViewModel.new);
getIt.registerLazySingleton(
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"));
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
);
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
+3
View File
@@ -166,12 +166,15 @@ class CWEVM extends EVM {
amount: amount,
fee: fee,
tokenSymbol: tokenSymbol,
exponent: exponent,
direction: direction,
isPending: isPending,
date: date,
confirmations: confirmations,
to: to,
from: from,
evmSignatureName: evmSignatureName,
contractAddress: contractAddress,
chainId: chainId);
@override
+3
View File
@@ -433,6 +433,9 @@ class CWMonero extends Monero {
@override
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
@override
bool hasGlobalLedgerConnection() => gLedger != null;
@override
String? getLastLedgerCommand() => latestLedgerCommand;
@@ -49,6 +49,7 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
final oldTabLength = tabs.length;
final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled);
final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
chainId: widget.dashboardViewModel.wallet.chainId);
tabs = [
@@ -58,13 +59,14 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
AssetsSection(
dashboardViewModel: widget.dashboardViewModel,
),
hasAssetsButton ?
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
() {
Navigator.of(context).pushNamed(
Routes.homeSettings,
arguments: widget.dashboardViewModel.balanceViewModel,
);
})),
}) : null),
AssetsHistorySectionTab(
S.current.history,
HistorySection(
@@ -12,7 +12,6 @@ class ModalTopBar extends StatelessWidget {
this.onTrailingPressed = nothing,
this.leadingIcon,
this.trailingIcon,
this.bottomText,
this.padding,
this.leadingWidget,
this.trailingWidget}) {
@@ -75,6 +75,23 @@ void startAuthenticationStateChange(
);
return;
}
ledgerVM.suppressMoneroReconnectPrompt = true;
if (!monero!.hasGlobalLedgerConnection()) {
await showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: "Unexpected Error",
alertContent:
"Unexpected Error while trying to connect to the device (has no global LedgerConnection)",
buttonText: S.of(context).try_again,
buttonAction: Navigator.of(context).pop,
),
);
return;
}
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
@@ -85,14 +102,17 @@ void startAuthenticationStateChange(
buttonAction: () => Navigator.of(context).pop(),
),
);
bool tryOpening = true;
var isConnected = false;
var tryOpening = true;
while (tryOpening) {
try {
await loadCurrentWallet();
isConnected = true;
tryOpening = false;
} on Exception catch (e) {
final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString());
if (ledgerErrorMessage != null) {
if (ledgerErrorMessage != null && context.mounted) {
await showPopUp<void>(
context: context,
builder: (context) => AlertWithTwoActions(
@@ -116,16 +136,17 @@ void startAuthenticationStateChange(
}
getIt.get<BottomSheetService>().showNext();
await navigatorKey.currentState!
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
if (isConnected) {
ledgerVM.suppressMoneroReconnectPrompt = false;
await navigatorKey.currentState!
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
},
allowChangeWallet: true,
),
);
// await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice));
} else {
navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final linkViewModel = getIt.get<LinkViewModel>();
+16 -3
View File
@@ -122,9 +122,13 @@ class CWSolana extends Solana {
return CryptoCurrency.sol;
}
return (wallet as SolanaWallet).splTokenCurrencies.firstWhere(
(element) => transaction.amount.currency.symbol == element.symbol,
);
final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
if (token == null) {
throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}');
}
return token;
}
@override
@@ -401,6 +405,15 @@ class CWSolana extends Solana {
}
await Future.wait(tokenChecks);
final discoveredMints = result.newTokens
.where((item) => item.token.enabled)
.map((item) => item.token.mintAddress)
.toList();
if (discoveredMints.isNotEmpty) {
await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
}
} catch (_) {}
}
@@ -1,54 +1,54 @@
import 'dart:async';
import 'dart:io';
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/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/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';
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: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/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";
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/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
class ConnectDevicePageParams {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final HardwareWalletType hardwareWalletType;
ConnectDevicePageParams({
const ConnectDevicePageParams({
required this.walletType,
required this.hardwareWalletType,
required this.onConnectDevice,
this.allowChangeWallet = false,
this.isReconnect = true,
});
}
class ConnectDevicePage extends BasePage {
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final HardwareWalletViewModel hardwareWalletVM;
final HardwareWalletType hardwareWalletType;
}
class ConnectDevicePage extends BasePage {
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
: walletType = params.walletType,
onConnectDevice = params.onConnectDevice,
allowChangeWallet = params.allowChangeWallet,
isReconnect = params.isReconnect;
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final bool isReconnect;
final HardwareWalletViewModel hardwareWalletVM;
@override
String get title => isReconnect
@@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage {
@override
Widget body(BuildContext context) => PopScope(
canPop: !isReconnect,
child: ConnectDevicePageBody(
walletType,
onConnectDevice,
allowChangeWallet,
hardwareWalletVM,
currentTheme,
));
canPop: !isReconnect,
child: ConnectDevicePageBody(
walletType,
onConnectDevice,
hardwareWalletVM,
currentTheme,
allowChangeWallet: allowChangeWallet,
),
);
}
class ConnectDevicePageBody extends StatefulWidget {
const ConnectDevicePageBody(
this.walletType,
this.onConnectDevice,
this.hardwareWalletVM,
this.currentTheme, {
this.allowChangeWallet = false,
});
final WalletType walletType;
final OnConnectDevice onConnectDevice;
final bool allowChangeWallet;
final HardwareWalletViewModel hardwareWalletVM;
final MaterialThemeBase currentTheme;
const ConnectDevicePageBody(
this.walletType,
this.onConnectDevice,
this.allowChangeWallet,
this.hardwareWalletVM,
this.currentTheme,
);
@override
ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState();
}
@@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
late Timer? _usbRefreshTimer = null;
late Timer? _bleRefreshTimer = null;
late Timer? _bleStateTimer = null;
late StreamSubscription<HardwareWalletDevice>? _bleRefresh = null;
Timer? _usbRefreshTimer;
Timer? _bleRefreshTimer;
Timer? _bleStateTimer;
StreamSubscription<HardwareWalletDevice>? _bleRefresh;
bool longWait = false;
Timer? _longWaitTimer;
@@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_bleStateTimer =
Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState());
_bleStateTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => widget.hardwareWalletVM.updateBleState(),
);
_bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
_bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices());
if (Platform.isAndroid) {
_usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
_usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices());
}
if (widget.hardwareWalletVM.hasBluetooth) {
_longWaitTimer = Timer(Duration(seconds: 10), () {
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty)
_longWaitTimer = Timer(const Duration(seconds: 10), () {
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
setState(() => longWait = true);
}
});
}
});
@@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
_longWaitTimer?.cancel();
widget.hardwareWalletVM.stopScanning();
_isConnectPressed = false;
super.dispose();
}
Future<void> _refreshUsbDevices() async {
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) setState(() => usbDevices = dev);
if (usbDevices.length != dev.length) {
setState(() => usbDevices = dev);
}
}
Future<void> _refreshBleDevices() async {
try {
if (widget.hardwareWalletVM.isBleEnabled) {
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() {
bleDevices.add(device);
if (longWait) longWait = false;
}))
..onError((Object e) {
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen(
(device) => setState(() {
bleDevices.add(device);
if (longWait) {
longWait = false;
}
}),
)..onError((e) {
printV(e);
});
_bleRefreshTimer?.cancel();
@@ -160,48 +171,67 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
}
}
var _isConnectPressed = false;
Future<void> _connectToDevice(HardwareWalletDevice device) async {
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
if (_isConnectPressed) {
return;
}
_isConnectPressed = true;
try {
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
_isConnectPressed = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isConnected)
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
});
{
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
}
});
} catch (_) {
_isConnectPressed = false;
}
}
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
printV(deviceType);
switch (deviceType) {
case HardwareWalletDeviceType.ledgerNanoX:
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletDeviceType.ledgerNanoGen5:
return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg';
return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg";
case HardwareWalletDeviceType.ledgerStax:
return 'assets/new-ui/hardware_wallets/ledger_stax.svg';
return "assets/new-ui/hardware_wallets/device_ledger_stax.svg";
case HardwareWalletDeviceType.ledgerFlex:
return 'assets/new-ui/hardware_wallets/ledger_flex.svg';
return "assets/new-ui/hardware_wallets/device_ledger_flex.svg";
case HardwareWalletDeviceType.BitBox02:
case HardwareWalletDeviceType.BitBox02Nova:
return 'assets/new-ui/hardware_wallets/device_bitbox.svg';
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletDeviceType.trezorModelOne:
return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg';
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';
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';
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';
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';
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
default:
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
}
}
String get description {
if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble;
if (!Platform.isAndroid) {
return S.of(context).connect_your_hardware_wallet_ble;
}
if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet;
if (widget.hardwareWalletVM.hasBluetooth) {
return S.of(context).connect_your_hardware_wallet;
}
return S.of(context).connect_your_hardware_wallet_usb;
}
@@ -210,7 +240,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
Widget build(BuildContext context) => Center(
child: Container(
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
child: Column(
children: [
Expanded(
@@ -218,9 +248,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
child: Column(
spacing: 24,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/hardware_wallet.svg",
),
const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"),
Text(
description,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
@@ -231,7 +259,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
offstage: widget.hardwareWalletVM.isBleEnabled ||
!widget.hardwareWalletVM.hasBluetooth,
child: Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
child: Text(
S.of(context).ledger_please_enable_bluetooth,
style: Theme.of(context).textTheme.titleMedium,
@@ -242,67 +270,89 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18)),
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),
behavior: HitTestBehavior.opaque,
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",
shrinkWrap: true,
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
final item = allDevices[index];
return GestureDetector(
onTap: () => _connectToDevice(item),
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 12,
children: [
Observer(
builder: (_) {
if (widget.hardwareWalletVM.isConnecting) {
return CupertinoActivityIndicator(
animating: true,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
radius: 12,
);
}
return CakeImageWidget(
imageUrl: _getDeviceTileLeading(item.type),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn),
),
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
);
},
),
Text(item.name),
],
),
Row(
spacing: 12,
children: [
if (item.connectionType ==
HardwareWalletConnectionType.ble)
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_forward.svg",
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn))
],
)
],
),
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,
),
),
],
),
],
),
),
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh),
itemCount: allDevices.length),
),
);
},
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
itemCount: allDevices.length,
),
),
if (widget.allowChangeWallet) ...[
PrimaryButton(
@@ -310,7 +360,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: _onChangeWallet,
)
),
],
],
),
@@ -321,7 +371,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary,
onPressed: () => _onHowToConnect(context),
)
),
],
),
),
@@ -339,17 +389,25 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet(
builder: (_) => InfoStepsBottomSheet(
titleText: S.of(context).how_to_connect,
steps: [
InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg',
S.of(context).connect_hw_info_step_1),
InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg',
S.of(context).connect_hw_info_step_2),
InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg',
S.of(context).connect_hw_info_step_3),
InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg',
S.of(context).connect_hw_info_step_4),
InfoStep(
"assets/images/wallet_connect_step_icons/step1_power.svg",
S.of(context).connect_hw_info_step_1,
),
InfoStep(
"assets/images/wallet_connect_step_icons/step2_connect.svg",
S.of(context).connect_hw_info_step_2,
),
InfoStep(
"assets/images/wallet_connect_step_icons/step3_unlock.svg",
S.of(context).connect_hw_info_step_3,
),
InfoStep(
"assets/images/wallet_connect_step_icons/step4_select.svg",
S.of(context).connect_hw_info_step_4,
),
],
),
);
+24 -23
View File
@@ -1,25 +1,25 @@
import 'dart:async';
import "dart:async";
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:shared_preferences/shared_preferences.dart';
import "package:cake_wallet/core/amount_parsing_proxy.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/entities/preferences_key.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart";
import "package:cake_wallet/store/authentication_store.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:cake_wallet/store/wallet_list_store.dart";
import "package:cake_wallet/themes/core/theme_store.dart";
import "package:cake_wallet/utils/exception_handler.dart";
import "package:cw_core/balance.dart";
import "package:cw_core/transaction_history.dart";
import "package:cw_core/transaction_info.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
import "package:shared_preferences/shared_preferences.dart";
part 'app_store.g.dart';
part "app_store.g.dart";
class AppStore = AppStoreBase with _$AppStore;
@@ -59,8 +59,9 @@ abstract class AppStoreBase with Store {
@action
Future<void> changeCurrentWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet) async {
bool changingToSameWalletType = this.wallet?.type == wallet.type;
this.wallet?.close(shouldCleanup: !changingToSameWalletType);
final changingToSameWalletType = this.wallet?.type == wallet.type;
await this.wallet?.close(shouldCleanup: !changingToSameWalletType);
this.wallet = wallet;
this.wallet!.setExceptionHandler(ExceptionHandler.onError);
@@ -80,7 +81,7 @@ abstract class AppStoreBase with Store {
wcService.create();
await wcService.init();
} catch (e, s) {
printV('WalletConnect setup failed: $e\n$s');
printV("WalletConnect setup failed: $e\n$s");
}
}
}
@@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
: 0,
quantity = 1,
card = vendor.card!,
min = _toDouble(vendor.card!.minValue) ?? 0,
max = _toDouble(vendor.card!.maxValue) ?? 0 {
min = vendor.card!.prepaidRange.isNotEmpty
? vendor.card!.prepaidRange.first.minValue
: _toDouble(vendor.card!.minValue) ?? 0,
max = vendor.card!.prepaidRange.isNotEmpty
? vendor.card!.prepaidRange.first.maxValue
: _toDouble(vendor.card!.maxValue) ?? 0 {
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
}
@@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
final CakePayVendor vendor;
final SendViewModel sendViewModel;
final CakePayService _cakePayService;
final double max;
final double min;
final CakePayCard card;
final WalletType walletType;
final Box<Order> orders;
@@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
bool confirmsNoVpn = false;
bool confirmsVoidedRefund = false;
bool confirmsTermsAgreed = false;
double max;
double min;
(String, int?) selectedDenomination = ('', null);
String simulatedResponse = '';
@@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store {
@action
void onQuantityChanged(int? input) => quantity = input ?? 1;
void onPrepaidRangeChanged(PrepaidRange range) {
min = range.minValue;
max = range.maxValue;
selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId));
if (amount < min || amount > max) {
amount = min;
}
}
@action
void onAmountChanged(String input) {
if (input.isEmpty) return;
amount = double.parse(input.replaceAll(',', '.'));
if (card.prepaidRange.isNotEmpty) {
selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2);
}
}
@action
@@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store {
sendViewModel.state =
FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.');
}
final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty;
int selectedCardId = card.id;
String selectedPrice = amount.toString();
if (isPrepaidRangeSelected) {
selectedCardId = selectedDenomination.$2 ?? card.id;
selectedPrice =
selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString();
} else if (isDenominationSelected) {
selectedCardId = selectedDenomination.$2 ?? card.id;
selectedPrice = selectedDenomination.$1;
}
try {
order = await _cakePayService.createOrder(
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
cardId: selectedCardId,
price: selectedPrice,
quantity: quantity,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
@@ -14,7 +14,6 @@ import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
@@ -498,7 +497,6 @@ abstract class BalanceViewModelBase with Store {
final record = wallet.balance[curr]!;
final available = record.available - (record.secondAvailable ?? Money.zero(curr));
final price = fiatConversionStore.prices[curr] ?? 0;
printV(record.available);
ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
.replaceAll(",", "")) ??
0;
@@ -227,9 +227,10 @@ abstract class DashboardViewModelBase with Store {
1;
} catch (_) {}
} else {
confirmations = appStore.wallet!.transactionHistory.transactions.values
.map((item) => item.isPending)
.fold(0, (val, pending) => pending ? val + 1 : val);
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
.where((item) => item.isPending)
.length;
confirmations = pendingCount + 1;
}
return length * confirmations;
}, _transactionDisposerCallback, delay: 300);
@@ -1331,9 +1332,10 @@ abstract class DashboardViewModelBase with Store {
1;
} catch (_) {}
} else {
confirmations = appStore.wallet!.transactionHistory.transactions.values
.map((item) => item.isPending)
.fold(0, (val, pending) => pending ? val + 1 : val);
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
.where((item) => item.isPending)
.length;
confirmations = pendingCount + 1;
}
return length * confirmations;
}, _transactionDisposerCallback, delay: 300);
@@ -195,7 +195,7 @@ class TransactionListItem extends ActionListItem with Keyable {
case WalletType.decred:
case WalletType.zcash:
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(transaction.amount.toString()),
cryptoAmount: transaction.amount.toDouble(),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
case WalletType.ethereum:
@@ -206,15 +206,15 @@ class TransactionListItem extends ActionListItem with Keyable {
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(transaction.amount.toString()),
cryptoAmount: transaction.amount.toDouble(),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.solana:
final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(transaction.amount.toString()),
cryptoAmount: transaction.amount.toDouble(),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -222,7 +222,7 @@ class TransactionListItem extends ActionListItem with Keyable {
final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(transaction.amount.toString()),
cryptoAmount: transaction.amount.toDouble(),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -234,7 +234,7 @@ class TransactionListItem extends ActionListItem with Keyable {
}
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: double.parse(transaction.amount.toString()),
cryptoAmount: transaction.amount.toDouble(),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
@@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
Future<void> stopScanning() async {}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is BitboxHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (isConnecting) return false;
isConnecting = true;
try {
final appDocDir = await getAppDir();
@@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
await bitboxManager.channelHashVerify();
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
bool _isConnecting = false;
@override
bool isConnected(WalletType type) => false;
@@ -1,13 +1,14 @@
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.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:cake_wallet/entities/hardware_wallet/hardware_wallet_device.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";
abstract class HardwareWalletViewModel {
HardwareWalletType get hardwareWalletType;
bool get isBleEnabled;
bool get hasBluetooth;
bool get isConnecting;
bool isConnected(WalletType type);
@@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel {
Future<void> initWallet(WalletBase wallet);
String? interpretErrorCode(String error) => null;
Future<void> close() async {}
}
@@ -1,50 +1,47 @@
import 'dart:async';
import 'dart:io';
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/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.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:flutter/widgets.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
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/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
import "package:cake_wallet/store/app_store.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:flutter/widgets.dart";
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
part 'ledger_view_model.g.dart';
part "ledger_view_model.g.dart";
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
.isNotEmpty;
}
return true;
}
LedgerViewModelBase() {
LedgerViewModelBase(AppStore appStore) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
if (isBleEnabled) {
_initBLE();
}
});
reaction((_) => appStore.wallet, (wallet) async {
if (wallet != null && wallet.type != _connectingWalletType) {
await close();
}
});
updateBleState();
if (!Platform.isIOS) {
@@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
}
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero,
HardwareWalletType.ledger,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
@@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => true;
@@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (isBleEnabled && !_bleIsInitialized) {
ledgerPlusBLE = sdk.LedgerInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
if (Platform.isMacOS) {
return true;
}
Map<Permission, PermissionStatus> statuses = await [
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
@@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
@@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
if (_bleIsInitialized) {
await ledgerPlusBLE.stopScanning();
}
if (!Platform.isIOS) {
await ledgerPlusUSB.stopScanning();
}
}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is LedgerHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! LedgerHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
_connectingWalletType = type;
if (isConnected(type)) {
try {
@@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final ledger =
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
if (_connectionChangeSubscription == null) {
_connectionChangeSubscription =
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
}
_connectionChangeSubscription ??=
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
try {
_connection = await ledger.connect(device.device);
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
sdk.LedgerConnection? _connection;
bool _isConnecting = false;
WalletType? _connectingWalletType;
bool suppressMoneroReconnectPrompt = false;
void _connectionChangeListener(sdk.BleConnectionState event) {
printV('Ledger Device State Changed: $event');
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
printV("Ledger Device State Changed: $event");
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
_connection = null;
if (_connectingWalletType == WalletType.monero) {
monero!.resetLedgerConnection();
if (suppressMoneroReconnectPrompt) {
return;
}
Navigator.of(navigatorKey.currentContext!).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
@@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
hardwareWalletType: HardwareWalletType.ledger,
allowChangeWallet: true,
isReconnect: true,
onConnectDevice: (context, ledgerVM) async {
onConnectDevice: (context, ledgerVM) {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
@@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
@override
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
sdk.LedgerConnection get connection => _connection!;
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.monero:
return monero!.setLedgerConnection(wallet, connection);
case WalletType.bitcoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
}
}
@@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return error;
}
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
if (errorCode.contains("6985")) {
return S.current.ledger_error_tx_rejected_by_user;
} else if (errorCode.contains("5515")) {
return S.current.ledger_error_device_locked;
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
return S.current.ledger_error_wrong_app;
}
return null;
}
@override
Future<void> close() async {
try {
printV("Resetting ledger VM");
await _connectionChangeSubscription?.cancel();
await _connection?.disconnect().catchError((_) {});
_connectingWalletType = null;
_connectionChangeSubscription = null;
_connection = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
@@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isConnecting = false;
@override
@observable
bool isBleEnabled = false;
@@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
if (!Platform.isIOS) await trezorUSB.stopScanning();
}
bool _isConnecting = false;
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
@@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
void setParingPin(String pin) => _pinCompleter?.complete(pin);
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
[bool isRetry = false]) async {
if (!(device is TrezorHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (isConnecting) return false;
isConnecting = true;
paringState = TrezorParingState.initial;
try {
@@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
printV(e);
return false;
} finally {
_isConnecting = false;
isConnecting = false;
}
}
+4
View File
@@ -1121,6 +1121,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
if (isEVMWallet) {
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
);
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
id: pendingTransaction!.evmTxHashFromRawHex!,
height: 0,
@@ -1132,6 +1135,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
date: DateTime.now(),
confirmations: 0,
chainId: wallet.chainId ?? 0,
contractAddress: selectedToken?.contractAddress,
));
}
@@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store {
if (walletItem.type == WalletType.haven) {
return;
}
// bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type;
// await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType);
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
await _appStore.changeCurrentWallet(wallet);
updateList();