Cw 78 ethereum (#862)

* Add initial flow for ethereum

* Add initial create Eth wallet flow

* Complete Ethereum wallet creation flow

* Fix web3dart versioning issue

* Add primary receive address extracted from private key

* Implement open wallet functionality

* Implement restore wallet from seed functionality

* Fixate web3dart version as higher versions cause some issues

* Add Initial Transaction priorities for eth
Add estimated gas price

* Rename priority value to tip

* Re-order wallet types

* Change ethereum node
Fix connection issues

* Fix estimating gas for priority

* Add case for ethereum to fetch it's seeds

* Add case for ethereum to request node

* Fix Exchange screen initial pairs

* Add initial send transaction flow

* Add missing configure for ethereum class

* Add Eth address initial setup

* Fix Private key for Ethereum wallets

* Change sign/send transaction flow

* - Fix Conflicts with main
- Remove unused function from Haven configure.dart

* Add build command for ethereum package

* Add missing Node list file to pubspec

* - Fix balance display
- Fix parsing of Ethereum amount
- Add more Ethereum Nodes 

* - Fix extracting Ethereum Private key from seeds
- Integrate signing/sending transaction with the send view model

* - Update and Fix Conflicts with main

* Add Balances for ERC20 tokens

* Fix conflicts with main

* Add erc20 abi json

* Add send erc20 tokens initial function

* add missing getHeightByDate in Haven

* Allow contacts and wallets from the same tag

* Add Shiba Inu icon

* Add send ERC-20 tokens initial flow

* Add missing import in generated file

* Add initial approach for transaction sending for ERC-20 tokens

* Refactor signing/sending transactions

* Add initial flow for transactions subscription

* Refactor signing/sending transactions

* Add home settings icon

* Fix conflicts with main

* Initial flow for home settings

* Add logic flow for adding erc20 tokens

* Fix initial UI

* Finalize UI for Tokens

* Integrate UI with Ethereum flow

* Add "Enable/Disable" feature for ERC20 tokens

* Add initial Erc20 tokens

* Add Sorting and Pin Native Token features

* Fix price sorting

* Sort tokens list as well when Sort criteria changes

* - Improve sorting balances flow
- Add initial add token from search bar flow

* Fix Accounts Popup UI

* Fix Pin native token

* Fix Enabling/Disabling tokens
Fix sorting by fiat once app is opened
Improve token availability mechanism

* Fix deleting token
Fix renaming tokens

* Fix issue with search

* Add more tokens

* - Fix scroll issue
- Add ERC20 tokens placeholder image in picker

* - Separate and organize default erc20 tokens
- Fix scrolling
- Add token placeholder images in picker
- Sort disabled tokens alphabetically

* Change BNB token initial availability

* Fix Conflicts with main

* Fix Conflicts with main

* Add Verse ERC20 token to the initial tokens list

* Add rename wallet to Ethereum

* Integrate EtherScan API for fetching address transactions
Generate Ethereum specific secrets in Ethereum package

* Adjust transactions fiat price for ERC20 tokens

* Free Up GitHub Actions Ubuntu Runner Disk Space

* Free Up GitHub Actions Ubuntu Runner Disk space (trial 2)

* Fix Transaction Fee display

* Save transaction history

* Enhance loading time for erc20 tokens transactions

* Minor Fixes and Enhancements

* Fix sending erc20
fix block explorer issue

* Fix int overflow

* Fix transaction amount conversions

* Minor: `slow` -> `Slow` 

* Update build guide

* Fix fetching fiat rate taking a lot of time by only fetching enabled tokens only and making the API calls in parallel not sequential

* Update transactions on a periodic basis

* For fee, use ETH spot price, not ERC-20 spot price

* Add Etherscan History privacy option to enable/disable Etherscan API

* Show estimated fee amounts in the send screen

* fix send fiat fields parsing issue

* Fix transactions estimated fee less than actual fee

* handle balance sorting when balance is disabled
Handle empty transactions list

* Fix Delete Ethereum wallet
Fix balance < 0.01

* Fix Decimal place for Ethereum amount
Fix sending amount issue

* Change words count

* Remove balance hint and Full balance row from Ethereum wallets

* support changing the asset type in send templates

* Fix Templates for ERC tokens issues

* Fix conflicts in send templates

* Disable batch sending in Ethereum

* Fix Fee calculation with different priorities

* Fix Conflicts with main

* Add offline error to ignored exceptions

---------

Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
This commit is contained in:
Omar Hatem
2023-08-04 20:01:49 +03:00
committed by GitHub
co-authored by Justin Ehrenhofer
parent 4120394121
commit 3ce4000dcf
137 changed files with 7164 additions and 1492 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin {
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
priority: priority != null ? priority as BitcoinTransactionPriority : null,
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
@override
+14 -9
View File
@@ -2,6 +2,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type})
@@ -14,6 +15,9 @@ class AddressValidator extends TextValidator {
length: getLength(type));
static String getPattern(CryptoCurrency type) {
if (type is Erc20Token) {
return '0x[0-9a-zA-Z]';
}
switch (type) {
case CryptoCurrency.xmr:
return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$';
@@ -56,6 +60,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zrx:
case CryptoCurrency.dydx:
case CryptoCurrency.steth:
case CryptoCurrency.shib:
return '0x[0-9a-zA-Z]';
case CryptoCurrency.xrp:
return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$';
@@ -116,17 +121,14 @@ class AddressValidator extends TextValidator {
}
static List<int>? getLength(CryptoCurrency type) {
if (type is Erc20Token) {
return [42];
}
switch (type) {
case CryptoCurrency.xmr:
return null;
case CryptoCurrency.ada:
return null;
case CryptoCurrency.avaxc:
return [42];
case CryptoCurrency.bch:
return [42];
case CryptoCurrency.bnb:
return [42];
case CryptoCurrency.btc:
return null;
case CryptoCurrency.dash:
@@ -166,6 +168,10 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zrx:
case CryptoCurrency.dydx:
case CryptoCurrency.steth:
case CryptoCurrency.shib:
case CryptoCurrency.avaxc:
case CryptoCurrency.bch:
case CryptoCurrency.bnb:
return [42];
case CryptoCurrency.ltc:
return [34, 43, 63];
@@ -203,11 +209,8 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.xusd:
return [98, 99, 106];
case CryptoCurrency.btt:
return [34];
case CryptoCurrency.bttc:
return [34];
case CryptoCurrency.doge:
return [34];
case CryptoCurrency.firo:
return [34];
case CryptoCurrency.hbar:
@@ -258,6 +261,8 @@ class AddressValidator extends TextValidator {
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.eth:
return '0x[0-9a-zA-Z]{42}';
default:
return null;
}
+18
View File
@@ -240,6 +240,9 @@ class BackupService {
data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?;
final shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?;
final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?;
final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?;
final useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
@@ -349,6 +352,15 @@ class BackupService {
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
shouldRequireTOTP2FAForAllSecurityAndBackupSettings);
if (sortBalanceTokensBy != null)
await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy);
if (pinNativeTokenAtTop != null)
await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop);
if (useEtherscan != null)
await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
await preferencesFile.delete();
}
@@ -492,6 +504,12 @@ class BackupService {
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets),
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences
.getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
PreferencesKey.sortBalanceBy:
_sharedPreferences.getInt(PreferencesKey.sortBalanceBy),
PreferencesKey.pinNativeTokenAtTop:
_sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
PreferencesKey.useEtherscan:
_sharedPreferences.getBool(PreferencesKey.useEtherscan),
};
return json.encode(preferences);
+10 -7
View File
@@ -5,21 +5,20 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com';
const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion';
const _fiatApiPath = '/v2/rates';
Future<double> _fetchPrice(Map<String, dynamic> args) async {
final crypto = args['crypto'] as CryptoCurrency;
final fiat = args['fiat'] as FiatCurrency;
final crypto = args['crypto'] as String;
final fiat = args['fiat'] as String;
final torOnly = args['torOnly'] as bool;
final Map<String, String> queryParams = {
'interval_count': '1',
'base': crypto.toString(),
'quote': fiat.toString(),
'key' : secrets.fiatApiKey,
'base': crypto,
'quote': fiat,
'key': secrets.fiatApiKey,
};
double price = 0.0;
@@ -52,7 +51,11 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
}
Future<double> _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async =>
compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly});
compute(_fetchPrice, {
'fiat': fiat.toString(),
'crypto': crypto.toString(),
'torOnly': torOnly,
});
class FiatConversionService {
static Future<double> fetchPrice({
+3
View File
@@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/entities/mnemonic_item.dart';
@@ -25,6 +26,8 @@ class SeedValidator extends Validator<MnemonicItem> {
return monero!.getMoneroWordList(language);
case WalletType.haven:
return haven!.getMoneroWordList(language);
case WalletType.ethereum:
return ethereum!.getEthereumWordList(language);
default:
return [];
}
+27 -6
View File
@@ -7,6 +7,7 @@ import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/ionia/ionia_anypay.dart';
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_tip.dart';
@@ -16,6 +17,8 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
@@ -40,6 +43,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
@@ -70,6 +74,7 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/core/backup_service.dart';
import 'package:cw_core/wallet_service.dart';
@@ -239,9 +244,9 @@ Future setup({
getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
}
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) &&
(secrets.wyreApiKey.isNotEmpty ?? false) &&
(secrets.wyreAccountId.isNotEmpty ?? false);
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) &&
(secrets.wyreApiKey.isNotEmpty) &&
(secrets.wyreAccountId.isNotEmpty);
final settingsStore = await SettingsStoreBase.load(
nodeSource: _nodeSource,
@@ -638,7 +643,7 @@ Future setup({
});
getIt.registerFactory(() {
return PrivacySettingsViewModel(getIt.get<SettingsStore>());
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
});
getIt.registerFactory(() {
@@ -745,6 +750,8 @@ Future setup({
return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!);
case WalletType.litecoin:
return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!);
case WalletType.ethereum:
return ethereum!.createEthereumWalletService(_walletInfoSource);
default:
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
}
@@ -787,8 +794,8 @@ Future setup({
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), void>(
(param1, _) => NewWalletTypePage(onTypeSelected: param1));
getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), bool?>(
(param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true));
getIt.registerFactoryParam<PreSeedPage, WalletType, void>(
(WalletType type, _) => PreSeedPage(type));
@@ -1034,5 +1041,19 @@ Future setup({
getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>(
(type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
getIt.registerFactoryParam<HomeSettingsViewModel, BalanceViewModel, void>(
(balanceViewModel, _) => HomeSettingsViewModel(getIt.get<SettingsStore>(), balanceViewModel));
getIt.registerFactoryParam<EditTokenPage, HomeSettingsViewModel, Map<String, dynamic>>(
(homeSettingsViewModel, arguments) => EditTokenPage(
homeSettingsViewModel: homeSettingsViewModel,
erc20token: arguments['token'] as Erc20Token?,
initialContractAddress: arguments['contractAddress'] as String?,
),
);
_isSetupFinished = true;
}
+44 -2
View File
@@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002';
const havenDefaultNodeUri = 'nodes.havenprotocol.org:443';
const ethereumDefaultNodeUri = 'ethereum.publicnode.com';
Future defaultSettingsMigration(
{required int version,
@@ -157,6 +158,12 @@ Future defaultSettingsMigration(
case 20:
await migrateExchangeStatus(sharedPreferences);
break;
case 21:
await addEthereumNodeList(nodes: nodes);
await changeEthereumCurrentNodeToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
break;
default:
break;
}
@@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box<Node> nodes}) {
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven);
}
Node? getEthereumDefaultNode({required Box<Node> nodes}) {
return nodes.values.firstWhereOrNull(
(Node node) => node.uriRaw == ethereumDefaultNodeUri)
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum);
}
Node getMoneroDefaultNode({required Box<Node> nodes}) {
final timeZone = DateTime.now().timeZoneOffset.inHours;
var nodeUri = '';
@@ -438,6 +451,8 @@ Future<void> checkCurrentNodes(
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final currentHavenNodeId = sharedPreferences
.getInt(PreferencesKey.currentHavenNodeIdKey);
final currentEthereumNodeId = sharedPreferences
.getInt(PreferencesKey.currentEthereumNodeIdKey);
final currentMoneroNode = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentMoneroNodeId);
final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull(
@@ -446,6 +461,8 @@ Future<void> checkCurrentNodes(
(node) => node.key == currentLitecoinElectrumSeverId);
final currentHavenNodeServer = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentHavenNodeId);
final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentEthereumNodeId);
if (currentMoneroNode == null) {
final newCakeWalletNode =
@@ -479,6 +496,13 @@ Future<void> checkCurrentNodes(
await sharedPreferences.setInt(
PreferencesKey.currentHavenNodeIdKey, node.key as int);
}
if (currentEthereumNodeServer == null) {
final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum);
await nodeSource.add(node);
await sharedPreferences.setInt(
PreferencesKey.currentEthereumNodeIdKey, node.key as int);
}
}
Future<void> resetBitcoinElectrumServer(
@@ -522,8 +546,26 @@ Future<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async {
return;
}
await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled
await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled
? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw);
await sharedPreferences.remove(PreferencesKey.disableExchangeKey);
}
Future<void> addEthereumNodeList({required Box<Node> nodes}) async {
final nodeList = await loadDefaultEthereumNodes();
for (var node in nodeList) {
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await nodes.add(node);
}
}
}
Future<void> changeEthereumCurrentNodeToDefault(
{required SharedPreferences sharedPreferences,
required Box<Node> nodes}) async {
final node = getEthereumDefaultNode(nodes: nodes);
final nodeId = node?.key as int? ?? 0;
await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId);
}
+2
View File
@@ -46,6 +46,7 @@ class MainActions {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.ethereum:
if (viewModel.isEnabledBuyAction) {
final uri = getIt.get<OnRamperBuyProvider>().requestUrl();
if (DeviceInfo.instance.isMobile) {
@@ -116,6 +117,7 @@ class MainActions {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.ethereum:
if (viewModel.isEnabledSellAction) {
final moonPaySellProvider = MoonPaySellProvider();
final uri = await moonPaySellProvider.requestUrl(
+16
View File
@@ -70,6 +70,22 @@ Future<List<Node>> loadDefaultHavenNodes() async {
return nodes;
}
Future<List<Node>> loadDefaultEthereumNodes() async {
final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml');
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = WalletType.ethereum;
nodes.add(node);
}
}
return nodes;
}
Future resetToDefault(Box<Node> nodeSource) async {
final moneroNodes = await loadDefaultNodes();
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
+5
View File
@@ -5,6 +5,7 @@ class PreferencesKey {
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
static const currentHavenNodeIdKey = 'current_node_id_xhv';
static const currentEthereumNodeIdKey = 'current_node_id_eth';
static const currentFiatCurrencyKey = 'current_fiat_currency';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
@@ -31,6 +32,7 @@ class PreferencesKey {
static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin';
static const havenTransactionPriority = 'current_fee_priority_haven';
static const litecoinTransactionPriority = 'current_fee_priority_litecoin';
static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
@@ -38,6 +40,9 @@ class PreferencesKey {
static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
static const lastPopupDate = 'last_popup_date';
static const lastAppReviewDate = 'last_app_review_date';
static const sortBalanceBy = 'sort_balance_by';
static const pinNativeTokenAtTop = 'pin_native_token_at_top';
static const useEtherscan = 'use_etherscan';
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
@@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cw_core/transaction_priority.dart';
@@ -14,6 +15,8 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
return bitcoin!.getLitecoinTransactionPriorities();
case WalletType.haven:
return haven!.getTransactionPriorities();
case WalletType.ethereum:
return ethereum!.getTransactionPriorities();
default:
return [];
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:cake_wallet/generated/i18n.dart';
enum SortBalanceBy {
FiatBalance,
GrossBalance,
Alphabetical;
@override
String toString() {
switch (this) {
case SortBalanceBy.FiatBalance:
return S.current.fiat_balance;
case SortBalanceBy.GrossBalance:
return S.current.gross_balance;
case SortBalanceBy.Alphabetical:
return S.current.alphabetical;
}
}
}
+1 -1
View File
@@ -55,5 +55,5 @@ class Template extends HiveObject {
String get amount => amountRaw ?? '';
List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
List<Template>? get additionalRecipients => additionalRecipientsRaw;
}
+126
View File
@@ -0,0 +1,126 @@
part of 'ethereum.dart';
class CWEthereum extends Ethereum {
@override
List<String> getEthereumWordList(String language) => EthereumMnemonics.englishWordlist;
WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource) =>
EthereumWalletService(walletInfoSource);
@override
WalletCredentials createEthereumNewWalletCredentials({
required String name,
WalletInfo? walletInfo,
}) =>
EthereumNewWalletCredentials(name: name, walletInfo: walletInfo);
@override
WalletCredentials createEthereumRestoreWalletFromSeedCredentials({
required String name,
required String mnemonic,
required String password,
}) =>
EthereumRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
@override
String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address;
@override
TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium;
@override
List<TransactionPriority> getTransactionPriorities() => EthereumTransactionPriority.all;
@override
TransactionPriority deserializeEthereumTransactionPriority(int raw) =>
EthereumTransactionPriority.deserialize(raw: raw);
Object createEthereumTransactionCredentials(
List<Output> outputs, {
required TransactionPriority priority,
required CryptoCurrency currency,
int? feeRate,
}) =>
EthereumTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
priority: priority as EthereumTransactionPriority,
currency: currency,
feeRate: feeRate,
);
Object createEthereumTransactionCredentialsRaw(
List<OutputInfo> outputs, {
TransactionPriority? priority,
required CryptoCurrency currency,
required int feeRate,
}) =>
EthereumTransactionCredentials(
outputs,
priority: priority as EthereumTransactionPriority?,
currency: currency,
feeRate: feeRate,
);
@override
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
@override
double formatterEthereumAmountToDouble(
{TransactionInfo? transaction, BigInt? amount, int exponent = 18}) {
assert(transaction != null || amount != null);
if (transaction != null) {
transaction as EthereumTransactionInfo;
return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
} else {
return (amount!) / BigInt.from(10).pow(exponent);
}
}
@override
List<Erc20Token> getERC20Currencies(WalletBase wallet) {
final ethereumWallet = wallet as EthereumWallet;
return ethereumWallet.erc20Currencies;
}
@override
Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as EthereumWallet).addErc20Token(token);
@override
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as EthereumWallet).deleteErc20Token(token);
@override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
final ethereumWallet = wallet as EthereumWallet;
return await ethereumWallet.getErc20Token(contractAddress);
}
@override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as EthereumTransactionInfo;
if (transaction.tokenSymbol == CryptoCurrency.eth.title) {
return CryptoCurrency.eth;
}
wallet as EthereumWallet;
return wallet.erc20Currencies
.firstWhere((element) => transaction.tokenSymbol == element.symbol);
}
@override
void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) {
(wallet as EthereumWallet).updateEtherscanUsageState(isEnabled);
}
}
+1 -1
View File
@@ -141,7 +141,7 @@ Future<void> main() async {
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 19);
initialMigrationVersion: 21);
runApp(App());
}, (error, stackTrace) async {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
+15
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -31,6 +32,20 @@ Future<void> startFiatRateUpdate(
fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
}
if (appStore.wallet!.type == WalletType.ethereum) {
final currencies =
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
for (final currency in currencies) {
() async {
fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
crypto: currency,
fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
}.call();
}
}
} catch (e) {
print(e);
}
+15 -1
View File
@@ -1,6 +1,6 @@
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_info.dart';
@@ -97,6 +97,20 @@ void startCurrentWalletChangeReaction(AppStore appStore,
crypto: wallet.currency,
fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
if (wallet.type == WalletType.ethereum) {
final currencies =
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
for (final currency in currencies) {
() async {
fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
crypto: currency,
fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
}.call();
}
}
} catch (e) {
print(e.toString());
}
+29 -8
View File
@@ -10,6 +10,8 @@ import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
@@ -313,7 +315,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) => getIt.get<SecurityBackupPage>());
case Routes.privacyPage:
return CupertinoPageRoute<void>(
fullscreenDialog: true,
@@ -328,7 +330,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) => getIt.get<OtherSettingsPage>());
case Routes.newNode:
final args = settings.arguments as Map<String, dynamic>?;
return CupertinoPageRoute<void>(
@@ -336,7 +338,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
param1: args?['editingNode'] as Node?,
param2: args?['isSelected'] as bool?));
case Routes.accountCreation:
return CupertinoPageRoute<String>(
@@ -466,7 +468,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
fullscreenDialog: true,
builder: (_) => getIt.get<IoniaWelcomePage>(),
);
case Routes.ioniaLoginPage:
return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaLoginPage>());
@@ -480,7 +482,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.ioniaBuyGiftCardPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardPage>(param1: args));
case Routes.ioniaBuyGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardDetailPage>(param1: args));
@@ -497,7 +499,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.ioniaAccountPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountPage>());
case Routes.ioniaAccountCardsPage:
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountCardsPage>());
@@ -508,11 +510,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.ioniaGiftCardDetailPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaGiftCardDetailPage>(param1: args.first));
case Routes.ioniaCustomRedeemPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaCustomRedeemPage>(param1: args));
case Routes.ioniaMoreOptionsPage:
final args = settings.arguments as List;
return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaMoreOptionsPage>(param1: args));
@@ -584,6 +586,25 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.modify2FAPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<Modify2FAPage>());
case Routes.homeSettings:
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
);
case Routes.editToken:
final args = settings.arguments as Map<String, dynamic>;
return CupertinoPageRoute<void>(
settings: RouteSettings(name: Routes.editToken),
builder: (_) => getIt.get<EditTokenPage>(
param1: args['homeSettingsViewModel'],
param2: {
'token': args['token'],
'contractAddress': args['contractAddress'],
},
),
);
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(
+2
View File
@@ -88,4 +88,6 @@ class Routes {
static const setup_2faQRPage = '/setup_2fa_qr_page';
static const totpAuthCodePage = '/totp_auth_code_page';
static const modify2FAPage = '/modify_2fa_page';
static const homeSettings = '/home_settings';
static const editToken = '/edit_token';
}
@@ -30,6 +30,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset(
@@ -136,6 +137,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return litecoinIcon;
case WalletType.haven:
return havenIcon;
case WalletType.ethereum:
return ethereumIcon;
default:
return nonWalletTypeIcon;
}
@@ -0,0 +1,309 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/checkbox_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EditTokenPage extends BasePage {
EditTokenPage({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.initialContractAddress,
}) : assert(erc20token == null || initialContractAddress == null);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final String? initialContractAddress;
@override
String? get title => S.current.edit_token;
@override
Widget body(BuildContext context) {
return EditTokenPageBody(
homeSettingsViewModel: homeSettingsViewModel,
erc20token: erc20token,
initialContractAddress: initialContractAddress,
);
}
}
class EditTokenPageBody extends StatefulWidget {
const EditTokenPageBody({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.initialContractAddress,
}) : super(key: key);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final String? initialContractAddress;
@override
State<EditTokenPageBody> createState() => _EditTokenPageBodyState();
}
class _EditTokenPageBodyState extends State<EditTokenPageBody> {
final TextEditingController _contractAddressController = TextEditingController();
final TextEditingController _tokenNameController = TextEditingController();
final TextEditingController _tokenSymbolController = TextEditingController();
final TextEditingController _tokenDecimalController = TextEditingController();
final FocusNode _contractAddressFocusNode = FocusNode();
final FocusNode _tokenNameFocusNode = FocusNode();
final FocusNode _tokenSymbolFocusNode = FocusNode();
final FocusNode _tokenDecimalFocusNode = FocusNode();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool _showDisclaimer = false;
bool _disclaimerChecked = false;
@override
void initState() {
super.initState();
if (widget.erc20token != null) {
_contractAddressController.text = widget.erc20token!.contractAddress;
_tokenNameController.text = widget.erc20token!.name;
_tokenSymbolController.text = widget.erc20token!.symbol;
_tokenDecimalController.text = widget.erc20token!.decimal.toString();
}
if (widget.initialContractAddress != null) {
_contractAddressController.text = widget.initialContractAddress!;
_getTokenInfo();
}
_contractAddressFocusNode.addListener(() {
if (!_contractAddressFocusNode.hasFocus) {
_getTokenInfo();
}
final contractAddress = _contractAddressController.text;
if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) {
setState(() {
_showDisclaimer = true;
});
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Padding(
padding: EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 28),
decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/images/restore_keys.png'),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).warning,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
),
),
Padding(
padding: EdgeInsets.only(top: 5),
child: Text(
S.of(context).add_token_warning,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).primaryTextTheme.labelSmall!.color!,
),
),
),
],
),
),
],
),
),
SizedBox(height: 50),
_tokenForm(),
],
),
),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(
children: [
if (_showDisclaimer) ...[
CheckboxWidget(
value: _disclaimerChecked,
caption: S.of(context).add_token_disclaimer_check,
onChanged: (value) {
_disclaimerChecked = value;
},
),
SizedBox(height: 20),
],
Row(
children: <Widget>[
Expanded(
child: PrimaryButton(
onPressed: () async {
if (widget.erc20token != null) {
await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!);
}
Navigator.pop(context);
},
text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel,
color: Colors.red,
textColor: Colors.white,
),
),
SizedBox(width: 20),
Expanded(
child: PrimaryButton(
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addErc20Token(Erc20Token(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
contractAddress: _contractAddressController.text,
decimal: int.parse(_tokenDecimalController.text),
));
Navigator.pop(context);
}
},
text: S.of(context).save,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
),
),
],
),
],
),
),
);
}
void _getTokenInfo() async {
if (_contractAddressController.text.isNotEmpty) {
final token =
await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text);
if (token != null) {
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol;
if (_tokenDecimalController.text.isEmpty)
_tokenDecimalController.text = token.decimal.toString();
}
}
}
Future<void> _pasteText() async {
final value = await Clipboard.getData('text/plain');
if (value?.text?.isNotEmpty ?? false) {
_contractAddressController.text = value!.text!;
_getTokenInfo();
setState(() {
_showDisclaimer = true;
});
}
}
Widget _tokenForm() {
return Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AddressTextField(
controller: _contractAddressController,
focusNode: _contractAddressFocusNode,
placeholder: S.of(context).token_contract_address,
options: [AddressTextFieldOption.paste],
buttonColor: Theme.of(context).hintColor,
validator: AddressValidator(type: widget.homeSettingsViewModel.nativeToken),
onPushPasteButton: (_) {
_pasteText();
},
),
const SizedBox(height: 8),
BaseTextFormField(
controller: _tokenNameController,
focusNode: _tokenNameFocusNode,
onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenSymbolFocusNode),
textInputAction: TextInputAction.next,
hintText: S.of(context).token_name,
validator: (text) {
if (text?.isNotEmpty ?? false) {
return null;
}
return S.of(context).field_required;
},
),
const SizedBox(height: 8),
BaseTextFormField(
controller: _tokenSymbolController,
focusNode: _tokenSymbolFocusNode,
onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenDecimalFocusNode),
textInputAction: TextInputAction.next,
hintText: S.of(context).token_symbol,
validator: (text) {
if (text?.isNotEmpty ?? false) {
return null;
}
return S.of(context).field_required;
},
),
const SizedBox(height: 8),
BaseTextFormField(
controller: _tokenDecimalController,
focusNode: _tokenDecimalFocusNode,
textInputAction: TextInputAction.done,
hintText: S.of(context).token_decimal,
validator: (text) {
if (text?.isEmpty ?? true) {
return S.of(context).field_required;
}
if (int.tryParse(text!) == null) {
return S.of(context).invalid_input;
}
return null;
},
),
SizedBox(height: 24),
],
),
);
}
}
@@ -0,0 +1,164 @@
import 'dart:math';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class HomeSettingsPage extends BasePage {
HomeSettingsPage(this._homeSettingsViewModel);
final HomeSettingsViewModel _homeSettingsViewModel;
final TextEditingController _searchController = TextEditingController();
@override
String? get title => S.current.home_screen_settings;
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
Observer(
builder: (_) => SettingsPickerCell<SortBalanceBy>(
title: S.current.sort_by,
items: SortBalanceBy.values,
selectedItem: _homeSettingsViewModel.sortBalanceBy,
onItemSelected: _homeSettingsViewModel.setSortBalanceBy,
),
),
Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!),
Observer(
builder: (_) => SettingsSwitcherCell(
title: S.of(context).pin_at_top(_homeSettingsViewModel.nativeToken.title),
value: _homeSettingsViewModel.pinNativeToken,
onValueChange: (_, bool value) {
_homeSettingsViewModel.setPinNativeToken(value);
},
),
),
Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: TextFormField(
controller: _searchController,
style: TextStyle(color: Theme.of(context).primaryTextTheme.titleLarge!.color!),
decoration: InputDecoration(
hintText: S.of(context).search_add_token,
prefixIcon: Image.asset("assets/images/search_icon.png"),
filled: true,
fillColor: Theme.of(context).accentTextTheme.displaySmall!.color!,
alignLabelWithHint: false,
contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: const BorderSide(color: Colors.transparent),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: const BorderSide(color: Colors.transparent),
),
),
onChanged: (String text) => _homeSettingsViewModel.changeSearchText(text),
),
),
),
RawMaterialButton(
onPressed: () async {
Navigator.pushNamed(context, Routes.editToken, arguments: {
'homeSettingsViewModel': _homeSettingsViewModel,
if (AddressValidator(type: _homeSettingsViewModel.nativeToken)
.isValid(_searchController.text))
'contractAddress': _searchController.text,
});
},
elevation: 0,
fillColor: Theme.of(context).accentTextTheme.bodySmall!.color!,
child: Icon(
Icons.add,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
size: 22.0,
),
padding: EdgeInsets.all(12),
shape: CircleBorder(),
splashColor: Theme.of(context).accentTextTheme.bodySmall!.color!,
),
],
),
Padding(
padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
child: Observer(
builder: (_) => ListView.builder(
itemCount: _homeSettingsViewModel.tokens.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(top: 16),
child: Observer(
builder: (_) {
final token = _homeSettingsViewModel.tokens.elementAt(index);
return SettingsSwitcherCell(
title: "${token.name} "
"(${token.symbol})",
value: token.enabled,
onValueChange: (_, bool value) {
_homeSettingsViewModel.changeTokenAvailability(token, value);
},
onTap: (_) {
Navigator.pushNamed(context, Routes.editToken, arguments: {
'homeSettingsViewModel': _homeSettingsViewModel,
'token': token,
});
},
leading: token.iconPath != null
? Container(
child: Image.asset(
token.iconPath!,
height: 30.0,
width: 30.0,
),
)
: Container(
height: 30.0,
width: 30.0,
child: Center(
child: Text(
token.symbol.substring(0, min(token.symbol.length, 2)),
style: TextStyle(fontSize: 11),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade400,
),
),
decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
borderRadius: BorderRadius.circular(30),
),
);
},
),
);
},
),
),
),
],
),
);
}
}
@@ -27,15 +27,15 @@ class AddressPage extends BasePage {
required this.addressListViewModel,
required this.dashboardViewModel,
required this.receiveOptionViewModel,
}) : _cryptoAmountFocus = FocusNode(),
_formKey = GlobalKey<FormState>(),
_amountController = TextEditingController(){
_amountController.addListener(() {
if (_formKey.currentState!.validate()) {
addressListViewModel.changeAmount(
_amountController.text,
);
}
}) : _cryptoAmountFocus = FocusNode(),
_formKey = GlobalKey<FormState>(),
_amountController = TextEditingController() {
_amountController.addListener(() {
if (_formKey.currentState!.validate()) {
addressListViewModel.changeAmount(
_amountController.text,
);
}
});
}
@@ -63,15 +63,11 @@ class AddressPage extends BasePage {
Widget? leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
size: 16,
);
final _closeButton = currentTheme.type == ThemeType.dark
? closeButtonImageDarkTheme
: closeButtonImage;
final _closeButton =
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
@@ -82,13 +78,10 @@ class AddressPage extends BasePage {
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView
? S.of(context).close
: S.of(context).seed_alert_back,
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith(
(states) => Colors.transparent),
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
@@ -100,8 +93,7 @@ class AddressPage extends BasePage {
}
@override
Widget middle(BuildContext context) =>
PresentReceiveOptionPicker(
Widget middle(BuildContext context) => PresentReceiveOptionPicker(
receiveOptionViewModel: receiveOptionViewModel,
hasWhiteBackground: currentTheme.type == ThemeType.light,
);
@@ -136,10 +128,7 @@ class AddressPage extends BasePage {
icon: Icon(
Icons.share,
size: 20,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
),
),
);
@@ -180,10 +169,7 @@ class AddressPage extends BasePage {
tapOutsideToDismiss: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context)
.accentTextTheme!
.bodyLarge!
.backgroundColor!,
keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
@@ -205,62 +191,54 @@ class AddressPage extends BasePage {
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
ThemeType.light))),
Observer(builder: (_) {
return addressListViewModel.hasAddressList
? GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
child: Container(
height: 50,
padding: EdgeInsets.only(left: 24, right: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(25)),
border: Border.all(
color: Theme.of(context)
.textTheme!
.titleMedium!
.color!,
width: 1),
color: Theme.of(context)
.textTheme!
.titleLarge!
.backgroundColor!),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Observer(
builder: (_) => Text(
addressListViewModel.hasAccounts
? S.of(context).accounts_subaddresses
: S.of(context).addresses,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!),
)),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
)
],
),
),
)
: Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.backgroundColor!));
if (addressListViewModel.hasAddressList) {
return GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
child: Container(
height: 50,
padding: EdgeInsets.only(left: 24, right: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(25)),
border: Border.all(
color: Theme.of(context).textTheme.titleMedium!.color!, width: 1),
color: Theme.of(context).textTheme.titleLarge!.backgroundColor!),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Observer(
builder: (_) => Text(
addressListViewModel.hasAccounts
? S.of(context).accounts_subaddresses
: S.of(context).addresses,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.accentTextTheme.displayMedium!
.backgroundColor!),
)),
Icon(
Icons.arrow_forward_ios,
size: 14,
color:
Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
)
],
),
),
);
} else if (addressListViewModel.showElectrumAddressDisclaimer) {
return Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color:
Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!));
} else {
return const SizedBox();
}
})
],
),
@@ -1,8 +1,8 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@@ -20,51 +20,78 @@ class BalancePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
child: SingleChildScrollView(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(height: 56),
Container(
onLongPress: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing =
!dashboardViewModel.balanceViewModel.isReversing,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 56),
Container(
margin: const EdgeInsets.only(left: 24, bottom: 16),
child: Observer(builder: (_) {
return Text(dashboardViewModel.balanceViewModel.asset,
style: TextStyle(
fontSize: 24,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
height: 1),
maxLines: 1,
textAlign: TextAlign.center);
})),
Observer(builder: (_) {
if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) {
return IntroducingCard(
title: S.of(context).introducing_cake_pay,
subTitle: S.of(context).cake_pay_learn_more,
borderColor: settingsStore.currentTheme.type == ThemeType.bright
? Color.fromRGBO(255, 255, 255, 0.2)
: Colors.transparent,
closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard);
}
return Container();
}),
Observer(builder: (_) {
return ListView.separated(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)),
itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length,
itemBuilder: (__, index) {
final balance =
dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
return buildBalanceRow(context,
child: Observer(
builder: (_) {
return Row(
children: [
Text(
dashboardViewModel.balanceViewModel.asset,
style: TextStyle(
fontSize: 24,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.center,
),
if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled)
InkWell(
onTap: () => Navigator.pushNamed(context, Routes.homeSettings,
arguments: dashboardViewModel.balanceViewModel),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset(
'assets/images/home_screen_settings_icon.png',
color:
Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
),
),
),
],
);
},
),
),
Observer(
builder: (_) {
if (dashboardViewModel.balanceViewModel.isShowCard &&
FeatureFlag.isCakePayEnabled) {
return IntroducingCard(
title: S.of(context).introducing_cake_pay,
subTitle: S.of(context).cake_pay_learn_more,
borderColor: settingsStore.currentTheme.type == ThemeType.bright
? Color.fromRGBO(255, 255, 255, 0.2)
: Colors.transparent,
closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard);
}
return Container();
},
),
Observer(
builder: (_) {
return ListView.separated(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)),
itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length,
itemBuilder: (__, index) {
final balance =
dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
return buildBalanceRow(
context,
availableBalanceLabel:
'${dashboardViewModel.balanceViewModel.availableBalanceLabel}',
availableBalance: balance.availableBalance,
@@ -75,45 +102,57 @@ class BalancePage extends StatelessWidget {
additionalFiatBalance: balance.fiatAdditionalBalance,
frozenBalance: balance.frozenBalance,
frozenFiatBalance: balance.fiatFrozenBalance,
currency: balance.formattedAssetTitle);
});
})
])));
currency: balance.formattedAssetTitle,
hasAdditionalBalance:
dashboardViewModel.balanceViewModel.hasAdditionalBalance,
);
},
);
},
)
],
),
),
);
}
Widget buildBalanceRow(BuildContext context,
{required String availableBalanceLabel,
required String availableBalance,
required String availableFiatBalance,
required String additionalBalanceLabel,
required String additionalBalance,
required String additionalFiatBalance,
required String frozenBalance,
required String frozenFiatBalance,
required String currency}) {
Widget buildBalanceRow(
BuildContext context, {
required String availableBalanceLabel,
required String availableBalance,
required String availableFiatBalance,
required String additionalBalanceLabel,
required String additionalBalance,
required String additionalFiatBalance,
required String frozenBalance,
required String frozenFiatBalance,
required String currency,
required bool hasAdditionalBalance,
}) {
return Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0),
border: Border.all(
color: settingsStore.currentTheme.type == ThemeType.bright
? Color.fromRGBO(255, 255, 255, 0.2)
: Colors.transparent,
width: 1,
),
color: Theme.of(context).textTheme!.titleLarge!.backgroundColor!),
borderRadius: BorderRadius.circular(30.0),
border: Border.all(
color: settingsStore.currentTheme.type == ThemeType.bright
? Color.fromRGBO(255, 255, 255, 0.2)
: Colors.transparent,
width: 1,
),
color: Theme.of(context).textTheme.titleLarge!.backgroundColor!,
),
child: Container(
margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showBalanceDescription(context),
onTap: hasAdditionalBalance ? () => _showBalanceDescription(context) : null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -129,19 +168,19 @@ class BalancePage extends StatelessWidget {
.displaySmall!
.backgroundColor!,
height: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.backgroundColor!),
)
if (hasAdditionalBalance)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.help_outline,
size: 16,
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.backgroundColor!),
),
],
),SizedBox(
height: 6,
),
SizedBox(height: 6),
AutoSizeText(availableBalance,
style: TextStyle(
fontSize: 24,
@@ -154,9 +193,7 @@ class BalancePage extends StatelessWidget {
height: 1),
maxLines: 1,
textAlign: TextAlign.start),
SizedBox(
height: 6,
),
SizedBox(height: 6),
Text('${availableFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
@@ -168,7 +205,6 @@ class BalancePage extends StatelessWidget {
.displayMedium!
.backgroundColor!,
height: 1)),
],
),
),
@@ -177,97 +213,99 @@ class BalancePage extends StatelessWidget {
fontSize: 28,
fontFamily: 'Lato',
fontWeight: FontWeight.w800,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
color: Theme.of(context).accentTextTheme!.displayMedium!.backgroundColor!,
height: 1)),
],
),
SizedBox(height: 26),
if (frozenBalance.isNotEmpty)
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(S.current.frozen_balance,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 26),
Text(
S.current.frozen_balance,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.backgroundColor!,
height: 1)),
SizedBox(height: 8),
AutoSizeText(frozenBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
height: 1),
maxLines: 1,
textAlign: TextAlign.center),
SizedBox(height: 4),
Text(
frozenFiatBalance,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
height: 1),
),
SizedBox(height: 24)
]),
Text('${additionalBalanceLabel}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displaySmall!
.backgroundColor!,
height: 1)),
SizedBox(height: 8),
AutoSizeText(additionalBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
height: 1),
maxLines: 1,
textAlign: TextAlign.center),
SizedBox(
height: 4,
),
Text(
'${additionalFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context)
.accentTextTheme!
.displayMedium!
.backgroundColor!,
height: 1),
)
])),
color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!,
height: 1,
),
),
SizedBox(height: 8),
AutoSizeText(
frozenBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.center,
),
SizedBox(height: 4),
Text(
frozenFiatBalance,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
height: 1,
),
),
],
),
if (hasAdditionalBalance)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Text(
'${additionalBalanceLabel}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!,
height: 1,
),
),
SizedBox(height: 8),
AutoSizeText(
additionalBalance,
style: TextStyle(
fontSize: 20,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
height: 1,
),
maxLines: 1,
textAlign: TextAlign.center,
),
SizedBox(height: 4),
Text(
'${additionalFiatBalance}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
height: 1,
),
),
],
),
],
),
),
);
}
@@ -19,17 +19,18 @@ class MenuWidget extends StatefulWidget {
class MenuWidgetState extends State<MenuWidget> {
MenuWidgetState()
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png');
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png'),
this.ethereumIcon = Image.asset('assets/images/eth_icon.png');
final largeScreen = 731;
@@ -46,6 +47,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image bitcoinIcon;
Image litecoinIcon;
Image havenIcon;
Image ethereumIcon;
@override
void initState() {
@@ -85,16 +87,14 @@ class MenuWidgetState extends State<MenuWidget> {
moneroIcon = Image.asset('assets/images/monero_menu.png',
color: Theme.of(context)
.accentTextTheme!
.accentTextTheme
.labelSmall!
.decorationColor!);
bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png',
color: Theme.of(context)
.accentTextTheme!
.accentTextTheme
.labelSmall!
.decorationColor!);
litecoinIcon = Image.asset('assets/images/litecoin_menu.png');
havenIcon = Image.asset('assets/images/haven_menu.png');
return Row(
mainAxisSize: MainAxisSize.max,
@@ -178,7 +178,7 @@ class MenuWidgetState extends State<MenuWidget> {
index--;
final item = SettingActions.all[index];
final isLastTile = index == itemCount - 1;
return SettingActionButton(
@@ -215,6 +215,8 @@ class MenuWidgetState extends State<MenuWidget> {
return litecoinIcon;
case WalletType.haven:
return havenIcon;
case WalletType.ethereum:
return ethereumIcon;
default:
throw Exception('No icon for ${type.toString()}');
}
@@ -511,17 +511,16 @@ class ExchangeCardState extends State<ExchangeCard> {
void _presentPicker(BuildContext context) {
showPopUp<void>(
builder: (_) => CurrencyPicker(
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
items: widget.currencies,
hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) =>
widget.onCurrencySelected != null
? widget.onCurrencySelected(item as CryptoCurrency)
: null),
context: context);
context: context,
builder: (_) => CurrencyPicker(
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
items: widget.currencies,
hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
),
);
}
void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) {
@@ -11,14 +11,17 @@ import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
class NewWalletTypePage extends BasePage {
NewWalletTypePage({required this.onTypeSelected});
NewWalletTypePage({required this.onTypeSelected, required this.isCreate});
final void Function(BuildContext, WalletType) onTypeSelected;
final bool isCreate;
final walletTypeImage = Image.asset('assets/images/wallet_type.png');
final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png');
@override
String get title => S.current.wallet_list_restore_wallet;
String get title =>
isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet;
@override
Widget body(BuildContext context) => WalletTypeForm(
@@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
class RestoreWalletOptionsPage extends BasePage {
RestoreWalletOptionsPage(
{required this.type,
required this.onRestoreFromSeed,
required this.onRestoreFromKeys});
final WalletType type;
final Function(BuildContext context) onRestoreFromSeed;
final Function(BuildContext context) onRestoreFromKeys;
@override
String get title => S.current.restore_restore_wallet;
final imageSeed = Image.asset('assets/images/restore_seed.png');
final imageKeys = Image.asset('assets/images/restore_keys.png');
@override
Widget body(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
padding: EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
RestoreButton(
onPressed: () => onRestoreFromSeed(context),
image: imageSeed,
title: S.of(context).restore_title_from_seed,
description: _fromSeedDescription(context)),
Padding(
padding: EdgeInsets.only(top: 24),
child: RestoreButton(
onPressed: () => onRestoreFromKeys(context),
image: imageKeys,
title: _fromKeyTitle(context),
description: _fromKeyDescription(context)),
)
],
),
));
}
String _fromSeedDescription(BuildContext context) {
switch (type) {
case WalletType.monero:
return S.of(context).restore_description_from_seed;
case WalletType.bitcoin:
// TODO: Add transaction for bitcoin description.
return S.of(context).restore_bitcoin_description_from_seed;
default:
return '';
}
}
String _fromKeyDescription(BuildContext context) {
switch (type) {
case WalletType.monero:
return S.of(context).restore_description_from_keys;
case WalletType.bitcoin:
// TODO: Add transaction for bitcoin description.
return S.of(context).restore_bitcoin_description_from_keys;
default:
return '';
}
}
String _fromKeyTitle(BuildContext context) {
switch (type) {
case WalletType.monero:
return S.of(context).restore_title_from_keys;
case WalletType.bitcoin:
// TODO: Add transaction for bitcoin description.
return S.of(context).restore_bitcoin_title_from_keys;
default:
return '';
}
}
}
+12 -3
View File
@@ -11,9 +11,7 @@ class PreSeedPage extends BasePage {
PreSeedPage(this.type)
: imageLight = Image.asset('assets/images/pre_seed_light.png'),
imageDark = Image.asset('assets/images/pre_seed_dark.png'),
wordsCount = type == WalletType.monero
? 25
: 24; // FIXME: Stupid fast implementation
wordsCount = _wordsCount(type);
final Image imageDark;
final Image imageLight;
@@ -68,4 +66,15 @@ class PreSeedPage extends BasePage {
),
));
}
static int _wordsCount(WalletType type) {
switch (type) {
case WalletType.monero:
return 25;
case WalletType.ethereum:
return 12;
default:
return 24;
}
}
}
+98 -104
View File
@@ -220,115 +220,108 @@ class SendPage extends BasePage {
),
),
),
if (sendViewModel.hasMultiRecipient)
Container(
height: 40,
width: double.infinity,
padding: EdgeInsets.only(left: 24),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Observer(
builder: (_) {
final templates = sendViewModel.templates;
final itemCount = templates.length;
Container(
height: 40,
width: double.infinity,
padding: EdgeInsets.only(left: 24),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Observer(
builder: (_) {
final templates = sendViewModel.templates;
final itemCount = templates.length;
return Row(
children: <Widget>[
AddTemplateButton(
onTap: () => Navigator.of(context)
.pushNamed(Routes.sendTemplate),
currentTemplatesLength: templates.length,
),
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (context, index) {
final template = templates[index];
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients:
template.additionalRecipients !=
null &&
template.additionalRecipients!
.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
from: template.isCurrencySelected
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
if (template.additionalRecipients !=
null) {
sendViewModel.clearOutputs();
return Row(
children: <Widget>[
AddTemplateButton(
onTap: () => Navigator.of(context)
.pushNamed(Routes.sendTemplate),
currentTemplatesLength: templates.length,
),
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: itemCount,
itemBuilder: (context, index) {
final template = templates[index];
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients:
template.additionalRecipients != null &&
template.additionalRecipients!.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
from: template.isCurrencySelected
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
if (template.additionalRecipients?.isNotEmpty ?? false) {
sendViewModel.clearOutputs();
template.additionalRecipients!
.forEach((currentElement) async {
int i = template
.additionalRecipients!
.indexOf(currentElement);
for (int i = 0;i < template.additionalRecipients!.length;i++) {
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate(
context,
output: output,
template: currentElement);
});
} else {
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template);
await _setInputsFromTemplate(
context,
output: output,
template: template.additionalRecipients![i],
);
}
},
onRemove: () {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle:
S.of(context).template,
alertContent: S
.of(context)
.confirm_delete_template,
rightButtonText:
S.of(context).delete,
leftButtonText:
S.of(context).cancel,
actionRightButton: () {
Navigator.of(dialogContext)
.pop();
sendViewModel
.sendTemplateViewModel
.removeTemplate(
template: template);
},
actionLeftButton: () =>
Navigator.of(dialogContext)
.pop());
},
} else {
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template,
);
},
);
},
),
],
);
},
),
}
},
onRemove: () {
showPopUp<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle:
S.of(context).template,
alertContent: S
.of(context)
.confirm_delete_template,
rightButtonText:
S.of(context).delete,
leftButtonText:
S.of(context).cancel,
actionRightButton: () {
Navigator.of(dialogContext)
.pop();
sendViewModel
.sendTemplateViewModel
.removeTemplate(
template: template);
},
actionLeftButton: () =>
Navigator.of(dialogContext)
.pop());
},
);
},
);
},
),
],
);
},
),
)
),
),
],
),
),
@@ -350,7 +343,7 @@ class SendPage extends BasePage {
.displaySmall!
.decorationColor!,
))),
if (sendViewModel.hasMultiRecipient)
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
@@ -518,6 +511,7 @@ class SendPage extends BasePage {
output.address = template.address;
if (template.isCurrencySelected) {
sendViewModel.setSelectedCryptoCurrency(template.cryptoCurrency);
output.setCryptoAmount(template.amount);
} else {
sendViewModel.setFiatCurrency(fiatFromTemplate);
+28 -43
View File
@@ -67,8 +67,7 @@ class SendTemplatePage extends BasePage {
controller: controller,
itemCount: sendTemplateViewModel.recipients.length,
itemBuilder: (_, index) {
final template =
sendTemplateViewModel.recipients[index];
final template = sendTemplateViewModel.recipients[index];
return SendTemplateCard(
template: template,
index: index,
@@ -76,8 +75,7 @@ class SendTemplatePage extends BasePage {
});
})),
Padding(
padding: EdgeInsets.only(
top: 10, left: 24, right: 24, bottom: 10),
padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
@@ -107,55 +105,42 @@ class SendTemplatePage extends BasePage {
),
),
])),
bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: [
// if (sendViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () {
sendTemplateViewModel.addRecipient();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(
sendTemplateViewModel.recipients.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context)
.accentTextTheme
.displaySmall!
.decorationColor!,
isDottedBorder: true,
borderColor: Theme.of(context)
.primaryTextTheme
.displaySmall!
.decorationColor!)),
if (sendTemplateViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () {
sendTemplateViewModel.addRecipient();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(sendTemplateViewModel.recipients.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context).accentTextTheme.displaySmall!.decorationColor!,
isDottedBorder: true,
borderColor:
Theme.of(context).primaryTextTheme.displaySmall!.decorationColor!)),
PrimaryButton(
onPressed: () {
if (_formKey.currentState != null &&
_formKey.currentState!.validate()) {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
final mainTemplate = sendTemplateViewModel.recipients[0];
print(sendTemplateViewModel.recipients.map((element) =>
element.toTemplate(
cryptoCurrency:
sendTemplateViewModel.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency)));
final additionalRecipients = sendTemplateViewModel.recipients
.map((element) => element.toTemplate(
cryptoCurrency: element.selectedCurrency.title,
fiatCurrency: sendTemplateViewModel.fiatCurrency))
.toList();
sendTemplateViewModel.addTemplate(
isCurrencySelected: mainTemplate.isCurrencySelected,
name: mainTemplate.name,
address: mainTemplate.address,
cryptoCurrency: mainTemplate.selectedCurrency.title,
amount: mainTemplate.output.cryptoAmount,
amountFiat: mainTemplate.output.fiatAmount,
additionalRecipients: sendTemplateViewModel.recipients
.map((element) => element.toTemplate(
cryptoCurrency: sendTemplateViewModel
.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency))
.toList());
additionalRecipients: additionalRecipients);
Navigator.of(context).pop();
}
},
@@ -4,29 +4,53 @@ class PrefixCurrencyIcon extends StatelessWidget {
PrefixCurrencyIcon({
required this.isSelected,
required this.title,
this.onTap,
});
final bool isSelected;
final String title;
final Function()? onTap;
@override
Widget build(BuildContext context) {
return Padding(
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.fromLTRB(0, 6.0, 8.0, 0),
child: Column(children: [
Container(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
color: isSelected ? Colors.green : Colors.transparent,
child: Column(
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
color: isSelected ? Colors.green : Colors.transparent,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (onTap != null)
Padding(
padding: EdgeInsets.only(right: 5),
child: Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
),
),
Text(
title + ':',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
child: Text(title + ':',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
)),
)
]));
],
),
),
);
}
}
+386 -363
View File
@@ -1,7 +1,10 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
@@ -32,18 +35,14 @@ class SendCard extends StatefulWidget {
@override
SendCardState createState() => SendCardState(
output: output,
sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest,
);
output: output,
sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest,
);
}
class SendCardState extends State<SendCard>
with AutomaticKeepAliveClientMixin<SendCard> {
SendCardState({
required this.output,
required this.sendViewModel,
this.initialPaymentRequest})
class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> {
SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest})
: addressController = TextEditingController(),
cryptoAmountController = TextEditingController(),
fiatAmountController = TextEditingController(),
@@ -100,40 +99,41 @@ class SendCardState extends State<SendCard>
return Stack(
children: [
KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context)
.accentTextTheme
.bodyLarge!
.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
KeyboardActionsItem(
focusNode: fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
)
]),
child: Container(
height: 0,
color: Colors.transparent,
)),
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
KeyboardActionsItem(
focusNode: fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
)
],
),
child: Container(
height: 0,
color: Colors.transparent,
),
),
Container(
decoration: ResponsiveLayoutUtil.instance.isMobile ? BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context)
.primaryTextTheme
.titleMedium!
.decorationColor!,
], begin: Alignment.topLeft, end: Alignment.bottomRight),
) : null,
decoration: ResponsiveLayoutUtil.instance.isMobile
? BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
)
: null,
child: Padding(
padding: EdgeInsets.fromLTRB(
24,
@@ -142,7 +142,8 @@ class SendCardState extends State<SendCard>
ResponsiveLayoutUtil.instance.isMobile ? 32 : 0,
),
child: SingleChildScrollView(
child: Observer(builder: (_) => Column(
child: Observer(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Observer(builder: (_) {
@@ -164,25 +165,15 @@ class SendCardState extends State<SendCard>
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!,
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
color:
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
onPushPasteButton: (context) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
@@ -197,170 +188,192 @@ class SendCardState extends State<SendCard>
selectedCurrency: sendViewModel.currency,
);
}),
if (output.isParsedAddress) Padding(
padding: const EdgeInsets.only(top: 20),
child: BaseTextFormField(
controller: extractedAddressController,
readOnly: true,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
validator: sendViewModel.addressValidator
)
),
if (output.isParsedAddress)
Padding(
padding: const EdgeInsets.only(top: 20),
child: BaseTextFormField(
controller: extractedAddressController,
readOnly: true,
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
validator: sendViewModel.addressValidator)),
Observer(
builder: (_) => Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Text(
sendViewModel.selectedCryptoCurrency.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
)),
sendViewModel.selectedCryptoCurrency.tag != null ? Padding(
padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0),
child: Container(
height: 32,
decoration: BoxDecoration(
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderRadius:
BorderRadius.all(Radius.circular(6))),
child: Center(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text( sendViewModel.selectedCryptoCurrency.tag!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.decorationColor!)),
builder: (_) => Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
sendViewModel.hasMultipleTokens
? Container(
padding: EdgeInsets.only(right: 8),
height: 32,
child: InkWell(
onTap: () => _presentPicker(context),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 5),
child: Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
),
),
Text(
sendViewModel.selectedCryptoCurrency.title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white),
),
],
),
),
),
),
) : Container(),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(':',
)
: Text(
sendViewModel.selectedCryptoCurrency.title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white)),
),
],
),
),
Expanded(
child: Stack(
children: [
BaseTextFormField(
focusNode: cryptoAmountFocus,
controller: cryptoAmountController,
keyboardType:
TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
],
suffixIcon: SizedBox(
width: prefixIconWidth,
),
hintText: '0.0000',
borderColor: Colors.transparent,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
),
sendViewModel.selectedCryptoCurrency.tag != null
? Padding(
padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0),
child: Container(
height: 32,
decoration: BoxDecoration(
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderRadius: BorderRadius.all(
Radius.circular(6),
)),
child: Center(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(
sendViewModel.selectedCryptoCurrency.tag!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.decorationColor!),
),
),
),
),
)
: Container(),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(
':',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white),
),
),
],
),
),
Expanded(
child: Stack(
children: [
BaseTextFormField(
focusNode: cryptoAmountFocus,
controller: cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
],
suffixIcon: SizedBox(
width: prefixIconWidth,
),
hintText: '0.0000',
borderColor: Colors.transparent,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: output.sendAll
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending)
Positioned(
top: 2,
right: 0,
child: Container(
width: prefixIconWidth,
height: prefixIconHeight,
child: InkWell(
onTap: () async => output.setSendAll(),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: output.sendAll
? sendViewModel.allAmountValidator
: sendViewModel
.amountValidator),
if (!sendViewModel.isBatchSending) Positioned(
top: 2,
right: 0,
child: Container(
width: prefixIconWidth,
height: prefixIconHeight,
child: InkWell(
onTap: () async =>
output.setSendAll(),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderRadius:
BorderRadius.all(
Radius.circular(6))),
child: Center(
child: Text(
S.of(context).all,
textAlign:
TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.bold,
color:
Theme.of(context)
.primaryTextTheme
.headlineMedium!
.decorationColor!))),
))))]),
.headlineMedium!
.color!,
borderRadius: BorderRadius.all(
Radius.circular(6),
),
),
child: Center(
child: Text(
S.of(context).all,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.decorationColor!,
),
),
),
),
),
),
),
],
),
],
)
)),
Divider(height: 1,color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
),
],
)),
),
Divider(
height: 1,
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
Observer(
builder: (_) => Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
S.of(context).available_balance +
':',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
)),
Text(
sendViewModel.balance,
builder: (_) => Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
S.of(context).available_balance + ':',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
@@ -368,10 +381,22 @@ class SendCardState extends State<SendCard>
.primaryTextTheme
.headlineSmall!
.decorationColor!),
)
],
),
)),
),
),
Text(
sendViewModel.balance,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
)
],
),
),
),
if (!sendViewModel.isFiatDisabled)
Padding(
padding: const EdgeInsets.only(top: 20),
@@ -379,171 +404,155 @@ class SendCardState extends State<SendCard>
focusNode: fiatAmountFocus,
controller: fiatAmountController,
keyboardType:
TextInputType.numberWithOptions(
signed: false, decimal: true),
TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'),
)
],
prefixIcon: Padding(
padding: EdgeInsets.only(top: 9),
child:
Text(sendViewModel.fiat.title + ':',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
)),
child: Text(
sendViewModel.fiat.title + ':',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme.headlineSmall!.decorationColor!,
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
)),
),
),
Padding(
padding: EdgeInsets.only(top: 20),
child: BaseTextFormField(
controller: noteController,
keyboardType: TextInputType.multiline,
maxLines: null,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintText: S.of(context).note_optional,
placeholderTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
color:
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
),
),
Observer(
builder: (_) => GestureDetector(
onTap: () =>
_setTransactionPriority(context),
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S
.of(context)
.send_estimated_fee,
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w500,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: Colors.white)),
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
output
.estimatedFee
.toString() +
' ' +
sendViewModel
.selectedCryptoCurrency.toString(),
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w600,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color:
Colors.white)),
Padding(
padding:
EdgeInsets.only(top: 5),
child: sendViewModel.isFiatDisabled
? const SizedBox(height: 14)
: Text(output
.estimatedFeeFiatAmount
+ ' ' +
sendViewModel
.fiat.title,
style: TextStyle(
fontSize: 12,
fontWeight:
FontWeight.w600,
color: Theme
.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!))
builder: (_) => GestureDetector(
onTap: () => _setTransactionPriority(context),
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
S.of(context).send_estimated_fee,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: Colors.white),
),
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
output.estimatedFee.toString() +
' ' +
sendViewModel.selectedCryptoCurrency.toString(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: Colors.white,
),
],
),
Padding(
padding: EdgeInsets.only(
top: 2,
left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
)
],
),
)
Padding(
padding: EdgeInsets.only(top: 5),
child: sendViewModel.isFiatDisabled
? const SizedBox(height: 14)
: Text(
output.estimatedFeeFiatAmount +
' ' +
sendViewModel.fiat.title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
),
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 2, left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
)
],
),
)
],
),
),
),
),
if (sendViewModel.isElectrumWallet)
Padding(
padding: EdgeInsets.only(top: 6),
child: GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList),
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).coin_control,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
],
),
),
)),
if (sendViewModel.isElectrumWallet) Padding(
padding: EdgeInsets.only(top: 6),
child: GestureDetector(
onTap: () => Navigator.of(context)
.pushNamed(Routes.unspentCoinsList),
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).coin_control,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white)),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
)
],
)
)
)
)
),
),
],
))
),
),
),
),
)
@@ -552,10 +561,10 @@ class SendCardState extends State<SendCard>
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
if (_effectsInstalled) {
return;
}
if (output.address.isNotEmpty) {
addressController.text = output.address;
}
@@ -664,16 +673,30 @@ class SendCardState extends State<SendCard>
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
await showPopUp<void>(
builder: (_) => Picker(
items: items,
displayItem: sendViewModel.displayFeeRate,
selectedAtIndex: selectedItem,
title: S.of(context).please_select,
mainAxisAlignment: MainAxisAlignment.center,
onItemSelected: (TransactionPriority priority) =>
sendViewModel.setTransactionPriority(priority),
),
context: context);
context: context,
builder: (_) => Picker(
items: items,
displayItem: sendViewModel.displayFeeRate,
selectedAtIndex: selectedItem,
title: S.of(context).please_select,
mainAxisAlignment: MainAxisAlignment.center,
onItemSelected: (TransactionPriority priority) =>
sendViewModel.setTransactionPriority(priority),
),
);
}
void _presentPicker(BuildContext context) {
showPopUp<void>(
context: context,
builder: (_) => CurrencyPicker(
selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
items: sendViewModel.currencies,
hintText: S.of(context).search_currency,
onItemSelected: (Currency cur) =>
sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency),
),
);
}
@override
@@ -1,6 +1,10 @@
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -35,161 +39,140 @@ class SendTemplateCard extends StatelessWidget {
_setEffects(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
], begin: Alignment.topLeft, end: Alignment.bottomRight)),
child: Column(children: <Widget>[
decoration: BoxDecoration(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
], begin: Alignment.topLeft, end: Alignment.bottomRight)),
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(children: <Widget>[
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(
children: <Widget>[
if (index == 0)
BaseTextFormField(
controller: _nameController,
hintText: sendTemplateViewModel.recipients.length > 1
? S.of(context).template_name
: S.of(context).send_name,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.templateValidator),
Padding(
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
selectedCurrency: sendTemplateViewModel.cryptoCurrency,
controller: _addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
validator: sendTemplateViewModel.addressValidator)),
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
selectedCurrency: sendTemplateViewModel.cryptoCurrency,
controller: _addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!,
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
),
validator: sendTemplateViewModel.addressValidator,
),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.cryptoCurrency.title,
isSelected: template.isCurrencySelected)),
hintText: '0.0000',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.amountValidator))),
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: template.selectedCurrency.title,
isSelected: template.isCurrencySelected,
onTap: sendTemplateViewModel.walletCurrencies.length > 1
? () => _presentPicker(context)
: null,
),
),
hintText: '0.0000',
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.amountValidator,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel.fiatCurrency,
isSelected: template.isFiatSelected)),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14))))
]))
]));
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel.fiatCurrency,
isSelected: template.isFiatSelected)),
hintText: '0.00',
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
),
),
],
),
)
],
),
);
}
void _setEffects(BuildContext context) {
@@ -264,4 +247,16 @@ class SendTemplateCard extends StatelessWidget {
_effectsInstalled = true;
}
void _presentPicker(BuildContext context) {
showPopUp<void>(
context: context,
builder: (_) => CurrencyPicker(
selectedAtIndex: sendTemplateViewModel.walletCurrencies.indexOf(template.selectedCurrency),
items: sendTemplateViewModel.walletCurrencies,
hintText: S.of(context).search_currency,
onItemSelected: (Currency cur) => template.changeSelectedCurrency(cur as CryptoCurrency),
),
);
}
}
+9 -1
View File
@@ -40,7 +40,8 @@ class PrivacyPage extends BasePage {
title: S.current.exchange,
items: ExchangeApiMode.all,
selectedItem: _privacySettingsViewModel.exchangeStatus,
onItemSelected: (ExchangeApiMode mode) => _privacySettingsViewModel.setExchangeApiMode(mode),
onItemSelected: (ExchangeApiMode mode) =>
_privacySettingsViewModel.setExchangeApiMode(mode),
),
),
SettingsSwitcherCell(
@@ -68,6 +69,13 @@ class PrivacyPage extends BasePage {
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
}),
if (_privacySettingsViewModel.canUseEtherscan)
SettingsSwitcherCell(
title: S.current.etherscan_history,
value: _privacySettingsViewModel.useEtherscan,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUseEtherscan(value);
}),
],
);
}),
@@ -3,14 +3,23 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
class SettingsSwitcherCell extends StandardListRow {
SettingsSwitcherCell(
{required String title, required this.value, this.onValueChange})
: super(title: title, isSelected: false);
SettingsSwitcherCell({
required String title,
required this.value,
this.onValueChange,
Decoration? decoration,
this.leading,
void Function(BuildContext context)? onTap,
}) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap);
final bool value;
final void Function(BuildContext context, bool value)? onValueChange;
final Widget? leading;
@override
Widget buildTrailing(BuildContext context) => StandardSwitch(
value: value, onTaped: () => onValueChange?.call(context, !value));
Widget buildTrailing(BuildContext context) =>
StandardSwitch(value: value, onTaped: () => onValueChange?.call(context, !value));
@override
Widget? buildLeading(BuildContext context) => leading;
}
@@ -46,6 +46,7 @@ class WalletListBodyState extends State<WalletListBody> {
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24);
final scrollController = ScrollController();
final double tileHeight = 60;
Flushbar<void>? _progressBar;
@@ -230,6 +231,8 @@ class WalletListBodyState extends State<WalletListBody> {
return litecoinIcon;
case WalletType.haven:
return havenIcon;
case WalletType.ethereum:
return ethereumIcon;
default:
return nonWalletTypeIcon;
}
+26 -41
View File
@@ -1,13 +1,8 @@
import 'dart:ui';
import 'package:cake_wallet/palette.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CheckboxWidget extends StatefulWidget {
CheckboxWidget({
required this.value,
required this.caption,
required this.onChanged});
CheckboxWidget({required this.value, required this.caption, required this.onChanged});
final bool value;
final String caption;
@@ -26,55 +21,45 @@ class CheckboxWidgetState extends State<CheckboxWidget> {
@override
Widget build(BuildContext context) {
return GestureDetector(
return InkWell(
onTap: () {
value = !value;
onChanged(value);
setState(() {});
},
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
height: 16,
width: 16,
height: 24.0,
width: 24.0,
margin: EdgeInsets.only(right: 10.0),
decoration: BoxDecoration(
color: value
? Palette.blueCraiola
: Theme.of(context)
.accentTextTheme!
.titleMedium!
.decorationColor!,
borderRadius: BorderRadius.all(Radius.circular(2)),
border: Border.all(
color: value
? Palette.blueCraiola
: Theme.of(context)
.accentTextTheme!
.labelSmall!
.color!,
width: 1)),
child: value
? Center(
child: Icon(
Icons.done,
color: Colors.white,
size: 14,
border: Border.all(
color: Theme.of(context).primaryTextTheme.bodySmall!.color!,
width: 1.0,
),
)
: Offstage(),
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
color: Theme.of(context).colorScheme.background,
),
child: value
? Icon(
Icons.check,
color: Colors.blue,
size: 20.0,
)
: null,
),
Padding(
padding: EdgeInsets.only(left: 16),
Expanded(
child: Text(
caption,
style: TextStyle(
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
decoration: TextDecoration.none
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
),
),
)
@@ -82,4 +67,4 @@ class CheckboxWidgetState extends State<CheckboxWidget> {
),
);
}
}
}
+49 -44
View File
@@ -1,5 +1,7 @@
// ignore_for_file: deprecated_member_use
import 'dart:math';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/material.dart';
@@ -145,8 +147,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.color!,
child: ConstrainedBox(
constraints: BoxConstraints(
@@ -163,8 +164,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
),
Divider(
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.backgroundColor!,
height: 1,
),
@@ -194,8 +194,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
fontFamily: 'Lato',
decoration: TextDecoration.none,
color: Theme.of(context)
.primaryTextTheme!
.titleLarge!
.primaryTextTheme.titleLarge!
.color!,
),
),
@@ -217,8 +216,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
Widget itemsList() {
return Container(
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.backgroundColor!,
child: widget.isGridView
? GridView.builder(
@@ -240,8 +238,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
separatorBuilder: (context, index) => widget.isSeparated
? Divider(
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.backgroundColor!,
height: 1,
)
@@ -254,15 +251,9 @@ class _PickerState<Item> extends State<Picker<Item>> {
Widget buildItem(int index) {
final item = filteredItems[index];
final tag = item is Currency ? item.tag : null;
final icon = item is Currency && item.iconPath != null
? Image.asset(
item.iconPath!,
height: 20.0,
width: 20.0,
)
: null;
final tag = item is Currency ? item.tag : null;
final icon = _getItemIcon(item);
final image = images.isNotEmpty ? filteredImages[index] : icon;
@@ -274,8 +265,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
child: Container(
height: 55,
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.color!,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Row(
@@ -298,8 +288,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme!
.titleLarge!
.primaryTextTheme.titleLarge!
.color!,
decoration: TextDecoration.none,
),
@@ -318,8 +307,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
fontSize: 7.0,
fontFamily: 'Lato',
color: Theme.of(context)
.textTheme!
.bodyMedium!
.textTheme.bodyMedium!
.color!),
),
),
@@ -327,8 +315,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context)
.textTheme!
.bodyMedium!
.textTheme.bodyMedium!
.decorationColor!,
),
),
@@ -345,15 +332,9 @@ class _PickerState<Item> extends State<Picker<Item>> {
Widget buildSelectedItem(int index) {
final item = items[index];
final tag = item is Currency ? item.tag : null;
final icon = item is Currency && item.iconPath != null
? Image.asset(
item.iconPath!,
height: 20.0,
width: 20.0,
)
: null;
final tag = item is Currency ? item.tag : null;
final icon = _getItemIcon(item);
final image = images.isNotEmpty ? images[index] : icon;
@@ -364,8 +345,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
child: Container(
height: 55,
color: Theme.of(context)
.accentTextTheme!
.titleLarge!
.accentTextTheme.titleLarge!
.color!,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Row(
@@ -388,8 +368,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
fontFamily: 'Lato',
fontWeight: FontWeight.w700,
color: Theme.of(context)
.primaryTextTheme!
.titleLarge!
.primaryTextTheme.titleLarge!
.color!,
decoration: TextDecoration.none,
),
@@ -408,8 +387,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
fontSize: 7.0,
fontFamily: 'Lato',
color: Theme.of(context)
.textTheme!
.bodyMedium!
.textTheme.bodyMedium!
.color!),
),
),
@@ -417,8 +395,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context)
.textTheme!
.bodyMedium!
.textTheme.bodyMedium!
.decorationColor!,
),
),
@@ -429,12 +406,40 @@ class _PickerState<Item> extends State<Picker<Item>> {
),
Icon(Icons.check_circle,
color: Theme.of(context)
.accentTextTheme!
.bodyLarge!
.accentTextTheme.bodyLarge!
.color!),
],
),
),
);
}
Widget? _getItemIcon(Item item) {
if (item is Currency) {
if (item.iconPath != null) {
return Image.asset(
item.iconPath!,
height: 20.0,
width: 20.0,
);
} else {
return Container(
height: 20.0,
width: 20.0,
child: Center(
child: Text(
item.name.substring(0, min(item.name.length, 2)).toUpperCase(),
style: TextStyle(fontSize: 11),
),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade400,
),
);
}
}
return null;
}
}
+7 -4
View File
@@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
class StandardListRow extends StatelessWidget {
StandardListRow(
{required this.title, required this.isSelected, this.onTap});
{required this.title, required this.isSelected, this.onTap, this.decoration});
final String title;
final bool isSelected;
final void Function(BuildContext context)? onTap;
final Decoration? decoration;
@override
Widget build(BuildContext context) {
@@ -19,9 +20,11 @@ class StandardListRow extends StatelessWidget {
return InkWell(
onTap: () => onTap?.call(context),
child: Container(
color: _backgroundColor(context),
height: 56,
padding: EdgeInsets.only(left: 24, right: 24),
decoration: decoration ?? BoxDecoration(
color: _backgroundColor(context),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
@@ -54,7 +57,7 @@ class StandardListRow extends StatelessWidget {
Color titleColor(BuildContext context) => isSelected
? Palette.blueCraiola
: Theme.of(context).primaryTextTheme!.titleLarge!.color!;
: Theme.of(context).primaryTextTheme.titleLarge!.color!;
Color _backgroundColor(BuildContext context) {
return Theme.of(context).colorScheme.background;
@@ -89,7 +92,7 @@ class StandardListSeparator extends StatelessWidget {
child: Container(
height: height,
color: Theme.of(context)
.primaryTextTheme!
.primaryTextTheme
.titleLarge
?.backgroundColor));
}
+79 -2
View File
@@ -5,7 +5,9 @@ import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/themes/theme_list.dart';
@@ -66,10 +68,14 @@ abstract class SettingsStoreBase with Store {
required bool initialShouldRequireTOTP2FAForAddingContacts,
required bool initialShouldRequireTOTP2FAForCreatingNewWallets,
required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
required this.sortBalanceBy,
required this.pinNativeTokenAtTop,
required this.useEtherscan,
TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority,
TransactionPriority? initialLitecoinTransactionPriority})
TransactionPriority? initialLitecoinTransactionPriority,
TransactionPriority? initialEthereumTransactionPriority})
: nodes = ObservableMap<WalletType, Node>.of(nodes),
_sharedPreferences = sharedPreferences,
fiatCurrency = initialFiatCurrency,
@@ -120,6 +126,10 @@ abstract class SettingsStoreBase with Store {
priority[WalletType.litecoin] = initialLitecoinTransactionPriority;
}
if (initialEthereumTransactionPriority != null) {
priority[WalletType.ethereum] = initialEthereumTransactionPriority;
}
reaction(
(_) => fiatCurrency,
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
@@ -145,6 +155,9 @@ abstract class SettingsStoreBase with Store {
case WalletType.haven:
key = PreferencesKey.havenTransactionPriority;
break;
case WalletType.ethereum:
key = PreferencesKey.ethereumTransactionPriority;
break;
default:
key = null;
}
@@ -279,6 +292,21 @@ abstract class SettingsStoreBase with Store {
(ExchangeApiMode mode) =>
sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, mode.serialize()));
reaction(
(_) => sortBalanceBy,
(SortBalanceBy sortBalanceBy) =>
_sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceBy.index));
reaction(
(_) => pinNativeTokenAtTop,
(bool pinNativeTokenAtTop) =>
_sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop));
reaction(
(_) => useEtherscan,
(bool useEtherscan) =>
_sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan));
this.nodes.observe((change) {
if (change.newValue != null && change.key != null) {
_saveCurrentNode(change.newValue!, change.key!);
@@ -385,6 +413,15 @@ abstract class SettingsStoreBase with Store {
@observable
ObservableMap<WalletType, TransactionPriority> priority;
@observable
SortBalanceBy sortBalanceBy;
@observable
bool pinNativeTokenAtTop;
@observable
bool useEtherscan;
String appVersion;
String deviceName;
@@ -429,6 +466,7 @@ abstract class SettingsStoreBase with Store {
TransactionPriority? havenTransactionPriority;
TransactionPriority? litecoinTransactionPriority;
TransactionPriority? ethereumTransactionPriority;
if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) {
havenTransactionPriority = monero?.deserializeMoneroTransactionPriority(
@@ -438,11 +476,16 @@ abstract class SettingsStoreBase with Store {
litecoinTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority(
sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!);
}
if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) {
ethereumTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority(
sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!);
}
moneroTransactionPriority ??= monero?.getDefaultTransactionPriority();
bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority();
havenTransactionPriority ??= monero?.getDefaultTransactionPriority();
litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium();
ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority();
final currentBalanceDisplayMode = BalanceDisplayMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
@@ -502,6 +545,12 @@ abstract class SettingsStoreBase with Store {
final pinCodeTimeOutDuration = timeOutDuration != null
? PinCodeRequiredDuration.deserialize(raw: timeOutDuration)
: defaultPinCodeTimeOutDuration;
final sortBalanceBy =
SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0];
final pinNativeTokenAtTop =
sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true;
final useEtherscan =
sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
// If no value
if (pinLength == null || pinLength == 0) {
@@ -516,10 +565,12 @@ abstract class SettingsStoreBase with Store {
final litecoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey);
final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final havenNode = nodeSource.get(havenNodeId);
final ethereumNode = nodeSource.get(ethereumNodeId);
final packageInfo = await PackageInfo.fromPlatform();
final deviceName = await _getDeviceName() ?? '';
final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@@ -542,6 +593,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.haven] = havenNode;
}
if (ethereumNode != null) {
nodes[WalletType.ethereum] = ethereumNode;
}
return SettingsStore(
sharedPreferences: sharedPreferences,
initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard,
@@ -567,6 +622,9 @@ abstract class SettingsStoreBase with Store {
initialPinLength: pinLength,
pinTimeOutDuration: pinCodeTimeOutDuration,
initialLanguageCode: savedLanguageCode,
sortBalanceBy: sortBalanceBy,
pinNativeTokenAtTop: pinNativeTokenAtTop,
useEtherscan: useEtherscan,
initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority,
initialHavenTransactionPriority: havenTransactionPriority,
@@ -582,6 +640,7 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets,
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
initialEthereumTransactionPriority: ethereumTransactionPriority,
shouldShowYatPopup: shouldShowYatPopup);
}
@@ -608,6 +667,11 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!) ??
priority[WalletType.litecoin]!;
}
if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) {
priority[WalletType.ethereum] = ethereum?.deserializeEthereumTransactionPriority(
sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ??
priority[WalletType.ethereum]!;
}
balanceDisplayMode = BalanceDisplayMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
@@ -616,7 +680,7 @@ abstract class SettingsStoreBase with Store {
shouldSaveRecipientAddress;
totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey;
useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA;
numberOfFailedTokenTrials =
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ??
@@ -677,6 +741,10 @@ abstract class SettingsStoreBase with Store {
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
sortBalanceBy = SortBalanceBy
.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index];
pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true;
useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId =
@@ -684,10 +752,12 @@ abstract class SettingsStoreBase with Store {
final litecoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey);
final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final havenNode = nodeSource.get(havenNodeId);
final ethereumNode = nodeSource.get(ethereumNodeId);
if (moneroNode != null) {
nodes[WalletType.monero] = moneroNode;
@@ -704,6 +774,10 @@ abstract class SettingsStoreBase with Store {
if (havenNode != null) {
nodes[WalletType.haven] = havenNode;
}
if (ethereumNode != null) {
nodes[WalletType.ethereum] = ethereumNode;
}
}
Future<void> _saveCurrentNode(Node node, WalletType walletType) async {
@@ -722,6 +796,9 @@ abstract class SettingsStoreBase with Store {
case WalletType.haven:
await _sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, node.key as int);
break;
case WalletType.ethereum:
await _sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, node.key as int);
break;
default:
break;
}
+1
View File
@@ -144,6 +144,7 @@ class ExceptionHandler {
"Connection closed before full header was received",
"Connection terminated during handshake",
"PERMISSION_NOT_GRANTED",
"Failed host lookup: ",
];
static Future<void> _addDeviceInfo(File file) async {
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_info.dart';
@@ -57,11 +58,15 @@ abstract class ContactListViewModelBase with Store {
@computed
List<ContactRecord> get contactsToShow => contacts
.where((element) => _currency == null || element.type == _currency)
.where((element) => _isValidForCurrency(element))
.toList();
@computed
List<WalletContact> get walletContactsToShow => walletContacts
.where((element) => _currency == null || element.type == _currency)
.where((element) => _isValidForCurrency(element))
.toList();
bool _isValidForCurrency(ContactBase element) {
return _currency == null || element.type == _currency || element.type.title == _currency!.tag;
}
}
@@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/balance.dart';
@@ -79,6 +80,15 @@ abstract class BalanceViewModelBase with Store {
@computed
bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
@computed
bool get isHomeScreenSettingsEnabled => wallet.type == WalletType.ethereum;
@computed
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
@computed
bool get pinNativeToken => settingsStore.pinNativeTokenAtTop;
@computed
String get asset {
final typeFormatted = walletTypeToString(appStore.wallet!.type);
@@ -109,6 +119,7 @@ abstract class BalanceViewModelBase with Store {
switch(wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.ethereum:
return S.current.xmr_available_balance;
default:
return S.current.confirmed;
@@ -120,6 +131,7 @@ abstract class BalanceViewModelBase with Store {
switch(wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.ethereum:
return S.current.xmr_full_balance;
default:
return S.current.unconfirmed;
@@ -262,32 +274,58 @@ abstract class BalanceViewModelBase with Store {
});
}
@computed
bool get hasAdditionalBalance => wallet.type != WalletType.ethereum;
@computed
List<BalanceRecord> get formattedBalances {
final balance = balances.values.toList();
balance.sort((BalanceRecord a, BalanceRecord b) {
if (b.asset == CryptoCurrency.xhv) {
return 1;
}
if (b.asset == CryptoCurrency.xusd) {
if (a.asset == CryptoCurrency.xhv) {
return -1;
if (wallet.currency == CryptoCurrency.xhv) {
if (b.asset == CryptoCurrency.xhv) {
return 1;
}
return 1;
if (b.asset == CryptoCurrency.xusd) {
if (a.asset == CryptoCurrency.xhv) {
return -1;
}
return 1;
}
if (b.asset == CryptoCurrency.xbtc) {
return 1;
}
if (b.asset == CryptoCurrency.xeur) {
return 1;
}
return 0;
}
if (b.asset == CryptoCurrency.xbtc) {
return 1;
if (pinNativeToken) {
if (b.asset == wallet.currency) return 1;
if (a.asset == wallet.currency) return -1;
}
if (b.asset == CryptoCurrency.xeur) {
return 1;
}
switch (sortBalanceBy) {
case SortBalanceBy.FiatBalance:
final aFiatBalance = _getFiatBalance(
price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
final bFiatBalance = _getFiatBalance(
price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
return 0;
return (double.tryParse(bFiatBalance) ?? 0)
.compareTo((double.tryParse(aFiatBalance)) ?? 0);
case SortBalanceBy.GrossBalance:
return (double.tryParse(b.availableBalance) ?? 0)
.compareTo(double.tryParse(a.availableBalance) ?? 0);
case SortBalanceBy.Alphabetical:
return a.asset.title.compareTo(b.asset.title);
}
});
return balance;
@@ -335,7 +373,7 @@ abstract class BalanceViewModelBase with Store {
}
String _getFiatBalance({required double price, String? cryptoAmount}) {
if (cryptoAmount == null || cryptoAmount.isEmpty) {
if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
return '0.00';
}
@@ -0,0 +1,121 @@
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:mobx/mobx.dart';
part 'home_settings_view_model.g.dart';
class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel;
abstract class HomeSettingsViewModelBase with Store {
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
: tokens = ObservableSet<Erc20Token>() {
_updateTokensList();
}
final SettingsStore _settingsStore;
final BalanceViewModel _balanceViewModel;
final ObservableSet<Erc20Token> tokens;
@observable
String searchText = '';
@computed
SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy;
@action
void setSortBalanceBy(SortBalanceBy value) {
_settingsStore.sortBalanceBy = value;
_updateTokensList();
}
@computed
bool get pinNativeToken => _settingsStore.pinNativeTokenAtTop;
@action
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
Future<void> addErc20Token(Erc20Token token) async {
await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
_updateTokensList();
_updateFiatPrices(token);
}
Future<void> deleteErc20Token(Erc20Token token) async {
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token);
_updateTokensList();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress);
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
void _updateFiatPrices(Erc20Token token) async {
try {
_balanceViewModel.fiatConvertationStore.prices[token] =
await FiatConversionService.fetchPrice(
crypto: token,
fiat: _settingsStore.fiatCurrency,
torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly);
} catch (_) {}
}
void changeTokenAvailability(Erc20Token token, bool value) async {
token.enabled = value;
ethereum!.addErc20Token(_balanceViewModel.wallet, token);
_refreshTokensList();
}
@action
void _updateTokensList() {
int _sortFunc(Erc20Token e1, Erc20Token e2) {
int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
if (e1.enabled && !e2.enabled) {
return -1;
} else if (e2.enabled && !e1.enabled) {
return 1;
} else if (!e1.enabled && !e2.enabled) { // if both are disabled then sort alphabetically
return e1.name.compareTo(e2.name);
}
return index1.compareTo(index2);
}
tokens.clear();
tokens.addAll(ethereum!
.getERC20Currencies(_balanceViewModel.wallet)
.where((element) => _matchesSearchText(element))
.toList()
..sort(_sortFunc));
}
@action
void _refreshTokensList() {
final _tokens = Set.of(tokens);
tokens.clear();
tokens.addAll(_tokens);
}
@action
void changeSearchText(String text) {
searchText = text;
_updateTokensList();
}
bool _matchesSearchText(Erc20Token asset) {
return searchText.isEmpty ||
asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
asset.contractAddress == searchText;
}
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
@@ -84,6 +85,13 @@ class TransactionListItem extends ActionListItem with Keyable {
cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount),
price: price);
break;
case WalletType.ethereum:
final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction);
final price = balanceViewModel.fiatConvertationStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction),
price: price);
break;
default:
break;
}
@@ -699,6 +699,10 @@ abstract class ExchangeViewModelBase with Store {
depositCurrency = CryptoCurrency.xhv;
receiveCurrency = CryptoCurrency.btc;
break;
case WalletType.ethereum:
depositCurrency = CryptoCurrency.eth;
receiveCurrency = CryptoCurrency.xmr;
break;
default:
break;
}
@@ -63,6 +63,9 @@ abstract class NodeListViewModelBase with Store {
case WalletType.haven:
node = getHavenDefaultNode(nodes: _nodeSource)!;
break;
case WalletType.ethereum:
node = getEthereumDefaultNode(nodes: _nodeSource)!;
break;
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
}
+13 -1
View File
@@ -2,6 +2,7 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -90,6 +91,9 @@ abstract class OutputBase with Store {
case WalletType.haven:
_amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.ethereum:
_amount = ethereum!.formatterEthereumParseAmount(_cryptoAmount);
break;
default:
break;
}
@@ -123,6 +127,10 @@ abstract class OutputBase with Store {
if (_wallet.type == WalletType.haven) {
return haven!.formatterMoneroAmountToDouble(amount: fee);
}
if (_wallet.type == WalletType.ethereum) {
return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee));
}
} catch (e) {
print(e.toString());
}
@@ -133,8 +141,9 @@ abstract class OutputBase with Store {
@computed
String get estimatedFeeFiatAmount {
try {
final currency = _wallet.type == WalletType.ethereum ? _wallet.currency : cryptoCurrencyHandler();
final fiat = calculateFiatAmountRaw(
price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!,
price: _fiatConversationStore.prices[currency]!,
cryptoAmount: estimatedFee);
return fiat;
} catch (_) {
@@ -228,6 +237,9 @@ abstract class OutputBase with Store {
case WalletType.haven:
maximumFractionDigits = 12;
break;
case WalletType.ethereum:
maximumFractionDigits = 12;
break;
default:
break;
}
@@ -13,8 +13,7 @@ import 'package:cake_wallet/store/settings_store.dart';
part 'send_template_view_model.g.dart';
class SendTemplateViewModel = SendTemplateViewModelBase
with _$SendTemplateViewModel;
class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewModel;
abstract class SendTemplateViewModelBase with Store {
final WalletBase _wallet;
@@ -22,8 +21,8 @@ abstract class SendTemplateViewModelBase with Store {
final SendTemplateStore _sendTemplateStore;
final FiatConversionStore _fiatConversationStore;
SendTemplateViewModelBase(this._wallet, this._settingsStore,
this._sendTemplateStore, this._fiatConversationStore)
SendTemplateViewModelBase(
this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore)
: recipients = ObservableList<TemplateViewModel>() {
addRecipient();
}
@@ -33,7 +32,6 @@ abstract class SendTemplateViewModelBase with Store {
@action
void addRecipient() {
recipients.add(TemplateViewModel(
cryptoCurrency: cryptoCurrency,
wallet: _wallet,
settingsStore: _settingsStore,
fiatConversationStore: _fiatConversationStore));
@@ -47,11 +45,13 @@ abstract class SendTemplateViewModelBase with Store {
AmountValidator get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
AddressValidator get addressValidator =>
AddressValidator(type: _wallet.currency);
AddressValidator get addressValidator => AddressValidator(type: _wallet.currency);
TemplateValidator get templateValidator => TemplateValidator();
bool get hasMultiRecipient =>
_wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum;
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;
@@ -68,6 +68,7 @@ abstract class SendTemplateViewModelBase with Store {
void addTemplate(
{required String name,
required bool isCurrencySelected,
required String cryptoCurrency,
required String address,
required String amount,
required String amountFiat,
@@ -76,7 +77,7 @@ abstract class SendTemplateViewModelBase with Store {
name: name,
isCurrencySelected: isCurrencySelected,
address: address,
cryptoCurrency: cryptoCurrency.title,
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
amountFiat: amountFiat,
@@ -89,4 +90,7 @@ abstract class SendTemplateViewModelBase with Store {
_sendTemplateStore.remove(template: template);
updateTemplate();
}
@computed
List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList();
}
+37 -12
View File
@@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.dart';
@@ -45,6 +46,7 @@ abstract class SendViewModelBase with Store {
: state = InitialExecutionState(),
currencies = _wallet.balance.keys.toList(),
selectedCryptoCurrency = _wallet.currency,
hasMultipleTokens = _wallet.type == WalletType.ethereum,
outputs = ObservableList<Output>(),
fiatFromSettings = _settingsStore.fiatCurrency {
final priority = _settingsStore.priority[_wallet.type];
@@ -105,8 +107,11 @@ abstract class SendViewModelBase with Store {
String get pendingTransactionFeeFiatAmount {
try {
if (pendingTransaction != null) {
final currency = walletType == WalletType.ethereum
? _wallet.currency
: selectedCryptoCurrency;
final fiat = calculateFiatAmount(
price: _fiatConversationStore.prices[selectedCryptoCurrency]!,
price: _fiatConversationStore.prices[currency]!,
cryptoAmount: pendingTransaction!.feeFormatted);
return fiat;
} else {
@@ -131,14 +136,14 @@ abstract class SendViewModelBase with Store {
CryptoCurrency get currency => _wallet.currency;
Validator get amountValidator =>
Validator<String> get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
Validator get allAmountValidator => AllAmountValidator();
Validator<String> get allAmountValidator => AllAmountValidator();
Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency);
Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);
Validator get textValidator => TextValidator();
Validator<String> get textValidator => TextValidator();
final FiatCurrency fiatFromSettings;
@@ -146,7 +151,7 @@ abstract class SendViewModelBase with Store {
PendingTransaction? pendingTransaction;
@computed
String get balance => balanceViewModel.availableBalance;
String get balance => _wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance;
@computed
bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
@@ -176,10 +181,9 @@ abstract class SendViewModelBase with Store {
List<CryptoCurrency> currencies;
bool get hasMultiRecipient => _wallet.type != WalletType.haven;
bool get hasYat => outputs
.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
bool get hasYat => outputs.any((out) =>
out.isParsedAddress &&
out.parsedAddress.parseFrom == ParseFrom.yatRecord);
WalletType get walletType => _wallet.type;
@@ -198,6 +202,7 @@ abstract class SendViewModelBase with Store {
final ContactListViewModel contactListViewModel;
final FiatConversionStore _fiatConversationStore;
final Box<TransactionDescription> transactionDescriptionBox;
final bool hasMultipleTokens;
@computed
List<ContactRecord> get contactsToShow => contactListViewModel.contacts
@@ -351,6 +356,15 @@ abstract class SendViewModelBase with Store {
return haven!.createHavenTransactionCreationCredentials(
outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title);
case WalletType.ethereum:
final priority = _settingsStore.priority[_wallet.type];
if (priority == null) {
throw Exception('Priority is null for wallet type: ${_wallet.type}');
}
return ethereum!.createEthereumTransactionCredentials(
outputs, priority: priority, currency: selectedCryptoCurrency);
default:
throw Exception('Unexpected wallet type: ${_wallet.type}');
}
@@ -369,11 +383,22 @@ abstract class SendViewModelBase with Store {
}
bool _isEqualCurrency(String currency) =>
currency.toLowerCase() == _wallet.currency.title.toLowerCase();
_wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());
@action
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
@action
void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;
void setFiatCurrency(FiatCurrency fiat) =>
_settingsStore.fiatCurrency = fiat;
@action
void setSelectedCryptoCurrency(String cryptoCurrency) {
try {
selectedCryptoCurrency = _wallet.balance.keys
.firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase());
} catch (e) {
selectedCryptoCurrency = _wallet.currency;
}
}
}
+21 -13
View File
@@ -11,23 +11,20 @@ part 'template_view_model.g.dart';
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
abstract class TemplateViewModelBase with Store {
final CryptoCurrency cryptoCurrency;
final WalletBase _wallet;
final SettingsStore _settingsStore;
final FiatConversionStore _fiatConversationStore;
TemplateViewModelBase(
{required this.cryptoCurrency,
required WalletBase wallet,
required SettingsStore settingsStore,
required FiatConversionStore fiatConversationStore})
: _wallet = wallet,
TemplateViewModelBase({
required WalletBase wallet,
required SettingsStore settingsStore,
required FiatConversionStore fiatConversationStore,
}) : _wallet = wallet,
_settingsStore = settingsStore,
_fiatConversationStore = fiatConversationStore,
output = Output(wallet, settingsStore, fiatConversationStore,
() => wallet.currency) {
output = Output(
_wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
_currency = wallet.currency,
output = Output(wallet, settingsStore, fiatConversationStore, () => wallet.currency) {
output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _currency);
}
@observable
@@ -39,6 +36,9 @@ abstract class TemplateViewModelBase with Store {
@observable
String address = '';
@observable
CryptoCurrency _currency;
@observable
bool isCurrencySelected = true;
@@ -66,8 +66,7 @@ abstract class TemplateViewModelBase with Store {
output.reset();
}
Template toTemplate(
{required String cryptoCurrency, required String fiatCurrency}) {
Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) {
return Template(
isCurrencySelectedRaw: isCurrencySelected,
nameRaw: name,
@@ -77,4 +76,13 @@ abstract class TemplateViewModelBase with Store {
amountRaw: output.cryptoAmount,
amountFiatRaw: output.fiatAmount);
}
@action
void changeSelectedCurrency(CryptoCurrency currency) {
isCurrencySelected = true;
_currency = currency;
}
@computed
CryptoCurrency get selectedCurrency => _currency;
}
@@ -1,5 +1,8 @@
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
@@ -8,9 +11,10 @@ part 'privacy_settings_view_model.g.dart';
class PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel;
abstract class PrivacySettingsViewModelBase with Store {
PrivacySettingsViewModelBase(this._settingsStore);
PrivacySettingsViewModelBase(this._settingsStore, this._wallet);
final SettingsStore _settingsStore;
final WalletBase _wallet;
@computed
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
@@ -30,8 +34,14 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get disableSell => _settingsStore.disableSell;
@computed
bool get useEtherscan => _settingsStore.useEtherscan;
bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
@action
void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value;
void setShouldSaveRecipientAddress(bool value) =>
_settingsStore.shouldSaveRecipientAddress = value;
@action
void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value;
@@ -48,4 +58,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@action
void setDisableSell(bool value) => _settingsStore.disableSell = value;
@action
void setUseEtherscan(bool value) {
_settingsStore.useEtherscan = value;
ethereum!.updateEtherscanUsageState(_wallet, value);
}
}
@@ -9,6 +9,7 @@ import 'package:cw_core/transaction_direction.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:hive/hive.dart';
import 'package:intl/src/intl/date_format.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -27,105 +28,27 @@ abstract class TransactionDetailsViewModelBase with Store {
required this.wallet,
required this.settingsStore})
: items = [],
isRecipientAddressShown = false,
showRecipientAddress = settingsStore.shouldSaveRecipientAddress {
isRecipientAddressShown = false,
showRecipientAddress = settingsStore.shouldSaveRecipientAddress {
final dateFormat = DateFormatter.withCurrentLocal();
final tx = transactionInfo;
if (wallet.type == WalletType.monero) {
final key = tx.additionalInfo['key'] as String?;
final accountIndex = tx.additionalInfo['accountIndex'] as int;
final addressIndex = tx.additionalInfo['addressIndex'] as int;
final feeFormatted = tx.feeFormatted();
final _items = [
StandartListItem(
title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date,
value: dateFormat.format(tx.date)),
StandartListItem(
title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(
title: S.current.transaction_details_amount,
value: tx.amountFormatted()),
if (feeFormatted != null)
StandartListItem(
title: S.current.transaction_details_fee, value: feeFormatted),
if (key?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_key, value: key!)
];
if (tx.direction == TransactionDirection.incoming &&
accountIndex != null &&
addressIndex != null) {
try {
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
if (address?.isNotEmpty ?? false) {
isRecipientAddressShown = true;
_items.add(
StandartListItem(
title: S.current.transaction_details_recipient_address,
value: address));
}
if (label?.isNotEmpty ?? false) {
_items.add(
StandartListItem(
title: S.current.address_label,
value: label)
);
}
} catch (e) {
print(e.toString());
}
}
items.addAll(_items);
}
if (wallet.type == WalletType.bitcoin
|| wallet.type == WalletType.litecoin) {
final _items = [
StandartListItem(
title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date,
value: dateFormat.format(tx.date)),
StandartListItem(
title: S.current.confirmations,
value: tx.confirmations.toString()),
StandartListItem(
title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(
title: S.current.transaction_details_amount,
value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(
title: S.current.transaction_details_fee,
value: tx.feeFormatted()!),
];
items.addAll(_items);
}
if (wallet.type == WalletType.haven) {
items.addAll([
StandartListItem(
title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date,
value: dateFormat.format(tx.date)),
StandartListItem(
title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(
title: S.current.transaction_details_amount,
value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(
title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
]);
switch (wallet.type) {
case WalletType.monero:
_addMoneroListItems(tx, dateFormat);
break;
case WalletType.bitcoin:
case WalletType.litecoin:
_addElectrumListItems(tx, dateFormat);
break;
case WalletType.haven:
_addHavenListItems(tx, dateFormat);
break;
case WalletType.ethereum:
_addEthereumListItems(tx, dateFormat);
break;
default:
break;
}
if (showRecipientAddress && !isRecipientAddressShown) {
@@ -136,10 +59,9 @@ abstract class TransactionDetailsViewModelBase with Store {
if (recipientAddress?.isNotEmpty ?? false) {
items.add(StandartListItem(
title: S.current.transaction_details_recipient_address,
value: recipientAddress!));
title: S.current.transaction_details_recipient_address, value: recipientAddress!));
}
} catch(_) {
} catch (_) {
// FIX-ME: Unhandled exception
}
}
@@ -192,6 +114,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return 'https://blockchair.com/litecoin/transaction/${txId}';
case WalletType.haven:
return 'https://explorer.havenprotocol.org/search?value=${txId}';
case WalletType.ethereum:
return 'https://etherscan.io/tx/${txId}';
default:
return '';
}
@@ -207,8 +131,92 @@ abstract class TransactionDetailsViewModelBase with Store {
return S.current.view_transaction_on + 'Blockchair.com';
case WalletType.haven:
return S.current.view_transaction_on + 'explorer.havenprotocol.org';
case WalletType.ethereum:
return S.current.view_transaction_on + 'etherscan.io';
default:
return '';
}
}
void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) {
final key = tx.additionalInfo['key'] as String?;
final accountIndex = tx.additionalInfo['accountIndex'] as int;
final addressIndex = tx.additionalInfo['addressIndex'] as int;
final feeFormatted = tx.feeFormatted();
final _items = [
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (feeFormatted != null)
StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted),
if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!),
];
if (tx.direction == TransactionDirection.incoming) {
try {
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
if (address.isNotEmpty) {
isRecipientAddressShown = true;
_items.add(StandartListItem(
title: S.current.transaction_details_recipient_address,
value: address,
));
}
if (label.isNotEmpty) {
_items.add(StandartListItem(title: S.current.address_label, value: label));
}
} catch (e) {
print(e.toString());
}
}
items.addAll(_items);
}
void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) {
final _items = [
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
];
items.addAll(_items);
}
void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) {
items.addAll([
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
]);
}
void _addEthereumListItems(TransactionInfo tx, DateFormat dateFormat) {
final _items = [
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()),
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
];
items.addAll(_items);
}
}
@@ -1,3 +1,4 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/yat/yat_store.dart';
@@ -93,6 +94,22 @@ class LitecoinURI extends PaymentURI {
}
}
class EthereumURI extends PaymentURI {
EthereumURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'ethereum:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
abstract class WalletAddressListViewModelBase with Store {
WalletAddressListViewModelBase({
required AppStore appStore,
@@ -151,6 +168,10 @@ abstract class WalletAddressListViewModelBase with Store {
return LitecoinURI(amount: amount, address: address.address);
}
if (_wallet.type == WalletType.ethereum) {
return EthereumURI(amount: amount, address: address.address);
}
throw Exception('Unexpected type: ${type.toString()}');
}
@@ -202,6 +223,12 @@ abstract class WalletAddressListViewModelBase with Store {
addressList.addAll(bitcoinAddresses);
}
if (wallet.type == WalletType.ethereum) {
final primaryAddress = ethereum!.getAddress(wallet);
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
}
return addressList;
}
@@ -226,6 +253,10 @@ abstract class WalletAddressListViewModelBase with Store {
@computed
bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven;
@computed
bool get showElectrumAddressDisclaimer =>
_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin;
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
+6 -2
View File
@@ -17,7 +17,8 @@ class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel;
abstract class WalletKeysViewModelBase with Store {
WalletKeysViewModelBase(this._appStore)
: title = _appStore.wallet!.type == WalletType.bitcoin ||
_appStore.wallet!.type == WalletType.litecoin
_appStore.wallet!.type == WalletType.litecoin ||
_appStore.wallet!.type == WalletType.ethereum
? S.current.wallet_seed
: S.current.wallet_keys,
_restoreHeight = _appStore.wallet!.walletInfo.restoreHeight,
@@ -89,7 +90,8 @@ abstract class WalletKeysViewModelBase with Store {
}
if (_appStore.wallet!.type == WalletType.bitcoin ||
_appStore.wallet!.type == WalletType.litecoin) {
_appStore.wallet!.type == WalletType.litecoin ||
_appStore.wallet!.type == WalletType.ethereum) {
items.addAll([
StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed),
]);
@@ -116,6 +118,8 @@ abstract class WalletKeysViewModelBase with Store {
return 'litecoin-wallet';
case WalletType.haven:
return 'haven-wallet';
case WalletType.ethereum:
return 'ethereum-wallet';
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
}
+3
View File
@@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@@ -42,6 +43,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
case WalletType.haven:
return haven!.createHavenNewWalletCredentials(
name: name, language: options as String);
case WalletType.ethereum:
return ethereum!.createEthereumNewWalletCredentials(name: name);
default:
throw Exception('Unexpected type: ${type.toString()}');;
}
@@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/mnemonic_length.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@@ -85,6 +86,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
height: height,
mnemonic: seed,
password: password);
case WalletType.ethereum:
return ethereum!.createEthereumRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password);
default:
break;
}