Merge remote-tracking branch 'origin/dev' into CW-1529-Improved-Seed-UI

# Conflicts:
#	lib/new-ui/pages/home_page.dart
#	res/values/strings_ar.arb
#	res/values/strings_bg.arb
#	res/values/strings_cs.arb
#	res/values/strings_es.arb
#	res/values/strings_fa.arb
#	res/values/strings_fr.arb
#	res/values/strings_gn.arb
#	res/values/strings_ha.arb
#	res/values/strings_hi.arb
#	res/values/strings_hr.arb
#	res/values/strings_hy.arb
#	res/values/strings_id.arb
#	res/values/strings_it.arb
#	res/values/strings_ja.arb
#	res/values/strings_ko.arb
#	res/values/strings_my.arb
#	res/values/strings_nl.arb
#	res/values/strings_pl.arb
#	res/values/strings_pt.arb
#	res/values/strings_pt_BR.arb
#	res/values/strings_ru.arb
#	res/values/strings_th.arb
#	res/values/strings_tl.arb
#	res/values/strings_tr.arb
#	res/values/strings_uk.arb
#	res/values/strings_ur.arb
#	res/values/strings_vi.arb
#	res/values/strings_yo.arb
#	res/values/strings_zh.arb
This commit is contained in:
Blazebrain
2026-09-15 10:45:35 +01:00
116 changed files with 6791 additions and 893 deletions
+4 -3
View File
@@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
enum ProviderRecommendation { bestRate, lowKyc, successRate }
@@ -13,11 +14,11 @@ extension RecommendationTitle on ProviderRecommendation {
String get title {
switch (this) {
case ProviderRecommendation.bestRate:
return 'BEST RATE';
return S.current.best_rate;
case ProviderRecommendation.lowKyc:
return 'LOW KYC';
return S.current.low_kyc;
case ProviderRecommendation.successRate:
return 'HIGHEST SUCCESS RATE';
return S.current.highest_success_rate;
}
}
}
+21 -11
View File
@@ -115,7 +115,7 @@ extension PaymentTypeTitle on PaymentType {
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card.svg';
return 'assets/new-ui/buy_payment_methods/debit_card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_light.svg';
case PaymentType.skrill:
@@ -132,17 +132,21 @@ extension PaymentTypeTitle on PaymentType {
String? get darkIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_dark.svg';
return 'assets/new-ui/buy_payment_methods/all_methods.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card_dark.svg';
return 'assets/new-ui/buy_payment_methods/debit_card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_dark.svg';
return 'assets/new-ui/buy_payment_methods/bank_transfer.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_dark.svg';
return 'assets/new-ui/buy_payment_methods/apple_pay.svg';
case PaymentType.googlePay:
return "assets/new-ui/buy_payment_methods/google_pay.svg";
case PaymentType.paypal:
return "assets/new-ui/buy_payment_methods/paypal.svg";
case PaymentType.revolutPay:
return 'assets/images/revolut_dark.svg';
default:
@@ -150,6 +154,12 @@ extension PaymentTypeTitle on PaymentType {
}
}
bool get isMonochromeIcon => [
"assets/new-ui/buy_payment_methods/all_methods.svg",
"assets/new-ui/buy_payment_methods/debit_card.svg",
"assets/new-ui/buy_payment_methods/bank_transfer.svg"
].contains(darkIconPath);
String? get description {
switch (this) {
default:
@@ -190,7 +200,7 @@ class PaymentMethod extends SelectableOption {
return PaymentMethod(
paymentMethodType: PaymentType.all,
customTitle: 'All Payment Methods',
customIconPath: 'assets/images/dollar_coin.svg');
customIconPath: 'assets/new-ui/buy_payment_methods/all_methods.svg');
}
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
@@ -199,7 +209,7 @@ class PaymentMethod extends SelectableOption {
paymentMethodType: type ?? PaymentType.unknown,
customPaymentMethodType: json['paymentTypeId'] as String?,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
customIconPath: json['icon'] as String? ?? 'assets/new-ui/buy_payment_methods/debit_card.svg',
customDescription: json['description'] as String?);
}
@@ -207,14 +217,14 @@ class PaymentMethod extends SelectableOption {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: paymentMethod,
customIconPath: 'assets/images/card.png');
customIconPath: 'assets/new-ui/buy_payment_methods/debit_card.svg');
}
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: json['paymentMethod'] as String,
customIconPath: 'assets/images/card.png');
customIconPath: 'assets/new-ui/buy_payment_methods/debit_card.svg');
}
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
@@ -223,7 +233,7 @@ class PaymentMethod extends SelectableOption {
return PaymentMethod(
paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
customIconPath: logos['dark'] as String? ?? 'assets/new-ui/buy_payment_methods/debit_card.svg',
customDescription: json['description'] as String?);
}
@@ -232,7 +242,7 @@ class PaymentMethod extends SelectableOption {
return PaymentMethod(
paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['payment_method'] as String? ?? 'Unknown',
customIconPath: 'assets/images/card.png',
customIconPath: 'assets/new-ui/buy_payment_methods/debit_card.svg',
);
}
+48 -8
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
@@ -13,13 +14,19 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
class CsvExportService {
CsvExportService({required this.transactionDescriptionBox});
final Box<TransactionDescription> transactionDescriptionBox;
static const _columns = [
'record_type',
'date_time',
@@ -42,23 +49,46 @@ class CsvExportService {
static const _utf8Bom = '';
static const _formulaTriggers = {'=', '+', '-', '@', '\t', '\r'};
static final _plainNumber = RegExp(r'^-?\d+(\.\d+)?$');
String buildCsvContent(List<ActionListItem> items) {
final buf = StringBuffer();
buf.write(_utf8Bom);
buf.writeln(_columns.join(','));
final primaryAddress = _primaryAddressOf(items);
for (final item in items) {
if (item is DateSectionItem) continue;
final row = _buildRow(item);
final row = _buildRow(item, primaryAddress);
if (row != null) buf.writeln(row);
}
return buf.toString();
}
String? _buildRow(ActionListItem item) {
if (item is TransactionListItem) return _transactionRow(item);
String _primaryAddressOf(List<ActionListItem> items) {
for (final item in items) {
if (item is TransactionListItem) {
return item.balanceViewModel.wallet.walletAddresses.primaryAddress;
}
}
return '';
}
String _noteFor(TransactionListItem item, String primaryAddress) {
final txHash = item.transaction.txHash;
final descriptionKey = '${txHash}_$primaryAddress';
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == txHash);
return description?.note ?? '';
}
String? _buildRow(ActionListItem item, String primaryAddress) {
if (item is TransactionListItem) return _transactionRow(item, primaryAddress);
if (item is TradeListItem) return _tradeRow(item);
if (item is OrderListItem) return _orderRow(item);
if (item is AnonpayTransactionListItem) return _anonpayRow(item);
@@ -66,7 +96,7 @@ class CsvExportService {
return null;
}
String _transactionRow(TransactionListItem item) {
String _transactionRow(TransactionListItem item, String primaryAddress) {
final tx = item.transaction;
final type = tx.direction == TransactionDirection.incoming ? 'incoming' : 'outgoing';
final status = tx.isPending ? 'pending' : 'confirmed';
@@ -88,7 +118,7 @@ class CsvExportService {
tx.id,
address,
status,
'',
_noteFor(item, primaryAddress),
'',
'',
'',
@@ -200,10 +230,20 @@ class CsvExportService {
String _row(List<String> fields) => fields.map(escapeField).join(',');
String escapeField(String field) {
if (field.contains(',') || field.contains('"') || field.contains('\n')) {
return '"${field.replaceAll('"', '""')}"';
final value = _neutralizeFormula(field);
if (value.contains(',') ||
value.contains('"') ||
value.contains('\n') ||
value.contains('\r')) {
return '"${value.replaceAll('"', '""')}"';
}
return field;
return value;
}
String _neutralizeFormula(String field) {
if (field.isEmpty || !_formulaTriggers.contains(field[0])) return field;
if (_plainNumber.hasMatch(field)) return field;
return "'$field";
}
String _isoDate(DateTime dt) => dt.toUtc().toIso8601String();
+20 -1
View File
@@ -22,6 +22,7 @@ import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/csv_export_service.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
@@ -56,6 +57,9 @@ import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
@@ -67,6 +71,7 @@ import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
@@ -599,6 +604,13 @@ Future<void> setup({
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
(dashboardViewModel, _) => AccountCustomizer(
accountListViewModel: getIt.get<MoneroAccountListViewModel>(),
dashboardViewModel: dashboardViewModel,
),
);
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
@@ -1408,6 +1420,9 @@ Future<void> setup({
param1: derivations,
)));
getIt.registerFactory<CsvExportService>(
() => CsvExportService(transactionDescriptionBox: _transactionDescriptionBox));
getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>((params, _) {
final transactionInfo = params[0] as TransactionInfo;
final canReplaceByFee = params[1] as bool? ?? false;
@@ -1493,7 +1508,11 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<BuySellViewModel, BuySellPageMode, void>((mode, _) => BuySellViewModel(
mode: mode, getIt.get<AppStore>(), fiatConversionStore: getIt.get<FiatConversionStore>()));
getIt.registerFactoryParam<NewBuySellAmountPage, BuySellPageMode, void>((mode, _) =>
NewBuySellAmountPage(buySellViewModel: getIt.get<BuySellViewModel>(param1: mode)));
getIt.registerFactory(
() => BuySellPage(getIt.get<BuySellViewModel>(), getIt.get<AddressResolverService>()));
+2 -1
View File
@@ -8,8 +8,9 @@ String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = fals
cryptoAmount = cryptoAmount.sanitized();
final _amount = double.tryParse(cryptoAmount);
if (_amount == null || _amount.isNaN) return '0.00';
if (_amount == null || !_amount.isFinite) return '0.00';
final _result = price * _amount;
if (!_result.isFinite) return '0.00';
final result = _result < 0 ? _result * -1 : _result;
if (result == 0.0) {
@@ -7,6 +7,10 @@ String calculateFiatAmountRaw({required double cryptoAmount, double? price}) {
final result = price * cryptoAmount;
if (!result.isFinite) {
return '0.00';
}
if (result == 0.0) {
return '0.00';
}
+1 -1
View File
@@ -52,7 +52,7 @@ const moneroWorldNodeUri = '.moneroworld.com';
const decredDefaultUri = "default-spv-nodes";
const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
const arbitrumDefaultNodeUri = 'arbitrum-one-rpc.publicnode.com';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
const zcashDefaultNodeUri = 'zec.rocks:443';
+148 -64
View File
@@ -1,12 +1,13 @@
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/enumerable_item.dart';
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/enumerable_item.dart";
class FiatCurrency extends EnumerableItem<String> with Serializable<String> implements Currency {
const FiatCurrency({
required this.symbol,
required this.countryCode,
required this.fullName,
required this.emoji,
this.decimals = 2,
}) : super(title: symbol, raw: symbol);
@@ -21,6 +22,8 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
@override
final String symbol;
final String emoji;
static List<FiatCurrency> get all => _all.values.toList();
static List<FiatCurrency> get currenciesAvailableToBuyWith => [
@@ -68,67 +71,148 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
kes,
];
static const amd = FiatCurrency(symbol: 'AMD', countryCode: "arm", fullName: "Armenian Dram");
static const ars = FiatCurrency(symbol: 'ARS', countryCode: "arg", fullName: "Argentine Peso");
static const aud = FiatCurrency(symbol: 'AUD', countryCode: "aus", fullName: "Australian Dollar");
static const bdt = FiatCurrency(symbol: 'BDT', countryCode: "bgd", fullName: "Bangladeshi Taka");
static const bgn = FiatCurrency(symbol: 'BGN', countryCode: "bgr", fullName: "Bulgarian Lev");
static const brl = FiatCurrency(symbol: 'BRL', countryCode: "bra", fullName: "Brazilian Real");
static const cad = FiatCurrency(symbol: 'CAD', countryCode: "cad", fullName: "Canadian Dollar");
static const chf = FiatCurrency(symbol: 'CHF', countryCode: "che", fullName: "Swiss Franc");
static const clp = FiatCurrency(symbol: 'CLP', countryCode: "chl", fullName: "Chilean Peso");
static const cny = FiatCurrency(symbol: 'CNY', countryCode: "chn", fullName: "Chinese Yuan");
static const cop = FiatCurrency(symbol: 'COP', countryCode: "col", fullName: "Colombian Peso");
static const czk = FiatCurrency(symbol: 'CZK', countryCode: "czk", fullName: "Czech Koruna");
static const dkk = FiatCurrency(symbol: 'DKK', countryCode: "dnk", fullName: "Danish Krone");
static const egp = FiatCurrency(symbol: 'EGP', countryCode: "egy", fullName: "Egyptian Pound");
static const eur = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
static const gbp = FiatCurrency(symbol: 'GBP', countryCode: "gbr", fullName: "Pound Sterling");
static const ghs = FiatCurrency(symbol: 'GHS', countryCode: "gha", fullName: "Ghanaian Cedi");
static const gtq =
FiatCurrency(symbol: 'GTQ', countryCode: "gtm", fullName: "Guatemalan Quetzal");
static const hkd = FiatCurrency(symbol: 'HKD', countryCode: "hkg", fullName: "Hong Kong Dollar");
static const hrk = FiatCurrency(symbol: 'HRK', countryCode: "hrv", fullName: "Croatian Kuna");
static const huf = FiatCurrency(symbol: 'HUF', countryCode: "hun", fullName: "Hungarian Forint");
static const idr = FiatCurrency(symbol: 'IDR', countryCode: "idn", fullName: "Indonesian Rupiah");
static const ils =
FiatCurrency(symbol: 'ILS', countryCode: "isr", fullName: "Israeli New Shekel");
static const inr = FiatCurrency(symbol: 'INR', countryCode: "ind", fullName: "Indian Rupee");
static const irr = FiatCurrency(symbol: 'IRR', countryCode: "irn", fullName: "Iranian Rial");
static const isk =
FiatCurrency(symbol: 'ISK', countryCode: "isl", fullName: "Icelandic Krona Króna");
static const jpy = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen");
static const krw = FiatCurrency(symbol: 'KRW', countryCode: "kor", fullName: "South Korean Won");
static const mad = FiatCurrency(symbol: 'MAD', countryCode: "mar", fullName: "Moroccan Dirham");
static const mxn = FiatCurrency(symbol: 'MXN', countryCode: "mex", fullName: "Mexican Peso");
static const myr = FiatCurrency(symbol: 'MYR', countryCode: "mys", fullName: "Malaysian Ringgit");
static const ngn = FiatCurrency(symbol: 'NGN', countryCode: "nga", fullName: "Nigerian Naira");
static const nok = FiatCurrency(symbol: 'NOK', countryCode: "nor", fullName: "Norwegian Krone");
static const nzd =
FiatCurrency(symbol: 'NZD', countryCode: "nzl", fullName: "New Zealand Dollar");
static const php = FiatCurrency(symbol: 'PHP', countryCode: "phl", fullName: "Philippine Peso");
static const pkr = FiatCurrency(symbol: 'PKR', countryCode: "pak", fullName: "Pakistani Rupee");
static const pln =
FiatCurrency(symbol: 'PLN', countryCode: "pol", fullName: "Poland Zloty złoty");
static const ron = FiatCurrency(symbol: 'RON', countryCode: "rou", fullName: "Romanian Leu");
static const rub = FiatCurrency(symbol: 'RUB', countryCode: "rus", fullName: "Russian Ruble");
static const sar = FiatCurrency(symbol: 'SAR', countryCode: "sau", fullName: "Saudi Riyal");
static const sek = FiatCurrency(symbol: 'SEK', countryCode: "swe", fullName: "Swedish Krona");
static const sgd = FiatCurrency(symbol: 'SGD', countryCode: "sgp", fullName: "Singapore Dollar");
static const thb =
FiatCurrency(symbol: 'THB', countryCode: "tha", fullName: "New Thaiwan Dollar");
static const twd = FiatCurrency(symbol: 'TWD', countryCode: "twn", fullName: "Thai Baht");
static const uah = FiatCurrency(symbol: 'UAH', countryCode: "ukr", fullName: "Ukrainian Hryvnia");
static const usd =
FiatCurrency(symbol: 'USD', countryCode: "usa", fullName: "United States Dollar");
static const vef =
FiatCurrency(symbol: 'VEF', countryCode: "ven", fullName: "Venezuelan Bolivar Bolívar");
static const vnd =
FiatCurrency(symbol: 'VND', countryCode: "vnm", fullName: "Vietnamese Dong đồng");
static const zar =
FiatCurrency(symbol: 'ZAR', countryCode: "saf", fullName: "South African Rand");
static const tur = FiatCurrency(symbol: 'TRY', countryCode: "tur", fullName: "Turkish Lira");
static const kes = FiatCurrency(symbol: 'KES', countryCode: "ken", fullName: "Kenyan Shillings");
static const amd =
FiatCurrency(symbol: "AMD", countryCode: "arm", fullName: "Armenian Dram", emoji: "🇦🇲");
static const ars =
FiatCurrency(symbol: "ARS", countryCode: "arg", fullName: "Argentine Peso", emoji: "🇦🇷");
static const aud =
FiatCurrency(symbol: "AUD", countryCode: "aus", fullName: "Australian Dollar", emoji: "🇦🇺");
static const bdt =
FiatCurrency(symbol: "BDT", countryCode: "bgd", fullName: "Bangladeshi Taka", emoji: "🇧🇩");
static const bgn =
FiatCurrency(symbol: "BGN", countryCode: "bgr", fullName: "Bulgarian Lev", emoji: "🇧🇬");
static const brl =
FiatCurrency(symbol: "BRL", countryCode: "bra", fullName: "Brazilian Real", emoji: "🇧🇷");
static const cad =
FiatCurrency(symbol: "CAD", countryCode: "cad", fullName: "Canadian Dollar", emoji: "🇨🇦");
static const chf =
FiatCurrency(symbol: "CHF", countryCode: "che", fullName: "Swiss Franc", emoji: "🇨🇭");
static const clp =
FiatCurrency(symbol: "CLP", countryCode: "chl", fullName: "Chilean Peso", emoji: "🇨🇱");
static const cny =
FiatCurrency(symbol: "CNY", countryCode: "chn", fullName: "Chinese Yuan", emoji: "🇨🇳");
static const cop =
FiatCurrency(symbol: "COP", countryCode: "col", fullName: "Colombian Peso", emoji: "🇨🇴");
static const czk =
FiatCurrency(symbol: "CZK", countryCode: "czk", fullName: "Czech Koruna", emoji: "🇨🇿");
static const dkk =
FiatCurrency(symbol: "DKK", countryCode: "dnk", fullName: "Danish Krone", emoji: "🇩🇰");
static const egp =
FiatCurrency(symbol: "EGP", countryCode: "egy", fullName: "Egyptian Pound", emoji: "🇪🇬");
static const eur =
FiatCurrency(symbol: "EUR", countryCode: "eur", fullName: "Euro", emoji: "🇪🇺");
static const gbp =
FiatCurrency(symbol: "GBP", countryCode: "gbr", fullName: "Pound Sterling", emoji: "🇬🇧");
static const ghs =
FiatCurrency(symbol: "GHS", countryCode: "gha", fullName: "Ghanaian Cedi", emoji: "🇬🇭");
static const gtq = FiatCurrency(
symbol: "GTQ",
countryCode: "gtm",
fullName: "Guatemalan Quetzal",
emoji: "🇬🇹",
);
static const hkd =
FiatCurrency(symbol: "HKD", countryCode: "hkg", fullName: "Hong Kong Dollar", emoji: "🇭🇰");
static const hrk =
FiatCurrency(symbol: "HRK", countryCode: "hrv", fullName: "Croatian Kuna", emoji: "🇭🇷");
static const huf =
FiatCurrency(symbol: "HUF", countryCode: "hun", fullName: "Hungarian Forint", emoji: "🇭🇺");
static const idr =
FiatCurrency(symbol: "IDR", countryCode: "idn", fullName: "Indonesian Rupiah", emoji: "🇮🇩");
static const ils = FiatCurrency(
symbol: "ILS",
countryCode: "isr",
fullName: "Israeli New Shekel",
emoji: "🇮🇱",
);
static const inr =
FiatCurrency(symbol: "INR", countryCode: "ind", fullName: "Indian Rupee", emoji: "🇮🇳");
static const irr =
FiatCurrency(symbol: "IRR", countryCode: "irn", fullName: "Iranian Rial", emoji: "🇮🇷");
static const isk = FiatCurrency(
symbol: "ISK",
countryCode: "isl",
fullName: "Icelandic Krona Króna",
emoji: "🇮🇸",
);
static const jpy =
FiatCurrency(symbol: "JPY", countryCode: "jpn", fullName: "Japanese Yen", emoji: "🇯🇵");
static const krw =
FiatCurrency(symbol: "KRW", countryCode: "kor", fullName: "South Korean Won", emoji: "🇰🇷");
static const mad =
FiatCurrency(symbol: "MAD", countryCode: "mar", fullName: "Moroccan Dirham", emoji: "🇲🇦");
static const mxn =
FiatCurrency(symbol: "MXN", countryCode: "mex", fullName: "Mexican Peso", emoji: "🇲🇽");
static const myr =
FiatCurrency(symbol: "MYR", countryCode: "mys", fullName: "Malaysian Ringgit", emoji: "🇲🇾");
static const ngn =
FiatCurrency(symbol: "NGN", countryCode: "nga", fullName: "Nigerian Naira", emoji: "🇳🇬");
static const nok =
FiatCurrency(symbol: "NOK", countryCode: "nor", fullName: "Norwegian Krone", emoji: "🇳🇴");
static const nzd = FiatCurrency(
symbol: "NZD",
countryCode: "nzl",
fullName: "New Zealand Dollar",
emoji: "🇳🇿",
);
static const php =
FiatCurrency(symbol: "PHP", countryCode: "phl", fullName: "Philippine Peso", emoji: "🇵🇭");
static const pkr =
FiatCurrency(symbol: "PKR", countryCode: "pak", fullName: "Pakistani Rupee", emoji: "🇵🇰");
static const pln = FiatCurrency(
symbol: "PLN",
countryCode: "pol",
fullName: "Poland Zloty złoty",
emoji: "🇵🇱",
);
static const ron =
FiatCurrency(symbol: "RON", countryCode: "rou", fullName: "Romanian Leu", emoji: "🇷🇴");
static const rub =
FiatCurrency(symbol: "RUB", countryCode: "rus", fullName: "Russian Ruble", emoji: "🇷🇺");
static const sar =
FiatCurrency(symbol: "SAR", countryCode: "sau", fullName: "Saudi Riyal", emoji: "🇸🇦");
static const sek =
FiatCurrency(symbol: "SEK", countryCode: "swe", fullName: "Swedish Krona", emoji: "🇸🇪");
static const sgd =
FiatCurrency(symbol: "SGD", countryCode: "sgp", fullName: "Singapore Dollar", emoji: "🇸🇬");
static const thb = FiatCurrency(
symbol: "THB",
countryCode: "tha",
fullName: "Thai Baht",
emoji: "🇹🇭",
);
static const twd =
FiatCurrency(symbol: "TWD", countryCode: "twn", fullName: "New Thaiwan Dollar", emoji: "🇹🇼");
static const uah =
FiatCurrency(symbol: "UAH", countryCode: "ukr", fullName: "Ukrainian Hryvnia", emoji: "🇺🇦");
static const usd = FiatCurrency(
symbol: "USD",
countryCode: "usa",
fullName: "United States Dollar",
emoji: "🇺🇸",
);
static const vef = FiatCurrency(
symbol: "VEF",
countryCode: "ven",
fullName: "Venezuelan Bolivar Bolívar",
emoji: "🇻🇪",
);
static const vnd = FiatCurrency(
symbol: "VND",
countryCode: "vnm",
fullName: "Vietnamese Dong đồng",
emoji: "🇻🇳",
);
static const zar = FiatCurrency(
symbol: "ZAR",
countryCode: "saf",
fullName: "South African Rand",
emoji: "🇿🇦",
);
static const tur =
FiatCurrency(symbol: "TRY", countryCode: "tur", fullName: "Turkish Lira", emoji: "🇹🇷");
static const kes =
FiatCurrency(symbol: "KES", countryCode: "ken", fullName: "Kenyan Shillings", emoji: "🇰🇪");
static final _all = {
FiatCurrency.amd.raw: FiatCurrency.amd,
+15 -3
View File
@@ -1,7 +1,15 @@
import 'package:cake_wallet/generated/locales.dart';
import 'package:devicelocale/devicelocale.dart';
import "package:flutter/widgets.dart";
import 'package:intl/intl.dart';
// Flutter's Material delegates match on the language subtag, so "pt_BR" must be split into two
Locale localeFromLanguageCode(String code) {
final parts = code.split("_");
return parts.length == 2 ? Locale(parts[0], parts[1]) : Locale(code);
}
class LanguageService {
static const Map<String, String> supportedLocales = {
'en': 'English',
@@ -85,12 +93,16 @@ class LanguageService {
static Future<String> localeDetection() async {
try {
var locale = await Devicelocale.currentLocale ?? '';
locale = Intl.shortLocale(locale);
final locale = Intl.canonicalizedLocale(await Devicelocale.currentLocale ?? "");
if (list.keys.contains(locale)) {
return locale;
}
final language = Intl.shortLocale(locale);
if (list.keys.contains(language)) {
return language;
}
return LanguageService.defaultLocale;
} catch (_) {
return LanguageService.defaultLocale;
@@ -22,6 +22,8 @@ class ListItemRegularRow extends ListItem {
this.leadingIconSize,
this.badgeIconSize,
this.iconColor,
this.secondaryLabel,
this.subtitleColor,
});
final String? subtitle;
@@ -31,10 +33,12 @@ class ListItemRegularRow extends ListItem {
final String? badgeIconPath;
final String? copyableText;
final VoidCallback? onTap;
final String? secondaryLabel;
final bool showArrow;
final Widget? bottomWidget;
final Widget? trailingWidget;
final bool truncateTrailingText;
final Color? subtitleColor;
final Color? foregroundColor;
final double? trailingIconSize;
final Widget? leadingIconErrorWidget;
+3 -1
View File
@@ -65,6 +65,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import "package:flutter_localizations/flutter_localizations.dart";
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:quick_actions/quick_actions.dart';
@@ -329,6 +330,7 @@ Future<void> initialSetup({
required int initialMigrationVersion,
}) async {
LanguageService.loadLocaleList();
await GlobalMaterialLocalizations.delegate.load(const Locale("en"));
await defaultSettingsMigration(
secureStorage: secureStorage,
version: initialMigrationVersion,
@@ -426,7 +428,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
themeMode: themeMode,
localizationsDelegates: localizationDelegates,
supportedLocales: S.delegate.supportedLocales,
locale: Locale(appStore.settingsStore.languageCode),
locale: localeFromLanguageCode(appStore.settingsStore.languageCode),
onGenerateRoute: (settings) => Router.createRoute(settings),
initialRoute: initialRoute,
scrollBehavior: AppScrollBehavior(),
+6 -1
View File
@@ -18,6 +18,7 @@ import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../view_model/dashboard/dashboard_view_model.dart';
class NewDashboard extends StatefulWidget {
@@ -54,7 +55,11 @@ class _NewDashboardState extends State<NewDashboard> {
});
});
Future.delayed(Duration(milliseconds: 300)).then((_) => _showChangelog(context));
Future.delayed(Duration(milliseconds: 300)).then((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showChangelog(context);
});
});
_showVulnerableSeedsPopup(context);
}
+2 -8
View File
@@ -1,7 +1,6 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
@@ -24,7 +23,6 @@ import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class AccountCustomizerListItem {
@@ -38,13 +36,9 @@ class AccountCustomizerListItem {
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
{super.key,
required this.accountListViewModel,
required this.accountEditOrCreateViewModel,
required this.dashboardViewModel});
{super.key, required this.accountListViewModel, required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
@@ -62,7 +56,7 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
final activeId = monero!.getCurrentAccount(widget.dashboardViewModel.wallet).id;
final activeId = widget.accountListViewModel.selected.id;
for (int i = 0; i < _items.length - 1; i++) {
if (_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
@@ -0,0 +1,537 @@
import "dart:async";
import "package:cake_wallet/buy/payment_method.dart";
import "package:cake_wallet/buy/sell_buy_states.dart";
import "package:cake_wallet/entities/fiat_currency.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
import "package:cake_wallet/new-ui/widgets/floating_amount_input.dart";
import "package:cake_wallet/new-ui/widgets/money/currency_symbol_text.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/new-ui/widgets/token_chain_display.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/themes/core/theme_extension.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
class NewBuySellAmountPage extends StatefulWidget {
const NewBuySellAmountPage({required this.buySellViewModel, super.key});
final BuySellViewModel buySellViewModel;
@override
State<NewBuySellAmountPage> createState() => _NewBuySellAmountPageState();
}
class _NewBuySellAmountPageState extends State<NewBuySellAmountPage> {
bool _customAmountMode = false;
bool _isLoadingPaymentMethods = false;
final customInputController = TextEditingController();
final customInputFocusNode = FocusNode();
@override
void dispose() {
customInputController.dispose();
customInputFocusNode.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
// this is a hack for the "up to" display to show the actual upper limit.
// limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected.
// it'll be refactored soon anyway, i guess.
widget.buySellViewModel.changeFiatAmount(amount: widget.buySellViewModel.defaultAmounts[3]);
widget.buySellViewModel.calculateBestRate();
when(
(_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded,
() => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods
.firstWhere((item) => item.paymentMethodType == PaymentType.all),
);
}
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
Observer(
builder: (_) => ModalTopBar(
title: _pageTitle,
bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null
? null
: "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}",
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
),
),
Expanded(
child: Observer(
builder: (_) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _customAmountMode
? BuySellCustomAmountInput(
fiatCurrency: widget.buySellViewModel.fiatCurrency,
cryptoCurrency: widget.buySellViewModel.cryptoCurrency,
cryptoAmount: widget.buySellViewModel.cryptoAmount,
isLoading: _isLoadingPaymentMethods,
hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies,
onCurrencySelectorPressed: () => selectCryptoCurrency(context),
controller: customInputController,
focusNode: customInputFocusNode,
onContinuePressed: () {
navigateToProviders(context);
},
onChanged: (amount) {
if (widget.buySellViewModel.mode == BuySellPageMode.sell) {
widget.buySellViewModel.setCryptoAmountFromFiat(amount);
} else {
widget.buySellViewModel.changeFiatAmount(amount: amount);
}
},
)
: BuySellDefaultAmountSelector(
key: const ValueKey(0),
defaultAmounts: widget.buySellViewModel.defaultAmounts,
fiatCurrency: widget.buySellViewModel.fiatCurrency,
currentAmount: widget.buySellViewModel.fiatAmount,
hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies,
onCurrencySelectorPressed: () => selectCryptoCurrency(context),
onFiatSelectorPressed: () => selectFiatCurrency(context),
cryptoCurrency: widget.buySellViewModel.cryptoCurrency,
isLoading: _isLoadingPaymentMethods,
mode: widget.buySellViewModel.mode,
onSelected: (amount) async {
if (amount == null) {
// this resets the rate and prevents showing 0 usd = 0.something btc
await widget.buySellViewModel.changeFiatAmount(amount: "");
setState(() {
_customAmountMode = true;
});
customInputFocusNode.requestFocus();
} else {
if (widget.buySellViewModel.mode == BuySellPageMode.sell) {
await widget.buySellViewModel.setCryptoAmountFromFiat(amount);
} else {
await widget.buySellViewModel.changeFiatAmount(amount: amount);
}
if (context.mounted) {
await navigateToProviders(context);
}
}
},
),
),
),
),
],
),
),
);
void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show(
context: context,
args: CurrencyPickerArgs(
items: widget.buySellViewModel.activeWalletCurrencies.toList(),
onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item),
symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol,
),
);
void selectFiatCurrency(BuildContext context) => FiatCurrencyPickerSheet.show(
context: context,
selected: widget.buySellViewModel.fiatCurrency,
onSelected: (item) => widget.buySellViewModel.changeFiatCurrency(currency: item),
);
String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy
? S.current.buy
: S.current.sell +
((widget.buySellViewModel.cryptoCurrencies.length == 1)
? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}"
: "");
Future<void> navigateToProviders(BuildContext context) async {
if (_isLoadingPaymentMethods) {
return;
}
try {
setState(() {
_isLoadingPaymentMethods = true;
});
await asyncWhen(
(_) => [PaymentMethodLoaded, PaymentMethodFailed]
.contains(widget.buySellViewModel.paymentMethodState.runtimeType),
);
if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) {
if (context.mounted) {
await showPopUp(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).failed_to_load_payment_methods,
alertContent: S.of(context).please_try_again_later,
buttonText: "OK",
buttonAction: Navigator.of(context).pop,
),
);
}
return;
}
widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods
.firstWhere((item) => item.paymentMethodType == PaymentType.all);
// unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie
unawaited(widget.buySellViewModel.calculateBestRate());
final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel);
if (context.mounted) {
unawaited(
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => Material(color: Colors.transparent, child: page),
),
),
);
}
} finally {
setState(() {
_isLoadingPaymentMethods = false;
});
}
}
}
class BuySellCustomAmountInput extends StatelessWidget {
const BuySellCustomAmountInput({
required this.fiatCurrency,
required this.cryptoCurrency,
required this.cryptoAmount,
required this.controller,
required this.onContinuePressed,
required this.isLoading,
required this.onChanged,
required this.focusNode,
required this.hasCurrencySelector,
required this.onCurrencySelectorPressed,
super.key,
});
final FiatCurrency fiatCurrency;
final CryptoCurrency cryptoCurrency;
final String cryptoAmount;
final TextEditingController controller;
final FocusNode focusNode;
final bool isLoading;
final bool hasCurrencySelector;
final VoidCallback onCurrencySelectorPressed;
final VoidCallback onContinuePressed;
final Function(String) onChanged;
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox.shrink(),
Column(
spacing: 8,
children: [
FloatingAmountInput(
currency: fiatCurrency,
focusNode: focusNode,
controller: controller,
onChanged: onChanged,
),
Opacity(
opacity: cryptoAmount.isEmpty ? 0 : 1,
child: Text(
"≈ ${cryptoAmount} ${cryptoCurrency.symbol}",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
Padding(
padding: const EdgeInsets.all(18),
child: NewPrimaryButton(
disabled: controller.text.isEmpty,
onPressed: onContinuePressed,
isLoading: isLoading,
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
);
}
class BuySellDefaultAmountSelector extends StatelessWidget {
const BuySellDefaultAmountSelector({
required this.onFiatSelectorPressed,
required this.defaultAmounts,
required this.fiatCurrency,
required this.mode,
required this.onSelected,
required this.isLoading,
required this.hasCurrencySelector,
required this.onCurrencySelectorPressed,
required this.cryptoCurrency,
super.key,
this.currentAmount,
});
final List<String> defaultAmounts;
final String? currentAmount;
final bool isLoading;
final bool hasCurrencySelector;
final VoidCallback onCurrencySelectorPressed;
final VoidCallback onFiatSelectorPressed;
final CryptoCurrency cryptoCurrency;
final FiatCurrency fiatCurrency;
final BuySellPageMode mode;
final Function(String?) onSelected;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 24,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasCurrencySelector)
BuySellCurrencyPickerPill(
title: mode == BuySellPageMode.sell ? S.of(context).sell : S.of(context).buy,
curr: cryptoCurrency,
onTap: onCurrencySelectorPressed,
),
BuySellCurrencyPickerPill(
title: mode == BuySellPageMode.sell
? S.of(context).receive
: S.of(context).purchase_with,
curr: fiatCurrency,
onTap: onFiatSelectorPressed,
),
Text(
mode == BuySellPageMode.sell
? S.of(context).choose_amount_to_sell
: S.of(context).choose_amount_to_buy,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// +1 for "custom" option
itemCount: defaultAmounts.length + 1,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 16,
mainAxisExtent: 105,
),
itemBuilder: (context, index) {
final String? item = index == defaultAmounts.length ? null : defaultAmounts[index];
return BuySellAmountPill(
isLoading: isLoading && item == currentAmount,
amount: item == null ? null : Money.parse(item, fiatCurrency),
onTap: () => onSelected(item),
);
},
),
],
),
);
}
class BuySellAmountPill extends StatelessWidget {
const BuySellAmountPill({required this.onTap, required this.isLoading, super.key, this.amount});
final Money? amount;
final VoidCallback onTap;
final bool isLoading;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(9999999999),
child: InkWell(
borderRadius: BorderRadius.circular(9999999999),
onTap: onTap,
child: isLoading
? const CupertinoActivityIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (amount != null)
MoneyText(
amount!,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
showSymbol: false,
),
Text(
amount?.currency.symbol ?? S.of(context).custom,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: amount == null
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
if (amount == null)
Text(
S.of(context).enter_amount,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
class BuySellCurrencyPickerPill extends StatelessWidget {
const BuySellCurrencyPickerPill(
{required this.title, required this.curr, required this.onTap, super.key});
final String title;
final Currency curr;
final VoidCallback onTap;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
Text(
title,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
GestureDetector(
onTap: onTap,
child: Container(
width: MediaQuery.sizeOf(context).width,
height: 62,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 12,
children: [
if (curr is CryptoCurrency)
TokenChainDisplay(size: 30, asset: curr as CryptoCurrency)
else if (curr is FiatCurrency)
SizedBox(
width: 30,
child: Text(
(curr as FiatCurrency).emoji,
style: const TextStyle(fontSize: 24),
)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 2,
children: [
Text(
curr.fullName ?? curr.symbol,
),
CurrencySymbolText(
curr,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 8,
height: 8,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
],
),
),
),
),
],
);
}
@@ -0,0 +1,144 @@
import "package:cake_wallet/buy/payment_method.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class BuySellConfirmationPage extends StatelessWidget {
const BuySellConfirmationPage({required this.buySellViewModel, super.key});
final BuySellViewModel buySellViewModel;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: _pageTitle,
leadingIcon: const Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
leadingSemanticLabel: S.of(context).seed_alert_back,
),
Expanded(
child: Observer(
builder: (_) => Column(
spacing: 24,
children: [
Column(
spacing: 4,
children: [
Text(
"${buySellViewModel.fiatAmount} ${buySellViewModel.fiatCurrency}",
style: const TextStyle(fontSize: 32),
),
if (buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!) !=
null)
Text(
"≈ ${buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: NewListSections(
sections: {
"": [
ListItemRegularRow(
keyValue: "provider",
label: S.of(context).provider,
trailingWidget: Row(
spacing: 8,
children: [
CakeImageWidget(
imageUrl: Theme.of(context).brightness == Brightness.light
? buySellViewModel.selectedQuote!.lightIconPath
: buySellViewModel.selectedQuote!.darkIconPath,
width: 24,
height: 24,
),
Text(
buySellViewModel.selectedQuote!.rampName ??
buySellViewModel.selectedQuote!.provider.title,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
const SizedBox.shrink(),
],
),
),
if (buySellViewModel.selectedQuote!.paymentType.title?.isNotEmpty ??
false)
ListItemRegularRow(
showArrow: false,
keyValue: "payment method",
label: S.of(context).payment_method,
trailingText: buySellViewModel.selectedQuote!.paymentType.title,
),
ListItemRegularRow(
showArrow: false,
keyValue: "rate",
label: S.of(context).rate,
trailingText: buySellViewModel.selectedQuote!.topLeftSubTitle,
),
],
},
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(18),
child: NewPrimaryButton(
onPressed: () => confirm(context),
text: S.of(context).proceed,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
),
);
String get _pageTitle =>
"${buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell} ${buySellViewModel.cryptoCurrency.fullName ?? ""}";
void confirm(BuildContext context) {
final page = BuySellRedirectingPage(buySellViewModel: buySellViewModel);
Navigator.of(context, rootNavigator: true).pop();
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => Material(
child: page,
),
),
);
}
}
@@ -0,0 +1,81 @@
import "dart:async";
import "package:cake_wallet/buy/payment_method.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class BuySellPaymentMethodPage extends StatelessWidget {
const BuySellPaymentMethodPage({required this.buySellViewModel, super.key});
final BuySellViewModel buySellViewModel;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: S.of(context).payment_method,
leadingIcon: const Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
leadingSemanticLabel: S.of(context).seed_alert_back,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Observer(
builder: (_) => NewListSections(
sections: {
"": buySellViewModel.paymentMethods
.map(
(item) => ListItemRegularRow(
keyValue: item.title,
label: item.title,
showArrow: false,
iconPath: Theme.of(context).brightness == Brightness.light
? item.lightIconPath
: item.darkIconPath,
iconColor: item.paymentMethodType.isMonochromeIcon
? Theme.of(context).colorScheme.onSurfaceVariant
: null,
trailingWidget: buySellViewModel.selectedPaymentMethod == item
? Icon(
Icons.check,
color: Theme.of(context).colorScheme.primary,
size: 16,
)
: null,
onTap: () {
buySellViewModel.changeOption(item);
unawaited(buySellViewModel.calculateBestRate());
Navigator.of(context).pop();
},
),
)
.toList(),
},
),
),
),
),
],
),
),
);
}
@@ -0,0 +1,197 @@
import "package:cake_wallet/buy/buy_quote.dart";
import "package:cake_wallet/buy/sell_buy_states.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_dropdown.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_payment_method_page.dart";
import "package:cake_wallet/new-ui/widgets/money/money_text.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class BuySellProviderPage extends StatefulWidget {
const BuySellProviderPage({required this.buySellViewModel, super.key});
final BuySellViewModel buySellViewModel;
@override
State<BuySellProviderPage> createState() => _BuySellProviderPageState();
}
class _BuySellProviderPageState extends State<BuySellProviderPage> {
bool _allProvidersExpanded = false;
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: _pageTitle,
leadingIcon: const Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
leadingSemanticLabel: S.of(context).seed_alert_back,
),
Expanded(
child: Observer(
builder: (_) {
if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 24,
children: [
const Icon(Icons.warning_amber_outlined, size: 48),
Column(
spacing: 10,
children: [
Text(
S.of(context).could_not_load_quotes,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
(widget.buySellViewModel.buySellQuotState as BuySellQuotFailed)
.errorMessage ??
S.of(context).please_try_again_later,
),
],
),
],
);
}
if (widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
const CupertinoActivityIndicator(),
Text(
S.of(context).loading_rates,
style:
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
);
}
return SingleChildScrollView(
controller: ModalScrollController.of(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: NewListSections(
showHeader: true,
sections: {
"": [
ListItemRegularRow(
keyValue: "payment method",
label: S.of(context).payment_method,
showArrow: true,
onTap: () {
final page = BuySellPaymentMethodPage(
buySellViewModel: widget.buySellViewModel,
);
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => Material(
color: Colors.transparent,
child: page,
),
),
);
},
trailingText: widget.buySellViewModel.selectedPaymentMethod?.title,
),
],
S.of(context).available_providers: [
...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem),
if (widget.buySellViewModel.sortedQuotes.isNotEmpty)
ListItemDropdown(
keyValue: "more options",
label: S.of(context).more_options,
onTap: () {
setState(() {
_allProvidersExpanded = !_allProvidersExpanded;
});
},
),
if (_allProvidersExpanded)
...widget.buySellViewModel.sortedQuotes.map(quoteListItem),
],
},
),
),
);
},
),
),
],
),
),
);
String get _pageTitle =>
"${widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell} ${widget.buySellViewModel.cryptoCurrency.fullName ?? ""}";
ListItem quoteListItem(Quote quote) {
final fiatAmount = widget.buySellViewModel.fiatAmountForQuote(quote);
return ListItemRegularRow(
keyValue: quote.provider.title,
label: quote.rampName ?? quote.provider.title,
secondaryLabel: quote.rampName != null ? quote.provider.title : null,
subtitle: quote.badges.isEmpty ? null : quote.badges.join(" - "),
subtitleColor: Theme.of(context).colorScheme.primary,
iconPath: Theme.of(context).brightness == Brightness.light
? quote.lightIconPath
: quote.darkIconPath,
onTap: () {
widget.buySellViewModel.changeOption(quote);
navigateToConfirmation(context);
},
leadingIconSize: 32,
trailingWidget: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
MoneyText.optional(widget.buySellViewModel.amountForQuote(quote)),
if (fiatAmount != null)
Text(
"= ${fiatAmount.toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}",
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
);
}
void navigateToConfirmation(BuildContext context) {
final page = BuySellConfirmationPage(buySellViewModel: widget.buySellViewModel);
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => Material(
color: Colors.transparent,
child: page,
),
),
);
}
}
@@ -0,0 +1,129 @@
import "package:cake_wallet/buy/sell_buy_states.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class BuySellRedirectingPage extends StatefulWidget {
const BuySellRedirectingPage({required this.buySellViewModel, super.key});
final BuySellViewModel buySellViewModel;
@override
State<BuySellRedirectingPage> createState() => _BuySellRedirectingPageState();
}
class _BuySellRedirectingPageState extends State<BuySellRedirectingPage> {
bool _hasRedirected = false;
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 2)).then((_) {
WidgetsBinding.instance
.addPostFrameCallback((_) => widget.buySellViewModel.launchTrade(context));
setState(() {
_hasRedirected = true;
});
});
}
@override
Widget build(BuildContext context) => PopScope(
canPop: false,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Observer(
builder: (_) {
final showExitButton =
_hasRedirected || widget.buySellViewModel.buySellQuotState is BuySellQuotFailed;
return SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox.shrink(),
Observer(
builder: (_) {
if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 24,
children: [
const Icon(Icons.warning_amber_outlined, size: 48),
Column(
spacing: 10,
children: [
Text(
S.of(context).could_not_proceed_with_trade,
style:
const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
(widget.buySellViewModel.buySellQuotState as BuySellQuotFailed)
.errorMessage ??
S.of(context).please_try_again_later,
),
],
),
],
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 24,
children: [
CakeImageWidget(
imageUrl: Theme.of(context).brightness == Brightness.light
? widget.buySellViewModel.selectedQuote!.lightIconPath
: widget.buySellViewModel.selectedQuote!.darkIconPath,
width: 64,
height: 64,
),
Column(
spacing: 10,
children: [
Text(
"${S.of(context).connecting_you_to} ${widget.buySellViewModel.selectedQuote!.provider.title}...",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
"${widget.buySellViewModel.fiatAmount} ${widget.buySellViewModel.fiatCurrency} → ${widget.buySellViewModel.amountForQuote(widget.buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}",
),
],
),
],
);
},
),
showExitButton
? Padding(
padding: const EdgeInsets.all(18),
child: NewPrimaryButton(
onPressed: Navigator.of(context).pop,
text: S.of(context).close,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
)
: const SizedBox.shrink(),
],
),
);
},
),
),
);
}
+81 -70
View File
@@ -17,11 +17,9 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/seed_backup_reminder_card.dart";
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -137,6 +135,7 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
spacing: 24.0,
children: [
TopBar(
key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
@@ -144,8 +143,8 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
_lightningMode = !_lightningMode;
});
},
onSettingsButtonPress: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
onSettingsButtonPress: () async {
await CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
@@ -155,74 +154,81 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
authService: getIt.get<AuthService>(),
))),
);
if (!mounted) {
return;
}
_setAccountViewModel();
await widget.dashboardViewModel.loadCardDesigns();
if (mounted) {
setState(() {});
}
},
),
Observer(
builder: (_) => WalletInfoBar(
lightningMode: _lightningMode,
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
name: widget.dashboardViewModel.wallet.name,
hasCustomize: accountListViewModel != null,
onCustomizeButtonTap: openAccountCustomizer),
),
Column(
spacing: 20,
children: [
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
),
),
Observer(builder: (_) {
return AnimatedSize(
duration: Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
? Column(
children: [
SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant),
)
],
)
: SizedBox(width: double.infinity),
);
}),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
],
),
Observer(
builder: (_) {
return Column(
Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
),
),
MwebAd(
Observer(builder: (_) {
return AnimatedSize(
duration: Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
? Column(
children: [
SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
],
)
: SizedBox(width: double.infinity),
);
}),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
SeedBackupReminderCard(
dashboardViewModel: widget.dashboardViewModel,
onTap: openSeedBackupReminder,
),
],
);
},
),
Observer(
builder: (_) {
return Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
),
MwebAd(
dashboardViewModel: widget.dashboardViewModel,
),
SeedBackupReminderCard(
dashboardViewModel: widget.dashboardViewModel,
onTap: openSeedBackupReminder,
),
],
);
},
),
],
),
],
),
@@ -271,7 +277,12 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
);
}
void openAccountCustomizer() async {
Future<void> openAccountCustomizer() async {
final accountList = accountListViewModel;
if (accountList == null) {
return;
}
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
@@ -280,15 +291,15 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: AccountCustomizer(
accountListViewModel: accountListViewModel!,
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>(),
dashboardViewModel: widget.dashboardViewModel,
)),
child: AccountCustomizer(
accountListViewModel: accountList,
dashboardViewModel: widget.dashboardViewModel,
),
),
);
},
);
widget.dashboardViewModel.loadCardDesigns();
await widget.dashboardViewModel.loadCardDesigns();
}
void openCardCustomizer() async {
+16 -1
View File
@@ -25,6 +25,8 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
bool _hasAccounts(DashboardViewModel vm) => vm.balanceViewModel.hasAccounts;
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -38,12 +40,14 @@ class SettingsListItem {
this.use2fa = _falseFunc,
this.condition = _trueFunc,
this.routeArgs,
this.routeArgsBuilder,
});
final String iconPath;
final String title;
final String route;
final Object? routeArgs;
final Object? Function(DashboardViewModel)? routeArgsBuilder;
final bool requireAuth;
final bool Function(DashboardViewModel) use2fa;
final bool Function(DashboardViewModel) condition;
@@ -58,6 +62,13 @@ class SettingsSectionData {
static SettingsSectionData walletSettings =
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
SettingsListItem(
"assets/new-ui/settings_row_icons/accounts.svg",
S.current.accounts,
Routes.accountCustomizer,
condition: _hasAccounts,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
SettingsListItem(
@@ -145,7 +156,11 @@ class SettingsMainPage extends StatelessWidget {
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route);
} else {
Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs);
Navigator.of(context).pushNamed(
item.route,
arguments:
item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs,
);
}
}
})
@@ -0,0 +1,153 @@
import "package:cake_wallet/di.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/themes/core/theme_extension.dart";
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import "package:flutter/material.dart";
class BuySellSelectorModal extends StatelessWidget {
const BuySellSelectorModal({super.key});
@override
Widget build(BuildContext context) => Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surfaceDim,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
top: false,
child: Column(
spacing: 24,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox.shrink(),
Text(
S.of(context).buy_or_sell,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
S.of(context).buy_or_sell_desc,
style:
TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 12,
children: [
BuySellSelectorModalButton(
title: S.of(context).buy_crypto,
description: S.of(context).buy_crypto_desc,
iconPath: "assets/new-ui/plus.svg",
onTap: () => openBuySellPage(context, BuySellPageMode.buy),
),
BuySellSelectorModalButton(
title: S.of(context).sell_crypto,
description: S.of(context).sell_crypto_desc,
iconPath: "assets/new-ui/sell.svg",
onTap: () => openBuySellPage(context, BuySellPageMode.sell),
),
],
),
),
const SizedBox.shrink(),
],
),
),
);
void openBuySellPage(BuildContext context, BuySellPageMode mode) {
Navigator.of(context).pop();
final page = getIt.get<NewBuySellAmountPage>(param1: mode);
showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
context: navigatorKey.currentContext!,
builder: (modalContext) => ModalNavigator(
rootPage: page,
parentContext: modalContext,
),
);
}
}
class BuySellSelectorModalButton extends StatelessWidget {
const BuySellSelectorModalButton({
required this.title,
required this.description,
required this.iconPath,
required this.onTap,
super.key,
});
final String title;
final String description;
final String iconPath;
final VoidCallback onTap;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
spacing: 20,
children: [
CakeImageWidget(
imageUrl: iconPath,
width: 55,
height: 55,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
),
Text(
description,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
),
);
}
@@ -26,7 +26,12 @@ class AssetsTopBar extends StatelessWidget {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
padding: const EdgeInsets.only(
top: 20,
bottom: 0,
left: 12,
right: 18,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -1,4 +1,5 @@
import 'package:cake_wallet/core/csv_export_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_filters_page.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_section.dart';
@@ -33,7 +34,7 @@ class HistoryModal extends StatelessWidget {
),
trailingSemanticLabel: S.of(context).export_csv,
onTrailingPressed: () =>
CsvExportService().exportToCsv(dashboardViewModel.items, context),
getIt.get<CsvExportService>().exportToCsv(dashboardViewModel.items, context),
),
Expanded(
child: Stack(
@@ -20,7 +20,7 @@ class HistoryTopBar extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
@@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
@@ -204,7 +205,10 @@ class _CardsViewState extends State<CardsView> {
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
onTap: () {
showModalBottomSheet(
context: context, builder: (context) => BuySellSelectorModal());
},
)
]
: [];
@@ -30,64 +30,59 @@ class LightningSwitcher extends StatelessWidget {
shape:
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
width: 63,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
padding: const EdgeInsets.all(4.5),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
margin: EdgeInsets.only(left: lightningMode ? 27 : 0),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
width: 27,
height: 27,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
Row(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: "assets/new-ui/switcher-bitcoin.svg",
key: ValueKey(lightningMode),
width: 27,
height: 27,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-lightning.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: "assets/new-ui/switcher-lightning.svg",
key: ValueKey(lightningMode),
width: 27,
height: 27,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
],
),
),
],
),
],
),
@@ -3,6 +3,7 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import "package:cake_wallet/themes/core/theme_extension.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import "package:flutter/cupertino.dart";
@@ -11,11 +12,11 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SyncBar extends StatelessWidget {
SyncBar({
super.key,
const SyncBar({
required this.dashboardViewModel,
required this.isSyncHeavy,
required this.showSyncedMessage,
super.key,
});
final DashboardViewModel dashboardViewModel;
@@ -46,106 +47,126 @@ class SyncBar extends StatelessWidget {
@override
Widget build(BuildContext context) => Observer(
builder: (_) {
final status = dashboardViewModel.status;
final Widget? icon = _getIcon(context, status.runtimeType);
builder: (_) {
final status = dashboardViewModel.status;
if (!_showFullBar) {
return _buildCompactBar(context);
}
return Expanded(
child: Stack(
alignment: Alignment.centerLeft,
children: [
if (!_showFullBar()) _buildCompactBar(context),
if (_showFullBar())
// A single node: the localized status text (plus any active
// Tor/MWEB/Silent Payments badge) is the label, and the hint says
// where tapping leads. Everything inside is redundant with it.
Semantics(
button: true,
label: _statusSemanticsLabel(context, status),
hint: S.of(context).manage_nodes,
onTap: () => _openNodeManagement(context),
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => _openNodeManagement(context),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(status,
dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
final Widget? icon = _getIcon(context, status.runtimeType);
final statusTitle = _statusTitle(context, status);
final silentPaymentsProgress = _silentPaymentsProgress(status);
final barHeight = silentPaymentsProgress == null ? 36.0 : 40.0;
// A single node: the localized status text (plus any active
// Tor/MWEB/Silent Payments badge) is the label, and the hint says
// where tapping leads. Everything inside is redundant with it.
return Semantics(
button: true,
label: _statusSemanticsLabel(context, status, statusTitle),
value: silentPaymentsProgress,
hint: S.of(context).manage_nodes,
onTap: () => _openNodeManagement(context),
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => _openNodeManagement(context),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: OverflowBox(
key: ValueKey(status.runtimeType),
alignment: Alignment.center,
minHeight: barHeight,
maxHeight: barHeight,
child: Container(
height: barHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
if (silentPaymentsProgress != null) ...[
Text(
silentPaymentsProgress,
style: TextStyle(
fontSize: 12,
color: context.customColors.warningOutlineColor,
),
],
),
Text(
"·",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
Flexible(
child: Text(
statusTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _getTextStyle(context, status.runtimeType),
),
),
),
],
),
),
),
),
],
),
);
},
);
),
),
);
},
);
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
void _openNodeManagement(BuildContext context) {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
),
),
);
}
/// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
/// the whole row needs a text equivalent.
Widget _buildCompactBar(BuildContext context) {
final row = Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
const CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) const CupertinoActivityIndicator(radius: 8),
if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
if (_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
);
final label = _joinLabels([
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (_showDot()) S.of(context).synchronizing,
if (_showLightSyncCheck()) S.of(context).sync_synced,
]);
if (label.isEmpty) return row;
if (label.isEmpty) {
return row;
}
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
@@ -153,11 +174,17 @@ class SyncBar extends StatelessWidget {
bool get _isShowingSyncedMessage =>
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
String _statusTitle(BuildContext context, SyncStatus status) {
final title = syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode);
return title.isNotEmpty ? title : S.of(context).synchronizing;
}
String _statusSemanticsLabel(BuildContext context, SyncStatus status, String statusTitle) {
final isFailure = failStatuses.contains(status.runtimeType);
return _joinLabels([
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
statusTitle,
if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
@@ -166,6 +193,16 @@ class SyncBar extends StatelessWidget {
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
String? _silentPaymentsProgress(SyncStatus status) {
if (!dashboardViewModel.hasSilentPayments ||
!dashboardViewModel.silentPaymentsScanningActive ||
status is! SyncingSyncStatus) {
return null;
}
return "${(status.progress() * 100).toInt()}%";
}
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
@@ -192,14 +229,12 @@ class SyncBar extends StatelessWidget {
color = Theme.of(context).colorScheme.onSurfaceVariant;
}
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: color);
return TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: color);
}
Widget? _getIcon(BuildContext context, Type status) {
if(status == SyncedSyncStatus) {
return Icon(Icons.check, color: syncedColor, size: 12);
if (status == SyncedSyncStatus) {
return const Icon(Icons.check, color: syncedColor, size: 12);
}
if (status == LostConnectionSyncStatus) {
@@ -219,22 +254,35 @@ class SyncBar extends StatelessWidget {
final List<Widget> children = [];
if (dashboardViewModel.isTorEnabled) {
children.add(CakeImageWidget(
children.add(
const CakeImageWidget(
imageUrl: "assets/new-ui/tor_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn)));
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn),
),
);
}
if (dashboardViewModel.hasMweb) {
children.add(CakeImageWidget(
imageUrl: "assets/new-ui/mweb_sync.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
));
children.add(
CakeImageWidget(
imageUrl: "assets/new-ui/mweb_sync.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
);
}
if (dashboardViewModel.hasSilentPayments) {
children.add(CakeImageWidget(
imageUrl: "assets/new-ui/silent_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn),
));
children.add(
CakeImageWidget(
imageUrl: "assets/new-ui/silent_sync.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(context.customColors.warningOutlineColor, BlendMode.srcIn),
),
);
}
if (children.isEmpty) {
return null;
}
return Row(
@@ -243,15 +291,16 @@ class SyncBar extends StatelessWidget {
);
}
bool _showFullBar() {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus)
bool get _showFullBar {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) {
return isSyncHeavy && _isShowingSyncedMessage;
}
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() =>
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
bool _showLightSyncCheck() =>
!isSyncHeavy && _isShowingSyncedMessage;
bool _showLightSyncCheck() => !isSyncHeavy && _isShowingSyncedMessage;
}
@@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart";
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
@@ -15,11 +16,11 @@ import 'package:mobx/mobx.dart';
class TopBar extends StatefulWidget {
const TopBar({
super.key,
required this.lightningMode,
required this.onLightningSwitchPress,
required this.dashboardViewModel,
required this.onSettingsButtonPress,
super.key,
});
final bool lightningMode;
@@ -36,25 +37,59 @@ class _TopBarState extends State<TopBar> {
bool showSyncedMessage = false;
Timer? syncedMessageTimer;
late final ReactionDisposer? _statusReactionDisposer;
ReactionDisposer? _statusReactionDisposer;
bool get replacesWalletName {
final status = widget.dashboardViewModel.status.runtimeType;
if (status == SyncedSyncStatus) {
return showSyncedMessage;
}
return widget.dashboardViewModel.isSyncHeavy ||
SyncBar.progressStatuses.contains(status) ||
SyncBar.failStatuses.contains(status);
}
@override
void initState() {
super.initState();
_bindStatusReaction();
}
@override
void didUpdateWidget(covariant TopBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (identical(oldWidget.dashboardViewModel, widget.dashboardViewModel)) {
return;
}
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
showSyncedMessage = false;
_statusReactionDisposer?.call();
_bindStatusReaction();
}
void _bindStatusReaction() {
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
if (status == SyncedSyncStatus) {
if(mounted) {
if (mounted) {
setState(() => showSyncedMessage = true);
}
syncedMessageTimer =
Timer(syncedMessageDuration, () => setState(() => showSyncedMessage = false));
syncedMessageTimer = Timer(syncedMessageDuration, () {
syncedMessageTimer = null;
if (mounted) {
setState(() => showSyncedMessage = false);
}
});
} else {
if(mounted) {
if (mounted) {
setState(() => showSyncedMessage = false);
}
}
@@ -65,51 +100,90 @@ class _TopBarState extends State<TopBar> {
@override
void dispose() {
syncedMessageTimer?.cancel();
_statusReactionDisposer?.reaction.dispose();
syncedMessageTimer = null;
_statusReactionDisposer?.call();
super.dispose();
}
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
(widget.dashboardViewModel.hasLightning)
? LightningSwitcher(
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
padding: EdgeInsets.only(left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
widget.dashboardViewModel.hasLightning
? LightningSwitcher(
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
Expanded(
child: SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
reverseDuration: Duration.zero,
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: replacesWalletName
? SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
)
: Row(
children: [
Expanded(
child: WalletInfoBar(
name: widget.dashboardViewModel.wallet.name,
hardwareWalletType:
widget.dashboardViewModel.wallet.hardwareWalletType,
),
),
if (widget.dashboardViewModel.isTorEnabled) ...[
const SizedBox(width: 6),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
],
],
),
),
),
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
),
),
),
);
);
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) return 24;
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) {
return 24;
}
return 0;
}
+42 -77
View File
@@ -1,102 +1,67 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
class WalletInfoBar extends StatelessWidget {
const WalletInfoBar(
{super.key,
required this.lightningMode,
required this.name,
required this.hardwareWalletType,
required this.onCustomizeButtonTap,
required this.hasCustomize});
const WalletInfoBar({
required this.name,
required this.hardwareWalletType,
super.key,
});
final bool lightningMode;
final String name;
final HardwareWalletType? hardwareWalletType;
final bool hasCustomize;
final VoidCallback onCustomizeButtonTap;
void _openAccountCustomizer() {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
}
@override
Widget build(BuildContext context) {
// The row, the hardware-wallet glyph and the inner accounts button are one
// control for a screen reader: a single labeled node opening the customizer.
final semanticsLabel =
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
final row = GestureDetector(
onTap: _openAccountCustomizer,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return SizeTransition(
return Semantics(
label: semanticsLabel,
child: ExcludeSemantics(
child: Row(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) => SizeTransition(
axis: Axis.horizontal,
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
padding: const EdgeInsets.only(right: 8),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: ValueKey("hardware_wallet_icon"),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
padding: const EdgeInsets.only(right: 4),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: const ValueKey("hardware_wallet_icon"),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
),
),
),
Text(
name,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
if (hasCustomize) ...[
SizedBox(width: 8),
ModernButton.svg(
size: 24,
onPressed: _openAccountCustomizer,
svgPath: "assets/new-ui/icon-accounts.svg",
semanticLabel: S.of(context).wallet_accounts,
)
]
],
),
Expanded(
child: Text(
name,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
),
],
),
),
);
if (!hasCustomize) {
return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
}
return Semantics(
button: true,
label: semanticsLabel,
hint: S.of(context).wallet_accounts,
onTap: _openAccountCustomizer,
child: ExcludeSemantics(child: row),
);
}
String? get hardwareWalletIcon {
@@ -0,0 +1,105 @@
import "package:cake_wallet/new-ui/widgets/money/currency_symbol_text.dart";
import "package:cw_core/currency.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class FloatingAmountInput extends StatefulWidget {
const FloatingAmountInput({
required this.currency,
required this.controller,
super.key,
this.focusNode,
this.inputFormatters,
this.onChanged,
this.validator,
});
final Currency currency;
final TextEditingController controller;
final FocusNode? focusNode;
final List<TextInputFormatter>? inputFormatters;
final Function(String)? onChanged;
final FormFieldValidator<String>? validator;
@override
State<FloatingAmountInput> createState() => _FloatingAmountInputState();
}
class _FloatingAmountInputState extends State<FloatingAmountInput> {
bool _amountFocused = false;
late FocusNode focusNode = widget.focusNode ?? FocusNode();
@override
void initState() {
super.initState();
focusNode.addListener(() => setState(() => _amountFocused = focusNode.hasFocus));
}
@override
void dispose() {
if (focusNode != widget.focusNode) {
focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) => Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
IntrinsicWidth(
child: TextFormField(
controller: widget.controller,
focusNode: focusNode,
maxLines: 1,
onChanged: widget.onChanged,
autovalidateMode: AutovalidateMode.always,
validator: widget.validator,
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"^\d*[.,]?\d*$"),
),
],
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.zero,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
hintText: _amountFocused || widget.controller.text.isNotEmpty ? null : "0.00",
hintStyle: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 45,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const SizedBox(width: 8),
CurrencySymbolText(
widget.currency,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 45,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
@@ -13,6 +13,7 @@ class ModalTopBar extends StatelessWidget {
this.leadingIcon,
this.trailingIcon,
this.padding,
this.bottomText,
this.leadingWidget,
this.trailingWidget,
this.leadingSemanticLabel,
@@ -27,6 +28,7 @@ class ModalTopBar extends StatelessWidget {
final String title;
final String? subtitle;
final String? bottomText;
final VoidCallback onLeadingPressed;
final VoidCallback onTrailingPressed;
final Widget? leadingIcon;
@@ -50,32 +52,44 @@ class ModalTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasBottomText = bottomText != null && bottomText!.isNotEmpty;
return Padding(
padding: padding ?? EdgeInsets.all(18),
child: Stack(
alignment: Alignment.topCenter,
children: [
Positioned(
top: 6,
top: hasBottomText ? -4 : 6,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Semantics(
Column(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Semantics(
key: ValueKey(title),
header: title.isNotEmpty,
// Android reads the heading from headingLevel since the
header: title.isNotEmpty,
// Android reads the heading from headingLevel since the
// Flutter 3.41 engine; header: alone only covers iOS.
headingLevel: title.isNotEmpty ? 1 : null,
child: Text(
title,
style: Theme.of(context).textTheme.headlineMedium,
style: TextStyle(
fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600),
),
),
),
if (hasBottomText)
Text(
bottomText!,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
),
if (subtitle != null && subtitle!.isNotEmpty)
Text(subtitle!,
style: Theme.of(context)
@@ -214,7 +214,7 @@ class SendTransactionDetails extends StatelessWidget {
sendViewModel.balance, sendViewModel.selectedCryptoCurrency) ??
zero;
return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ?? zero;
return o.cryptoAmountMoney;
}, sendViewModel.selectedCryptoCurrency))
: sendViewModel.amountParsingProxy.asDisplayString(transaction.amount);
@@ -14,6 +14,7 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -176,9 +177,11 @@ class SwapTransactionDetails extends StatelessWidget {
iconPath: exchangeViewModel.depositCurrency.iconPath ?? "",
badgeIconPath: _resolveChainBadgePath(exchangeViewModel.depositCurrency),
trailingText: exchangeViewModel.amountParsingProxy
.asDisplayStringWithSymbol(exchangeViewModel.depositCurrency
.tryParseAmount(exchangeTradeViewModel.trade.amount) ??
Money.zero(exchangeViewModel.depositCurrency)),
.asDisplayStringWithSymbol(
exchangeViewModel.depositCurrency.tryParseAmount(
exchangeTradeViewModel.trade.amount.sanitized(),
) ??
Money.zero(exchangeViewModel.depositCurrency)),
),
if (exchangeTradeViewModel.sendViewModel.pendingTransaction != null)
ListItemRegularRow(
@@ -0,0 +1,50 @@
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/material.dart";
class TokenChainDisplay extends StatelessWidget {
const TokenChainDisplay({required this.size, required this.asset, super.key});
final CryptoCurrency asset;
final double size;
static const double _chainIconSizeFactor = 0.35;
double get _chainIconSize => size * _chainIconSizeFactor;
@override
Widget build(BuildContext context) => SizedBox(
width: size+(_chainIconSize/2),
height: size+(_chainIconSize/2),
child: Stack(
children: [
CakeImageWidget(
imageUrl: asset.iconPath ?? "",
width: size,
),
if ((asset.chainIconPath ?? "").isNotEmpty)
Align(
alignment: Alignment.bottomRight,
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(5),
side: const BorderSide(color: Colors.black),
),
color: Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(2),
child: CakeImageWidget(
imageUrl: asset.chainIconPath,
width: _chainIconSize,
height: _chainIconSize,
colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
),
),
),
],
),
);
}
+12 -2
View File
@@ -4,10 +4,12 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import "package:cake_wallet/new-ui/pages/account_customizer.dart";
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import "package:cake_wallet/new-ui/pages/buy_sell/buy_sell_amount_page.dart";
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
@@ -15,6 +17,7 @@ import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import "package:cake_wallet/new-ui/pages/receive_page.dart";
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import "package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart";
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
@@ -145,6 +148,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
@@ -668,6 +672,13 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<MoneroAccountEditOrCreatePage>(
param1: settings.arguments as AccountListItem?));
case Routes.accountCustomizer:
return handleRouteWithPlatformAwareness(
(_) => getIt.get<AccountCustomizer>(
param1: settings.arguments! as DashboardViewModel,
),
);
case Routes.nanoAccountCreation:
return CupertinoPageRoute<String>(
builder: (_) =>
@@ -717,9 +728,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<OrderDetailsPage>(param1: settings.arguments as Order));
case Routes.buySellPage:
final args = settings.arguments as bool?;
return handleRouteWithPlatformAwareness(
(context) => getIt.get<BuySellPage>(param1: args),
(context) => Material(child: getIt.get<NewBuySellAmountPage>(param1: BuySellPageMode.buy)),
);
case Routes.buyOptionsPage:
+1
View File
@@ -32,6 +32,7 @@ class Routes {
static const login = '/login';
static const splash = '/splash';
static const accountCreation = '/account_new';
static const accountCustomizer = "/account_customizer";
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
+7 -54
View File
@@ -3,6 +3,7 @@ import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart'
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
@@ -410,21 +411,9 @@ class BuySellPage extends BasePage {
currencyButtonColor: Colors.transparent,
addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest,
borderColor: Theme.of(context).colorScheme.outlineVariant,
onPushPasteButton: (context) async {
final domain = cryptoCurrencyKey.currentState!.addressController.text;
final parsed = await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
if (parsed.isNotEmpty) {
buySellViewModel.cryptoCurrencyAddress = parsed;
}
},
onPushAddressBookButton: (context) async {
final domain = cryptoCurrencyKey.currentState!.addressController.text;
final parsed = await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
if (parsed.isNotEmpty) {
buySellViewModel.cryptoCurrencyAddress = parsed;
}
},
fillColor: buySellViewModel.isBuyAction
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
fillColor: buySellViewModel.mode == BuySellPageMode.buy
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
),
@@ -463,7 +452,7 @@ class BuySellPage extends BasePage {
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
fillColor: buySellViewModel.isBuyAction
fillColor: buySellViewModel.mode == BuySellPageMode.buy
? Theme.of(context).colorScheme.surfaceContainerLow
: Theme.of(context).colorScheme.surfaceContainer,
useSatoshis: buySellViewModel.useSatoshi,
@@ -473,50 +462,14 @@ class BuySellPage extends BasePage {
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return MobileExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return MobileExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
return Placeholder();
},
);
}
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return DesktopExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return DesktopExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
return Placeholder();
},
);
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/csv_export_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
@@ -66,7 +67,8 @@ class TransactionsPage extends StatelessWidget {
}),
HeaderRow(
dashboardViewModel: dashboardViewModel,
onExportCsv: () => CsvExportService().exportToCsv(dashboardViewModel.items, context),
onExportCsv: () =>
getIt.get<CsvExportService>().exportToCsv(dashboardViewModel.items, context),
key: ValueKey('transactions_page_header_row_key'),
),
Expanded(
+5 -1
View File
@@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:vector_graphics/vector_graphics.dart';
@@ -130,7 +131,10 @@ class CakeImageWidget extends StatelessWidget {
/// A caller-supplied [loadingWidget] owns its own semantics; the built-in
/// spinner is purely visual and must not become an unnamed focus stop.
Widget _buildLoadingWidget() =>
loadingWidget ?? const ExcludeSemantics(child: Center(child: CircularProgressIndicator()));
loadingWidget ?? ExcludeSemantics(child:SizedBox(
height: height,
width: width,
child: Center(child: CupertinoActivityIndicator())));
Widget _buildErrorWidget(BuildContext context) {
final Widget placeholder = Container(
@@ -29,31 +29,29 @@ class ListItemDropdownWidget extends StatelessWidget {
return ListItemStyleWrapper(
isFirstInSection: isFirstInSection,
isLastInSection: isLastInSection,
onTap: onTap,
builder: (context, textStyle, labelStyle) {
return InkWell(
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: textStyle),
Row(
children: [
if (trailingText != null)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
trailingText!,
style: labelStyle,
),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: textStyle),
Row(
children: [
if (trailingText != null)
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
trailingText!,
style: labelStyle,
),
Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
],
),
Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
],
);
},
);
@@ -23,22 +23,26 @@ class ListItemRegularRowWidget extends StatelessWidget {
this.foregroundColor,
this.trailingIconSize,
this.bottomWidget,
this.subtitleColor,
this.trailingWidget,
this.copyableText,
this.leadingIconErrorWidget,
this.leadingIconSize,
this.badgeIconSize,
this.iconColor});
this.iconColor,
this.secondaryLabel});
final String keyValue;
final String label;
final String? subtitle;
final String? trailingText;
final String? secondaryLabel;
final String? iconPath;
final String? badgeIconPath;
final VoidCallback? onTap;
final bool isFirstInSection;
final bool isLastInSection;
final Color? subtitleColor;
final bool showArrow;
final String? trailingIconPath;
final Widget? bottomWidget;
@@ -143,14 +147,28 @@ class ListItemRegularRowWidget extends StatelessWidget {
color: Theme.of(context).colorScheme.primary),
)
else
Text(label,
style: foregroundColor == null
? textStyle
: textStyle.copyWith(color: foregroundColor)),
Row(
spacing: 4,
children: [
Text(label,
style: foregroundColor == null
? textStyle
: textStyle.copyWith(color: foregroundColor)),
if (secondaryLabel != null)
Text(
secondaryLabel!,
style: textStyle.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
if (subtitle != null)
Text(
subtitle!,
style: labelStyle.copyWith(fontSize: 12),
style: subtitleColor == null
? labelStyle.copyWith(fontSize: 12)
: labelStyle.copyWith(fontSize: 12, color: subtitleColor),
),
],
),
@@ -32,30 +32,35 @@ class ListItemSelectorWidget extends StatelessWidget {
onTap: onTap,
isFirstInSection: isFirstInSection,
isLastInSection: isLastInSection,
builder: (context, textStyle, labelStyle) {
return Row(
builder: (context, textStyle, labelStyle) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(label, style: textStyle)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
spacing: 8,
children: [
Text(
options[selectedIndex],
style: labelStyle,
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Flexible(
child: Text(
options[selectedIndex],
style: labelStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
],
);
});
));
}
}
@@ -97,6 +97,8 @@ class NewListSections extends StatelessWidget {
iconPath: item.iconPath,
badgeIconPath: item.badgeIconPath,
trailingIconPath: item.trailingIconPath,
secondaryLabel: item.secondaryLabel,
subtitleColor: item.subtitleColor,
onTap: tapHandlers[item.keyValue] ?? item.onTap,
isFirstInSection: isFirst,
isLastInSection: isLast,
+14 -3
View File
@@ -1378,8 +1378,8 @@ abstract class SettingsStoreBase with Store {
pinLength = defaultPinLength;
}
final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ??
await LanguageService.localeDetection();
final savedLanguageCode =
_offeredLanguageCode(sharedPreferences) ?? await LanguageService.localeDetection();
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
@@ -1938,7 +1938,12 @@ abstract class SettingsStoreBase with Store {
}
pinCodeLength = pinLength;
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
final restoredLanguageCode = _offeredLanguageCode(sharedPreferences);
if (restoredLanguageCode == null) {
await sharedPreferences.setString(PreferencesKey.currentLanguageCode, languageCode);
} else {
languageCode = restoredLanguageCode;
}
shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
shouldShowDEuroDisclaimer =
@@ -2315,6 +2320,12 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setString(key, serializedData);
}
static String? _offeredLanguageCode(SharedPreferences sharedPreferences) {
final code = sharedPreferences.getString(PreferencesKey.currentLanguageCode);
return LanguageService.list.containsKey(code) ? code : null;
}
static Future<String?> _getDeviceName() async {
String? deviceName = '';
final deviceInfoPlugin = DeviceInfoPlugin();
+2 -1
View File
@@ -118,7 +118,8 @@ class ExceptionHandler {
if (kDebugMode || kProfileMode) {
if (_ignoreError(errorDetails.exception.toString()) ||
_ignoreError(errorDetails.stack.toString())) {
_ignoreError(errorDetails.stack.toString()) ||
_flutterErrorIgnore(errorDetails)) {
printV("(BELOW ERROR IS IGNORED AND WILL NOT TRIGGER POPUP IN PROD)");
}
FlutterError.presentError(errorDetails);
+186 -60
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
@@ -11,21 +12,26 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart';
part 'buy_sell_view_model.g.dart';
enum BuySellPageMode { buy, sell }
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
) : _cryptoAmount = '',
BuySellViewModelBase(AppStore appStore, {required this.mode, required this.fiatConversionStore})
: _cryptoAmount = '',
fiatAmount = '',
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
@@ -56,6 +62,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
late Timer bestRateSync;
final FiatConversionStore fiatConversionStore;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes();
return providerTypes
@@ -81,9 +89,27 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
final formattedFiatAmount = double.tryParse(fiatAmount);
final formattedCryptoAmount = double.tryParse(_cryptoAmount);
return isBuyAction
return mode == BuySellPageMode.buy
? formattedFiatAmount ?? 200.0
: formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
: formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.01 : 1);
}
// sets based on the absolute amout (from the fiat/charts api)
// works even if you have no rates
Future<void> setCryptoAmountFromFiat(String fiatAmount) async {
if(fiatAmount.isEmpty) {
await changeCryptoAmount(amount: "");
return;
}
if(fiatConversionStore.prices[cryptoCurrency] == null) {
return;
}
await changeCryptoAmount(
amount: (double.parse(fiatAmount) / (fiatConversionStore.prices[cryptoCurrency]!))
.toString(),
);
}
final AppStore _appStore;
@@ -100,8 +126,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
final BuySellPageMode mode;
@observable
List<BuyProvider> providerList;
@@ -149,6 +174,100 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
bool skipIsReadyToTradeReaction = false;
@computed
String? get maxFiatAmount {
if ((sortedQuotes.isEmpty && sortedRecommendedQuotes.isEmpty) ||
buySellQuotState is! BuySellQuotLoaded) {
return null;
}
final allQuotes = sortedRecommendedQuotes.followedBy(sortedQuotes);
final maxAmount = allQuotes.fold<double>(0.0, (current, item) {
final limitMax = item.limits?.max?.toDouble() ?? 0.0;
return max(current, limitMax);
});
return maxAmount.toStringAsFixed(2);
}
Money? amountForQuote(Quote quote) => Money.trySafeParse(
((double.tryParse(fiatAmount) ?? 0) / quote.rate).toStringAsFixed(min(20, cryptoCurrency.decimals)),
cryptoCurrency);
Money? fiatAmountForQuote(Quote quote) {
if(fiatConversionStore.prices[cryptoCurrency] == null) {
return null;
}
return Money.trySafeParse(
(fiatConversionStore.prices[cryptoCurrency]! *
(double.tryParse(amountForQuote(quote).toString())??0))
.toStringAsFixed(2),
fiatCurrency);
}
// based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction)
static final Map<FiatCurrency, List<String>> _defaultAmountsMap = {
FiatCurrency.amd: ["20000", "40000", "200000", "400000", "1000000"],
FiatCurrency.ars: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.aud: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.bdt: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.bgn: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.brl: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.cad: ["50", "100", "500", "1000", "2500"],
FiatCurrency.chf: ["50", "100", "500", "1000", "2500"],
FiatCurrency.clp: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.cny: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.cop: ["200000", "400000", "2000000", "4000000", "10000000"],
FiatCurrency.czk: ["1000", "2000", "10000", "20000", "50000"],
FiatCurrency.dkk: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.egp: ["2500", "5000", "25000", "50000", "125000"],
FiatCurrency.eur: ["50", "100", "500", "1000", "2500"],
FiatCurrency.gbp: ["50", "100", "500", "1000", "2500"],
FiatCurrency.ghs: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.gtq: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.hkd: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.hrk: ["400", "800", "4000", "8000", "20000"],
FiatCurrency.huf: ["20000", "40000", "200000", "400000", "1000000"],
FiatCurrency.idr: ["800000", "1600000", "8000000", "16000000", "40000000"],
FiatCurrency.ils: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.inr: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.irr: ["2000000", "4000000", "20000000", "40000000", "100000000"],
FiatCurrency.isk: ["7000", "14000", "70000", "140000", "350000"],
FiatCurrency.jpy: ["10000", "20000", "100000", "200000", "500000"],
FiatCurrency.kes: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.krw: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.mad: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.mxn: ["1000", "2000", "10000", "20000", "50000"],
FiatCurrency.myr: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.ngn: ["50000", "100000", "500000", "1000000", "2500000"],
FiatCurrency.nok: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.nzd: ["100", "200", "1000", "2000", "5000"],
FiatCurrency.php: ["3000", "6000", "30000", "60000", "150000"],
FiatCurrency.pkr: ["15000", "30000", "150000", "300000", "750000"],
FiatCurrency.pln: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.ron: ["250", "500", "2500", "5000", "12500"],
FiatCurrency.rub: ["5000", "10000", "50000", "100000", "250000"],
FiatCurrency.sar: ["200", "400", "2000", "4000", "10000"],
FiatCurrency.sek: ["500", "1000", "5000", "10000", "25000"],
FiatCurrency.sgd: ["50", "100", "500", "1000", "2500"],
FiatCurrency.thb: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.tur: ["1500", "3000", "15000", "30000", "75000"],
FiatCurrency.twd: ["1500", "3000", "15000", "30000", "75000"],
FiatCurrency.uah: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.usd: ["50", "100", "500", "1000", "2500"],
FiatCurrency.vef: ["2000", "4000", "20000", "40000", "100000"],
FiatCurrency.vnd: ["1000000", "2000000", "10000000", "20000000", "50000000"],
FiatCurrency.zar: ["1000", "2000", "10000", "20000", "50000"],
};
// the fallback is just the usd values.
// not great but this fallback shouldn't be triggered anyway
List<String> get defaultAmounts =>
_defaultAmountsMap[fiatCurrency] ?? ["50", "100", "500", "1000", "2500"];
@computed
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
@@ -181,12 +300,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
}
@action
void changeBuySellAction() {
isBuyAction = !isBuyAction;
_initialize();
}
@action
void changeFiatCurrency({required FiatCurrency currency}) {
fiatCurrency = currency;
@@ -200,12 +313,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
@computed
Iterable<CryptoCurrency> get activeWalletCurrencies => wallet.balance.keys;
@computed
bool get hasMultipleCurrencies => activeWalletCurrencies.length > 1;
@action
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
@action
Future<void> changeFiatAmount({required String amount}) async {
fiatAmount = amount;
fiatAmount = amount.replaceAll(",", ".");
if (amount.isEmpty) {
fiatAmount = '';
@@ -214,16 +333,18 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
if (!isReadyToTrade && !isBuySellQuoteFailed) {
_cryptoAmount = S.current.fetching;
_cryptoAmount = "...";
return;
} else if (isBuySellQuoteFailed) {
_cryptoAmount = '';
return;
}
printV(bestRateQuote);
if (bestRateQuote != null) {
final enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0;
final amount = enteredAmount / bestRateQuote!.rate;
printV(amount);
_cryptoAmount = amount.toString().withMaxDecimals(cryptoCurrency.decimals);
} else {
@@ -338,7 +459,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
void _setProviders() =>
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
providerList = mode == BuySellPageMode.buy ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
@@ -347,53 +468,52 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
paymentMethodState = InitialPaymentMethod();
buySellQuotState = InitialBuySellQuotState();
sortedRecommendedQuotes.clear();
sortedQuotes.clear();
paymentMethods.clear();
await _getAvailablePaymentTypes();
await calculateBestRate();
}
String _getInitialCryptoCurrencyAddress() {
if (cryptoCurrency == wallet.currency) {
if ([CryptoCurrency.zec, CryptoCurrency.btc].contains(cryptoCurrency)) {
return wallet.walletAddresses.addressForBuy;
}
return wallet.walletAddresses.address;
}
return '';
}
String _getInitialCryptoCurrencyAddress() => wallet.walletAddresses.addressForBuy;
@action
Future<void> _getAvailablePaymentTypes() async {
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) =>
element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
try{
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) => element
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, mode == BuySellPageMode.buy)
.timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
final List<PaymentMethod> tempPaymentMethods = [];
final List<PaymentMethod> tempPaymentMethods = [];
for (var methods in result) {
for (var method in methods) {
final alreadyExists = tempPaymentMethods.any((m) {
return m.paymentMethodType == method.paymentMethodType;
});
for (var methods in result) {
for (var method in methods) {
final alreadyExists = tempPaymentMethods.any((m) {
return m.paymentMethodType == method.paymentMethodType;
});
if (!alreadyExists) {
tempPaymentMethods.add(method);
if (!alreadyExists) {
tempPaymentMethods.add(method);
}
}
}
}
paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
paymentMethodState = PaymentMethodFailed();
}
} catch(e) {
paymentMethodState = PaymentMethodFailed();
}
}
@@ -403,17 +523,22 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
buySellQuotState = BuySellQuotLoading();
final List<BuyProvider> validProviders = providerList.where((provider) {
if (isBuyAction) {
return provider.supportedCryptoList
.any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency);
if (mode == BuySellPageMode.buy) {
return provider.supportedCryptoList.any((pair) =>
pair.from.symbol == cryptoCurrency.symbol &&
pair.from.tag == cryptoCurrency.tag &&
pair.to.symbol == fiatCurrency.symbol);
} else {
return provider.supportedFiatList
.any((pair) => pair.from == fiatCurrency && pair.to == cryptoCurrency);
return provider.supportedFiatList.any((pair) =>
pair.from.symbol == fiatCurrency.symbol &&
pair.to.symbol == cryptoCurrency.symbol &&
pair.to.tag == cryptoCurrency.tag);
}
}).toList();
if (validProviders.isEmpty) {
buySellQuotState = BuySellQuotFailed();
buySellQuotState = BuySellQuotFailed(
errorMessage: S.current.no_buy_providers(cryptoCurrency.fullName??cryptoCurrency.title));
return;
}
@@ -423,7 +548,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
isBuyAction: mode == BuySellPageMode.buy,
walletAddress: wallet.walletAddresses.address,
customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
)
@@ -441,11 +566,12 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
.toList();
if (validQuotes.isEmpty) {
buySellQuotState = BuySellQuotFailed();
buySellQuotState = BuySellQuotFailed(
errorMessage: S.current.no_buy_providers(cryptoCurrency.fullName??cryptoCurrency.title));
return;
}
if (isBuyAction) {
if (mode == BuySellPageMode.buy) {
validQuotes.sort((a, b) => b.payout.compareTo(a.payout));
} else {
validQuotes.sort((a, b) => a.payout.compareTo(b.payout));
@@ -486,7 +612,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
});
final Quote effectiveBestRateQuote = sortedRecommendedQuotes.reduce((a, b) {
return isBuyAction
return mode == BuySellPageMode.buy
? a.rate < b.rate
? a
: b
@@ -512,7 +638,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
context: context,
quote: selectedQuote!,
amount: amount,
isBuyAction: isBuyAction,
isBuyAction: mode == BuySellPageMode.buy,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
} catch (e) {
@@ -55,7 +55,7 @@ import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/file.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
@@ -1515,7 +1515,8 @@ abstract class DashboardViewModelBase with Store {
if (walletInfo.type == WalletType.bitcoin) {
final password = await keyService.getWalletPassword(walletName: walletInfo.name);
final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final encryption = encryptionFileUtilsFor(SettingsStoreBase.walletPasswordDirectInput);
final jsonSource = await encryption.read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
+14 -3
View File
@@ -338,10 +338,21 @@ abstract class OutputBase with Store {
? CryptoCurrency.btc
: cryptoCurrencyHandler();
final price = _fiatConversationStore.prices[cryptoCurrency];
if (price == null || !price.isFinite || price <= 0) {
printV('_updateCryptoAmount: invalid fiat rate $price for $cryptoCurrency');
cryptoAmount = '';
return;
}
final decimals = min(20, cryptoCurrencyHandler().decimals);
final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) /
_fiatConversationStore.prices[cryptoCurrency]!)
.toStringAsFixed(decimals);
final rawCrypto = double.parse(fiatAmount.replaceAll(',', '.')) / price;
if (!rawCrypto.isFinite) {
cryptoAmount = '';
return;
}
final crypto = rawCrypto.toStringAsFixed(decimals);
if (cryptoAmount != crypto) cryptoAmount = crypto;
} catch (e) {
+15 -4
View File
@@ -54,6 +54,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/amount/amount_sanitizer.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
@@ -882,7 +883,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final fee = actualFee > 0 ? actualFee : 0.0005;
final fromCurrency = trade.from ?? CryptoCurrency.sol;
final amount = Money.tryParse(trade.amount, fromCurrency) ?? Money.zero(fromCurrency);
final amount = Money.tryParse(
trade.amount.sanitized(),
fromCurrency,
strictParsing: false,
) ?? Money.zero(fromCurrency);
pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction(
wallet,
@@ -920,8 +925,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final bool isTradeTx = trade != null && provider != null;
if (isTradeTx) {
final tradeAmountDouble = double.tryParse(trade.amount) ?? 0.0;
if (tradeAmountDouble <= 0) throw Exception('Trade amount must be greater than 0');
final tradeAmountMoney = Money.tryParse(
trade.amount.sanitized(),
trade.from ?? selectedCryptoCurrency,
strictParsing: false,
);
if (tradeAmountMoney == null || tradeAmountMoney.sign <= 0) {
throw Exception('Trade amount must be greater than 0');
}
final tradeAmountDouble = double.tryParse(tradeAmountMoney.toString()) ?? 0.0;
if (trade.isSendAll == true) {
if (provider is NearIntentsExchangeProvider) {
@@ -1146,7 +1158,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
);
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
id: pendingTransaction!.evmTxHashFromRawHex!,
height: 0,