mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user