mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-25 05:58:42 +01:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin {
|
||||
passphrase: passphrase,
|
||||
);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinWalletFromKeys({
|
||||
required String name,
|
||||
required String password,
|
||||
required String xpub,
|
||||
}) =>
|
||||
BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
|
||||
{required String name,
|
||||
@@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
|
||||
return <String, String>{
|
||||
'wif': keys.wif,
|
||||
'privateKey': keys.privateKey,
|
||||
'publicKey': keys.publicKey
|
||||
};
|
||||
return bitcoinWallet.keys.toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -126,7 +130,8 @@ class CWBitcoin extends Bitcoin {
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
memo: out.memo,
|
||||
extra: out.extra))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: bitcoinFeeRate,
|
||||
@@ -275,7 +280,14 @@ class CWBitcoin extends Bitcoin {
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
|
||||
List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
if (keys.privateKey.isEmpty) {
|
||||
return BitcoinReceivePageOption.allViewOnly;
|
||||
}
|
||||
return BitcoinReceivePageOption.all;
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReceivePageOption> getLitecoinReceivePageOptions() {
|
||||
@@ -727,6 +739,12 @@ class CWBitcoin extends Bitcoin {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commitPsbtUR(Object wallet, List<String> urCodes) {
|
||||
final _wallet = wallet as BitcoinWalletBase;
|
||||
return _wallet.commitPsbtUR(urCodes);
|
||||
}
|
||||
|
||||
@override
|
||||
String getPayjoinEndpoint(Object wallet) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'dart:async';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
class NodeSwitchingService {
|
||||
NodeSwitchingService({
|
||||
required this.appStore,
|
||||
required this.settingsStore,
|
||||
required this.nodeSource,
|
||||
});
|
||||
|
||||
static const int _healthCheckIntervalSeconds = 30;
|
||||
|
||||
Timer? _healthCheckTimer;
|
||||
|
||||
bool _isSwitching = false;
|
||||
bool get isSwitching => _isSwitching;
|
||||
|
||||
final AppStore appStore;
|
||||
final SettingsStore settingsStore;
|
||||
final Box<Node> nodeSource;
|
||||
|
||||
// Track used nodes to cycle through all trusted nodes
|
||||
final Map<WalletType, List<dynamic>> _usedNodeKeys = {};
|
||||
|
||||
void startHealthCheckTimer() {
|
||||
_healthCheckTimer?.cancel();
|
||||
_healthCheckTimer = Timer.periodic(
|
||||
Duration(seconds: _healthCheckIntervalSeconds),
|
||||
(_) => performHealthCheck(),
|
||||
);
|
||||
|
||||
performHealthCheck();
|
||||
}
|
||||
|
||||
void stopHealthCheckTimer() {
|
||||
_healthCheckTimer?.cancel();
|
||||
_healthCheckTimer = null;
|
||||
}
|
||||
|
||||
Future<void> performHealthCheck() async {
|
||||
if (appStore.wallet == null) return;
|
||||
|
||||
if (!settingsStore.enableAutomaticNodeSwitching) return;
|
||||
|
||||
if (_isSwitching) return;
|
||||
|
||||
try {
|
||||
final isHealthy = await appStore.wallet!.checkNodeHealth();
|
||||
|
||||
if (!isHealthy) {
|
||||
await _switchToNextTrustedNode();
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error during health check: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to the next available trusted node
|
||||
Future<void> _switchToNextTrustedNode() async {
|
||||
_isSwitching = true;
|
||||
|
||||
try {
|
||||
final walletType = appStore.wallet!.type;
|
||||
final currentNode = settingsStore.getCurrentNode(walletType);
|
||||
|
||||
// Get all trusted nodes for this wallet type
|
||||
final trustedNodes = nodeSource.values
|
||||
.where((node) => node.type == walletType && node.isEnabledForAutoSwitching)
|
||||
.toList();
|
||||
|
||||
if (trustedNodes.isEmpty) {
|
||||
printV('No trusted nodes available for switching');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize used nodes list for this wallet type if it does not exist
|
||||
_usedNodeKeys.putIfAbsent(walletType, () => []);
|
||||
|
||||
// Add current node to used list if not already there
|
||||
if (!_usedNodeKeys[walletType]!.contains(currentNode.key)) {
|
||||
_usedNodeKeys[walletType]!.add(currentNode.key);
|
||||
}
|
||||
|
||||
// Get next unused trusted node from the list
|
||||
Node? nextNode;
|
||||
for (final node in trustedNodes) {
|
||||
if (!_usedNodeKeys[walletType]!.contains(node.key)) {
|
||||
nextNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all trusted nodes have been used, reset the list and start over
|
||||
if (nextNode == null) {
|
||||
printV('All trusted nodes have been tried, resetting and starting over');
|
||||
_usedNodeKeys[walletType]!.clear();
|
||||
nextNode = trustedNodes.first;
|
||||
}
|
||||
|
||||
// Add the next node to used list
|
||||
_usedNodeKeys[walletType]!.add(nextNode.key);
|
||||
|
||||
printV('Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw}');
|
||||
printV('Used nodes for ${walletType}: ${_usedNodeKeys[walletType]}');
|
||||
|
||||
// Update the current node in settings
|
||||
settingsStore.nodes[walletType] = nextNode;
|
||||
|
||||
// Connect the wallet to the new node
|
||||
await appStore.wallet!.connectToNode(node: nextNode);
|
||||
|
||||
await appStore.wallet!.startSync();
|
||||
|
||||
printV('Successfully switched to node: ${nextNode.uriRaw}');
|
||||
} catch (e) {
|
||||
printV('Error switching to next trusted node: $e');
|
||||
} finally {
|
||||
_isSwitching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -23,6 +23,7 @@ import 'package:cake_wallet/core/totp_request_details.dart';
|
||||
import 'package:cake_wallet/core/wallet_creation_service.dart';
|
||||
import 'package:cake_wallet/core/wallet_loading_service.dart';
|
||||
import 'package:cake_wallet/core/yat_service.dart';
|
||||
import 'package:cake_wallet/core/node_switching_service.dart';
|
||||
import 'package:cake_wallet/entities/biometric_auth.dart';
|
||||
import 'package:cake_wallet/entities/contact.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
@@ -977,7 +978,7 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
|
||||
|
||||
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
||||
getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>((Map<String, String> urQr, _) =>
|
||||
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
||||
|
||||
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
|
||||
@@ -1516,5 +1517,11 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => DEuroSavingsPage(getIt<DEuroViewModel>()));
|
||||
|
||||
getIt.registerLazySingleton(() => NodeSwitchingService(
|
||||
appStore: getIt.get<AppStore>(),
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
nodeSource: _nodeSource,
|
||||
));
|
||||
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
|
||||
@@ -5,6 +8,7 @@ import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:dnssec_proof/dnssec_proof.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Bip353Record {
|
||||
@@ -22,6 +26,19 @@ class Bip353Record {
|
||||
'address': 'On-Chain Address',
|
||||
};
|
||||
|
||||
static Future<String?> fetchDnsProof(String bip353Name) async {
|
||||
if (bip353Name.startsWith('₿')) {
|
||||
bip353Name = bip353Name.substring(1);
|
||||
}
|
||||
final parts = bip353Name.split('@');
|
||||
if (parts.length != 2) return null;
|
||||
final userPart = parts[0];
|
||||
final domainPart = parts[1];
|
||||
final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
|
||||
final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
|
||||
return base64.encode(proof);
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?> fetchUriByCryptoCurrency(
|
||||
String bip353Name, String asset) async {
|
||||
try {
|
||||
@@ -30,7 +47,10 @@ class Bip353Record {
|
||||
|
||||
if (parts.length != 2) return null;
|
||||
|
||||
final userPart = parts[0];
|
||||
String userPart = parts[0];
|
||||
if (userPart.startsWith('₿')) {
|
||||
userPart = userPart.substring(1);
|
||||
}
|
||||
final domainPart = parts[1];
|
||||
|
||||
// 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
|
||||
@@ -42,6 +62,10 @@ class Bip353Record {
|
||||
RRecordType.TXT,
|
||||
dnssec: true,
|
||||
);
|
||||
final proof = await fetchDnsProof(bip353Name);
|
||||
if (proof == null) {
|
||||
throw Exception('DNSSEC proof not found');
|
||||
}
|
||||
|
||||
if (txtRecords == null) return null;
|
||||
|
||||
|
||||
@@ -510,7 +510,10 @@ Future<void> defaultSettingsMigration(
|
||||
providerName: "SwapTrade",
|
||||
enabled: true,
|
||||
);
|
||||
break;
|
||||
break;
|
||||
case 50:
|
||||
migrateExistingNodesToUseAutoSwitching(nodes: nodes, powNodes: powNodes);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1088,7 +1091,7 @@ Future<void> checkCurrentNodes(
|
||||
|
||||
if (currentBitcoinElectrumServer == null) {
|
||||
final cakeWalletElectrum =
|
||||
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
|
||||
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true);
|
||||
await nodeSource.add(cakeWalletElectrum);
|
||||
final cakeWalletElectrumTestnet =
|
||||
Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
|
||||
@@ -1188,7 +1191,7 @@ Future<void> resetBitcoinElectrumServer(
|
||||
|
||||
if (cakeWalletNode == null) {
|
||||
cakeWalletNode =
|
||||
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false);
|
||||
Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false, isEnabledForAutoSwitching: true);
|
||||
// final cakeWalletElectrumTestnet =
|
||||
// Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false);
|
||||
// await nodeSource.add(cakeWalletElectrumTestnet);
|
||||
@@ -1272,3 +1275,47 @@ Future<void> removeMoneroWorld(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateExistingNodesToUseAutoSwitching(
|
||||
{required Box<Node> nodes, required Box<Node> powNodes}) async {
|
||||
final listOfDefaultNodesWithAutoSwitching = [
|
||||
'bitcoincash.stackwallet.com:50002',
|
||||
'bch.aftrek.org:50002',
|
||||
'btc-electrum.cakewallet.com:50002',
|
||||
'fulcrum.sethforprivacy.com:50002',
|
||||
'default-spv-nodes',
|
||||
'dcrd.sethforprivacy.com:9108',
|
||||
'ethereum-rpc.publicnode.com',
|
||||
'eth.nownodes.io',
|
||||
'ltc-electrum.cakewallet.com:50002',
|
||||
'litecoin.stackwallet.com:20063',
|
||||
'nano.nownodes.io',
|
||||
'rpc.nano.to',
|
||||
'node.nautilus.io',
|
||||
'rpc.nano.to',
|
||||
'workers.perish.co',
|
||||
'worker.nanoriver.cc',
|
||||
'xmr-node.cakewallet.com:18081',
|
||||
'node.sethforprivacy.com:443',
|
||||
'nodes.hashvault.pro:18081',
|
||||
'polygon-bor-rpc.publicnode.com',
|
||||
'matic.nownodes.io',
|
||||
'api.mainnet-beta.solana.com:443',
|
||||
'solana-rpc.publicnode.com:443',
|
||||
'solana-mainnet.core.chainstack.com',
|
||||
'api.trongrid.io',
|
||||
'trx.nownodes.io',
|
||||
'node3.monerodevs.org:34568',
|
||||
'node2.monerodevs.org:34568',
|
||||
'37.27.100.59:10500',
|
||||
'zano.cakewallet.com:11211',
|
||||
'electrum.cakewallet.com:50002',
|
||||
];
|
||||
for (var node in [...nodes.values.toList(), ...powNodes.values.toList()]) {
|
||||
if (listOfDefaultNodesWithAutoSwitching.contains(node.uriRaw)) {
|
||||
node.isEnabledForAutoSwitching = true;
|
||||
await node.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -344,7 +344,13 @@ class AddressResolver {
|
||||
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
|
||||
if (chosenAddress != null) {
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
try {
|
||||
final dnsProof = await Bip353Record.fetchDnsProof(text);
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof);
|
||||
} catch (e) {
|
||||
printV('Bip353Record.fetchBip353AddressAddress error: $e');
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class ParsedAddress {
|
||||
this.profileImageUrl = '',
|
||||
this.profileName = '',
|
||||
this.parseFrom = ParseFrom.notParsed,
|
||||
this.bip353DnsProof,
|
||||
});
|
||||
|
||||
factory ParsedAddress.fetchEmojiAddress({
|
||||
@@ -59,11 +60,13 @@ class ParsedAddress {
|
||||
factory ParsedAddress.fetchBip353AddressAddress ({
|
||||
required String address,
|
||||
required String name,
|
||||
String? dnsProof,
|
||||
}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.bip353,
|
||||
bip353DnsProof: dnsProof,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,4 +181,5 @@ class ParsedAddress {
|
||||
final String profileImageUrl;
|
||||
final String profileName;
|
||||
final ParseFrom parseFrom;
|
||||
final String? bip353DnsProof;
|
||||
}
|
||||
|
||||
@@ -112,4 +112,5 @@ class PreferencesKey {
|
||||
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
|
||||
static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}';
|
||||
static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled';
|
||||
static const enableAutomaticNodeSwitching = 'enable_automatic_node_switching';
|
||||
}
|
||||
|
||||
+4
-1
@@ -6,6 +6,7 @@ import 'package:cake_wallet/app_scroll_behavior.dart';
|
||||
import 'package:cake_wallet/buy/order.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/core/background_sync.dart';
|
||||
import 'package:cake_wallet/core/node_switching_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/contact.dart';
|
||||
import 'package:cake_wallet/entities/default_settings_migration.dart';
|
||||
@@ -249,7 +250,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
havenSeedStore: havenSeedStore,
|
||||
initialMigrationVersion: 49,
|
||||
initialMigrationVersion: 50,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,6 +328,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
final authService = getIt.get<AuthService>();
|
||||
final linkViewModel = getIt.get<LinkViewModel>();
|
||||
final tradeMonitor = getIt.get<TradeMonitor>();
|
||||
final nodeSwitchingService = getIt.get<NodeSwitchingService>();
|
||||
final settingsStore = appStore.settingsStore;
|
||||
final statusBarColor = Colors.transparent;
|
||||
final authenticationStore = getIt.get<AuthenticationStore>();
|
||||
@@ -351,6 +353,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
|
||||
authService: authService,
|
||||
linkViewModel: linkViewModel,
|
||||
tradeMonitor: tradeMonitor,
|
||||
nodeSwitchingService: nodeSwitchingService,
|
||||
child: ThemeProvider(
|
||||
themeStore: appStore.themeStore,
|
||||
materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp(
|
||||
|
||||
@@ -399,9 +399,9 @@ class CWMonero extends Monero {
|
||||
}
|
||||
|
||||
@override
|
||||
String exportOutputsUR(Object wallet, bool all) {
|
||||
Map<String, String> exportOutputsUR(Object wallet) {
|
||||
final moneroWallet = wallet as MoneroWallet;
|
||||
return moneroWallet.exportOutputsUR(all);
|
||||
return moneroWallet.exportOutputsUR();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/store/authentication_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/core/node_switching_service.dart';
|
||||
|
||||
Future<void> bootstrapOffline() async {
|
||||
final authenticationStore = getIt.get<AuthenticationStore>();
|
||||
@@ -40,4 +41,6 @@ void bootstrapOnline(GlobalKey<NavigatorState> navigatorKey, {required bool load
|
||||
startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore);
|
||||
startOnCurrentNodeChangeReaction(appStore);
|
||||
startFiatRateUpdate(appStore, settingsStore, fiatConversionStore);
|
||||
|
||||
getIt.get<NodeSwitchingService>().startHealthCheckTimer();
|
||||
}
|
||||
@@ -197,9 +197,9 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
Platform.isIOS
|
||||
? S.of(context).connect_your_hardware_wallet_ios
|
||||
: S.of(context).connect_your_hardware_wallet,
|
||||
Platform.isAndroid
|
||||
? S.of(context).connect_your_hardware_wallet
|
||||
: S.of(context).connect_your_hardware_wallet_ios,
|
||||
style: Theme.of(context)
|
||||
.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -174,8 +174,12 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
|
||||
},
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
color: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
textColor: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Observer(
|
||||
@@ -183,18 +187,19 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
final trade = widget.exchangeTradeViewModel.trade;
|
||||
final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
return widget.exchangeTradeViewModel.isSendable &&
|
||||
!(sendingState is TransactionCommitted)
|
||||
? LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
|
||||
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
|
||||
isLoading: sendingState is IsExecutingState,
|
||||
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
|
||||
text: S.current.send_from_cake_wallet,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
: Offstage();
|
||||
return Offstage(
|
||||
offstage: !(widget.exchangeTradeViewModel.isSendable &&
|
||||
!(sendingState is TransactionCommitted)),
|
||||
child: LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
|
||||
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
|
||||
isLoading: sendingState is IsExecutingState,
|
||||
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
|
||||
text: S.current.send_from_cake_wallet,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -9,7 +9,9 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/tooltip_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
@@ -37,86 +39,71 @@ class DEuroSavingsPage extends BasePage {
|
||||
@override
|
||||
String get title => S.current.deuro_savings;
|
||||
|
||||
Widget trailing(BuildContext context) => MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: 37,
|
||||
width: 37,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: "Refresh",
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
|
||||
),
|
||||
onPressed: _dEuroViewModel.reloadSavingsUserData,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: pageIconColor(context),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, _dEuroViewModel));
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Observer(
|
||||
builder: (_) => SavingsCard(
|
||||
isDarkTheme: currentTheme.isDark,
|
||||
interestRate: "${_dEuroViewModel.interestRate}%",
|
||||
savingsBalance: _dEuroViewModel.savingsBalance,
|
||||
fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalance,
|
||||
currency: CryptoCurrency.deuro,
|
||||
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
|
||||
onAddSavingsPressed: () => _onSavingsAdd(context),
|
||||
onRemoveSavingsPressed: () => _onSavingsRemove(context),
|
||||
onApproveSavingsPressed: _dEuroViewModel.prepareApproval,
|
||||
onTooltipPressed: () => _onSavingsTooltipPressed(context),
|
||||
isEnabled: _dEuroViewModel.isEnabled,
|
||||
isLoading: _dEuroViewModel.isLoading,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => InterestCardWidget(
|
||||
isDarkTheme: currentTheme.isDark,
|
||||
title: S.of(context).deuro_savings_collect_interest,
|
||||
fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterest,
|
||||
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
|
||||
accruedInterest: _dEuroViewModel.accruedInterest,
|
||||
onCollectInterest: _dEuroViewModel.prepareCollectInterest,
|
||||
onReinvestInterest: _dEuroViewModel.prepareReinvestInterest,
|
||||
onTooltipPressed: () => _onInterestTooltipPressed(context),
|
||||
isEnabled: _dEuroViewModel.isEnabled,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 30),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InfoChip(
|
||||
label: S.of(context).deuro_about_deuro,
|
||||
icon: CupertinoIcons.info,
|
||||
onPressed: () => _showWelcomeTooltip(context),
|
||||
return RefreshIndicator(
|
||||
displacement: responsiveLayoutUtil.screenHeight * 0.1,
|
||||
onRefresh: _dEuroViewModel.reloadSavingsUserData,
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverFillRemaining(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Observer(
|
||||
builder: (_) => SavingsCard(
|
||||
isDarkTheme: currentTheme.isDark,
|
||||
interestRate: "${_dEuroViewModel.interestRateFormated}%",
|
||||
savingsBalance: _dEuroViewModel.savingsBalanceFormated,
|
||||
fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated,
|
||||
currency: CryptoCurrency.deuro,
|
||||
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
|
||||
onAddSavingsPressed: () => _onSavingsAdd(context),
|
||||
onRemoveSavingsPressed: () => _onSavingsRemove(context),
|
||||
onApproveSavingsPressed: _dEuroViewModel.prepareApproval,
|
||||
onTooltipPressed: () => _onSavingsTooltipPressed(context),
|
||||
isEnabled: _dEuroViewModel.isEnabled,
|
||||
isLoading: _dEuroViewModel.isLoading,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => InterestCardWidget(
|
||||
isDarkTheme: currentTheme.isDark,
|
||||
title: S.of(context).deuro_savings_collect_interest,
|
||||
fiatAccruedInterest: _dEuroViewModel.fiatAccruedInterestFormated,
|
||||
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
|
||||
accruedInterest: _dEuroViewModel.accruedInterestFormated,
|
||||
onCollectInterest: _onCollectInterest,
|
||||
onReinvestInterest: _onReinvestInterest,
|
||||
onTooltipPressed: () => _onInterestTooltipPressed(context),
|
||||
isEnabled: _dEuroViewModel.isSavingsActionsEnabled,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 30),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InfoChip(
|
||||
label: S.of(context).deuro_about_deuro,
|
||||
icon: CupertinoIcons.info,
|
||||
onPressed: () => _showWelcomeTooltip(context),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
InfoChip(
|
||||
label: S.of(context).website,
|
||||
icon: CupertinoIcons.globe,
|
||||
onPressed: () => launchUrlString("https://deuro.com/"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
InfoChip(
|
||||
label: S.of(context).website,
|
||||
icon: CupertinoIcons.globe,
|
||||
onPressed: () => launchUrlString("https://deuro.com/"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -126,14 +113,36 @@ class DEuroSavingsPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
bool _editSheetIsOpen = false;
|
||||
|
||||
Future<void> _onSavingsAdd(BuildContext context) async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
final amount = await _showEditBottomSheet(context, isAdding: true);
|
||||
if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true);
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _onSavingsRemove(BuildContext context) async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
final amount = await _showEditBottomSheet(context, isAdding: false);
|
||||
if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false);
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _onReinvestInterest() async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
await _dEuroViewModel.prepareReinvestInterest();
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _onCollectInterest() async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
await _dEuroViewModel.prepareCollectInterest();
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
bool _isReactionsSet = false;
|
||||
@@ -175,10 +184,10 @@ class DEuroSavingsPage extends BasePage {
|
||||
currency: CryptoCurrency.deuro,
|
||||
amount: S.of(bottomSheetContext).send_amount,
|
||||
amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
|
||||
? _dEuroViewModel.accruedInterest
|
||||
? _dEuroViewModel.accruedInterestFormated
|
||||
: tx.amountFormatted,
|
||||
fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
|
||||
? _dEuroViewModel.fiatAccruedInterest
|
||||
? _dEuroViewModel.fiatAccruedInterestFormated
|
||||
: _dEuroViewModel.pendingTransactionFiatAmountFormatted,
|
||||
fee: S.of(bottomSheetContext).send_estimated_fee,
|
||||
feeValue: tx.feeFormatted,
|
||||
@@ -301,11 +310,11 @@ class DEuroSavingsPage extends BasePage {
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
|
||||
builder: (BuildContext bottomSheetContext) => TooltipSheet(
|
||||
currentTheme: currentTheme,
|
||||
titleText: title,
|
||||
titleIconPath: CryptoCurrency.deuro.iconPath,
|
||||
content: content,
|
||||
tooltip: content,
|
||||
footerType: FooterType.doubleActionButton,
|
||||
doubleActionRightButtonText: S.of(context).close,
|
||||
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'),
|
||||
@@ -358,7 +367,8 @@ class DEuroSavingsPage extends BasePage {
|
||||
content: S.of(context).deuro_tooltip_no_eth,
|
||||
singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
|
||||
singleActionButtonText: S.of(context).close,
|
||||
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton,
|
||||
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
|
||||
footerType: FooterType.singleActionButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -375,7 +385,9 @@ class DEuroSavingsPage extends BasePage {
|
||||
balanceTitle: isAdding
|
||||
? S.of(context).deuro_savings_available_to_add
|
||||
: S.of(context).deuro_savings_available_to_remove,
|
||||
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
|
||||
balance: isAdding
|
||||
? _dEuroViewModel.accountBalanceFormated
|
||||
: _dEuroViewModel.savingsBalanceFormated,
|
||||
footerType: FooterType.none,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SavingsEditSheet extends BaseBottomSheet {
|
||||
@@ -14,7 +15,8 @@ class SavingsEditSheet extends BaseBottomSheet {
|
||||
super.titleIconPath,
|
||||
this.balance,
|
||||
this.balanceTitle,
|
||||
required super.footerType, required super.maxHeight,
|
||||
required super.footerType,
|
||||
required super.maxHeight,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -53,8 +55,35 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
}
|
||||
|
||||
String amount = '0';
|
||||
bool isValid = false;
|
||||
final FocusNode _numpadFocusNode = FocusNode();
|
||||
|
||||
void _onPressedAll() => setState(() {
|
||||
amount = widget.balance!;
|
||||
isValid = _validate();
|
||||
});
|
||||
|
||||
void _onNumberPressed(int i) => setState(() {
|
||||
amount = amount == '0' ? i.toString() : '${amount}${i}';
|
||||
isValid = _validate();
|
||||
});
|
||||
|
||||
void _onDeletePressed() => setState(() {
|
||||
amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0';
|
||||
isValid = _validate();
|
||||
});
|
||||
|
||||
void _onDecimalPressed() => setState(() {
|
||||
amount = '${amount.replaceAll('.', '')}.';
|
||||
isValid = _validate();
|
||||
});
|
||||
|
||||
bool _validate() {
|
||||
final amountBigInt = parseFixed(amount, 18);
|
||||
final balanceBigInt = parseFixed(widget.balance!, 18);
|
||||
return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
child: Column(children: [
|
||||
@@ -80,18 +109,14 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
AssetBalanceRow(
|
||||
title: widget.balanceTitle!,
|
||||
amount: widget.balance!,
|
||||
onAllPressed: () => setState(() => amount = widget.balance!),
|
||||
onAllPressed: _onPressedAll,
|
||||
),
|
||||
],
|
||||
NumberPad(
|
||||
focusNode: _numpadFocusNode,
|
||||
onNumberPressed: (i) => setState(
|
||||
() => amount = amount == '0' ? i.toString() : '${amount}${i}',
|
||||
),
|
||||
onDeletePressed: () => setState(
|
||||
() => amount = amount.length > 1 ? amount.substring(0, amount.length - 1) : '0',
|
||||
),
|
||||
onDecimalPressed: () => setState(() => amount = '${amount.replaceAll('.', '')}.'),
|
||||
onNumberPressed: _onNumberPressed,
|
||||
onDeletePressed: _onDeletePressed,
|
||||
onDecimalPressed: _onDecimalPressed,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
@@ -101,7 +126,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
isDisabled: !isValid,
|
||||
),
|
||||
)
|
||||
]),
|
||||
|
||||
@@ -32,6 +32,7 @@ class NodeForm extends StatelessWidget {
|
||||
..setLogin((editingNode!.login ?? ''))
|
||||
..setSSL((editingNode!.isSSL))
|
||||
..setTrusted((editingNode!.trusted))
|
||||
..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching))
|
||||
..setSocksProxy((editingNode!.useSocksProxy))
|
||||
..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
|
||||
}
|
||||
@@ -153,6 +154,25 @@ class NodeForm extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.0),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => StandardCheckbox(
|
||||
value: nodeViewModel.isEnabledForAutoSwitching,
|
||||
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) => nodeViewModel.isEnabledForAutoSwitching = value,
|
||||
caption: S.current.enable_for_auto_switching,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.0),
|
||||
if (nodeViewModel.hasAuthCredentials) ...[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
|
||||
@@ -115,7 +115,8 @@ class QRWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (addressListViewModel.isPayjoinUnavailable &&
|
||||
!addressListViewModel.isSilentPayments) ...[
|
||||
!addressListViewModel.isSilentPayments &&
|
||||
!addressListViewModel.isCupcake) ...[
|
||||
GestureDetector(
|
||||
onTap: () => _onPayjoinInactivePressed(context),
|
||||
child: Row(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/option_tile.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
@@ -45,10 +46,6 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
bool isRestoring = false;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (!DeviceInfo.instance.isMobile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS)
|
||||
.isNotEmpty;
|
||||
@@ -57,13 +54,40 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
return true;
|
||||
}
|
||||
|
||||
String get imageRestoreHWPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_hw_dark.png'
|
||||
: 'assets/images/restore_hw.png';
|
||||
|
||||
String get imageRestoreCupcakePath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_cupcake_dark.png'
|
||||
: 'assets/images/restore_cupcake.png';
|
||||
|
||||
String get imageRestoreQRPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_qr_dark.png'
|
||||
: 'assets/images/restore_qr.png';
|
||||
|
||||
|
||||
String get imageRestoreHotWalletPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_hot_wallet_dark.png'
|
||||
: 'assets/images/restore_hot_wallet.png';
|
||||
|
||||
|
||||
String get imageRestoreBackupPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_backup_dark.png'
|
||||
: 'assets/images/restore_backup.png';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageColor = Theme.of(context).colorScheme.primary;
|
||||
final imageLedger = Image.asset('assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
width: 40, color: imageColor);
|
||||
final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor);
|
||||
final imageBackup = Image.asset('assets/images/backup.png', color: imageColor);
|
||||
final imageRestoreHW = Image.asset(imageRestoreHWPath, width: 55);
|
||||
final imageRestoreCupcake = Image.asset(imageRestoreCupcakePath, width: 55);
|
||||
final imageRestoreQR = Image.asset(imageRestoreQRPath, width: 55);
|
||||
final imageSeedKeys = Image.asset(imageRestoreHotWalletPath, width: 55);
|
||||
final imageRestoreBackup = Image.asset(imageRestoreBackupPath, width: 55);
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
@@ -80,15 +104,16 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
title: S.of(context).restore_title_from_seed_keys,
|
||||
description: S.of(context).restore_description_from_seed_keys,
|
||||
),
|
||||
if (widget.isNewInstall)
|
||||
if (FeatureFlag.hasBitcoinViewOnly && DeviceInfo.instance.isMobile)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_backup_button_key'),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
|
||||
image: imageBackup,
|
||||
title: S.of(context).restore_title_from_backup,
|
||||
description: S.of(context).restore_description_from_backup,
|
||||
key: ValueKey('restore_options_from_cupcake_button_key'),
|
||||
onPressed: () => _onScanQRCode(context),
|
||||
image: imageRestoreCupcake,
|
||||
title: S.of(context).restore_title_from_cupcake,
|
||||
description: S.of(context).restore_description_from_cupcake,
|
||||
tag: S.of(context).new_tag,
|
||||
),
|
||||
),
|
||||
if (_doesSupportHardwareWallets)
|
||||
@@ -98,25 +123,33 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
key: ValueKey('restore_options_from_hardware_wallet_button_key'),
|
||||
onPressed: () =>
|
||||
Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet),
|
||||
image: imageLedger,
|
||||
image: imageRestoreHW,
|
||||
title: S.of(context).restore_title_from_hardware_wallet,
|
||||
description: S.of(context).restore_description_from_hardware_wallet,
|
||||
),
|
||||
),
|
||||
if (widget.isNewInstall)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_backup_button_key'),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
|
||||
image: imageRestoreBackup,
|
||||
title: S.of(context).restore_title_from_backup,
|
||||
description: S.of(context).restore_description_from_backup,
|
||||
),
|
||||
),
|
||||
if (DeviceInfo.instance.isMobile)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_qr_button_key'),
|
||||
onPressed: () => _onScanQRCode(context),
|
||||
icon: Icon(
|
||||
Icons.qr_code_rounded,
|
||||
color: imageColor,
|
||||
size: 50,
|
||||
),
|
||||
image: imageRestoreQR,
|
||||
title: S.of(context).scan_qr_code,
|
||||
description: S.of(context).cold_or_recover_wallet),
|
||||
)
|
||||
description: S.of(context).cold_or_recover_wallet,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
|
||||
Widget _restoreFromKeysFormFields() {
|
||||
// Decred can only restore a view only wallet with an account pubkey. Other
|
||||
// fields are not used.
|
||||
if (widget.walletRestoreViewModel.type == WalletType.decred) {
|
||||
if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
return Column(
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
controller: viewKeyController,
|
||||
hintText: S.of(context).view_key_public,
|
||||
maxLines: null,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
BlockchainHeightWidget(
|
||||
key: blockchainHeightKey,
|
||||
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
|
||||
onHeightChange: (_) => null,
|
||||
onHeightOrDateEntered: widget.onHeightOrDateEntered,
|
||||
walletType: widget.walletRestoreViewModel.type,
|
||||
),
|
||||
if (widget.walletRestoreViewModel.hasBlockchainHeightSelector)
|
||||
BlockchainHeightWidget(
|
||||
key: blockchainHeightKey,
|
||||
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
|
||||
onHeightChange: (_) => null,
|
||||
onHeightOrDateEntered: widget.onHeightOrDateEntered,
|
||||
walletType: widget.walletRestoreViewModel.type,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage {
|
||||
credentials['seed'] =
|
||||
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
|
||||
|
||||
if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
|
||||
if (walletRestoreViewModel.hasBlockchainHeightSelector) {
|
||||
credentials['height'] =
|
||||
walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ??
|
||||
-1;
|
||||
@@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage {
|
||||
credentials['name'] =
|
||||
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text;
|
||||
credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text;
|
||||
if (walletRestoreViewModel.type != WalletType.decred) {
|
||||
if (!walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
credentials['address'] =
|
||||
walletRestoreFromKeysFormKey.currentState!.addressController.text;
|
||||
credentials['spendKey'] =
|
||||
@@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
}
|
||||
},
|
||||
onViewKeyEntered: (bool entered) {
|
||||
if (walletRestoreViewModel.type == WalletType.decred) {
|
||||
if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
walletRestoreViewModel.isButtonEnabled = entered;
|
||||
}
|
||||
},
|
||||
@@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
key: widget.walletRestoreFromSeedFormKey,
|
||||
restoredWallet: walletRestoreViewModel.restoredWallet,
|
||||
seedSettingsViewModel: widget.seedSettingsViewModel,
|
||||
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector,
|
||||
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector,
|
||||
displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector,
|
||||
type: widget.walletRestoreViewModel.type,
|
||||
blockHeightFocusNode: widget.blockHeightFocusNode,
|
||||
@@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
}
|
||||
|
||||
void _validateOnChange({bool isPolyseed = false}) {
|
||||
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
|
||||
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) {
|
||||
final hasHeight = walletRestoreFromSeedFormKey
|
||||
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/core/node_switching_service.dart';
|
||||
import 'package:cake_wallet/core/totp_request_details.dart';
|
||||
import 'package:cake_wallet/core/trade_monitor.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
@@ -29,6 +31,7 @@ class Root extends StatefulWidget {
|
||||
required this.authService,
|
||||
required this.linkViewModel,
|
||||
required this.tradeMonitor,
|
||||
required this.nodeSwitchingService,
|
||||
}) : super(key: key);
|
||||
|
||||
final AuthenticationStore authenticationStore;
|
||||
@@ -38,6 +41,7 @@ class Root extends StatefulWidget {
|
||||
final Widget child;
|
||||
final LinkViewModel linkViewModel;
|
||||
final TradeMonitor tradeMonitor;
|
||||
final NodeSwitchingService nodeSwitchingService;
|
||||
|
||||
@override
|
||||
RootState createState() => RootState();
|
||||
@@ -161,6 +165,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
widget.tradeMonitor.resumeTradeMonitoring();
|
||||
|
||||
// Trigger node health check when app resumes
|
||||
widget.nodeSwitchingService.performHealthCheck();
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -450,7 +450,7 @@ class SendPage extends BasePage {
|
||||
}
|
||||
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
|
||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
|
||||
arguments: 'export-outputs');
|
||||
arguments: monero!.exportOutputsUR(sendViewModel.wallet));
|
||||
await Future.delayed(
|
||||
Duration(seconds: 1)); // wait for monero to refresh the state
|
||||
}
|
||||
@@ -570,6 +570,7 @@ class SendPage extends BasePage {
|
||||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('send_page_confirm_sending_bottom_sheet_key'),
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
accessibleNavigationModeSlideActionButtonText: S.of(bottomSheetContext).send,
|
||||
currentTheme: currentTheme,
|
||||
footerType: FooterType.slideActionButton,
|
||||
walletType: sendViewModel.walletType,
|
||||
|
||||
@@ -258,7 +258,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
|
||||
currencyValueValidator: output.sendAll
|
||||
? sendViewModel.allAmountValidator
|
||||
: sendViewModel.amountValidator,
|
||||
: sendViewModel.amountValidator(output),
|
||||
allAmountCallback: () async =>
|
||||
output.setSendAll(await sendViewModel.sendingBalance),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ 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/nodes/widgets/node_list_row.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
@@ -34,7 +35,23 @@ class ManageNodesPage extends BasePage {
|
||||
onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Observer(
|
||||
builder: (_) => SettingsSwitcherCell(
|
||||
key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'),
|
||||
title: S.current.enable_auto_node_switching,
|
||||
value: isPow
|
||||
? powNodeListViewModel!.enableAutomaticNodeSwitching
|
||||
: nodeListViewModel!.enableAutomaticNodeSwitching,
|
||||
onValueChange: (BuildContext context, bool value) {
|
||||
if (isPow) {
|
||||
powNodeListViewModel!.setEnableAutomaticNodeSwitching(value);
|
||||
} else {
|
||||
nodeListViewModel!.setEnableAutomaticNodeSwitching(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Observer(
|
||||
builder: (BuildContext context) {
|
||||
int itemsCount =
|
||||
|
||||
@@ -36,22 +36,22 @@ class OtherSettingsPage extends BasePage {
|
||||
if (_otherSettingsViewModel.displayTransactionPriority)
|
||||
_otherSettingsViewModel.walletType == WalletType.bitcoin ?
|
||||
SettingsPriorityPickerCell(
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
customItemIndex: _otherSettingsViewModel.customPriorityItemIndex,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected,
|
||||
customValue: _otherSettingsViewModel.customBitcoinFeeRate,
|
||||
maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(),
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
customItemIndex: _otherSettingsViewModel.customPriorityItemIndex,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected,
|
||||
customValue: _otherSettingsViewModel.customBitcoinFeeRate,
|
||||
maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(),
|
||||
) :
|
||||
SettingsPickerCell(
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
|
||||
),
|
||||
title: S.current.settings_fee_priority,
|
||||
items: priorityForWalletType(_otherSettingsViewModel.walletType),
|
||||
displayItem: _otherSettingsViewModel.getDisplayPriority,
|
||||
selectedItem: _otherSettingsViewModel.transactionPriority,
|
||||
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
|
||||
),
|
||||
if (_otherSettingsViewModel.changeRepresentativeEnabled)
|
||||
SettingsCellWithArrow(
|
||||
title: S.current.change_rep,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bbqrdart/bbqrdart.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
@@ -7,31 +9,49 @@ import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/animated_ur_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// ur:xmr-txunsigned - unsigned transaction
|
||||
// should show a scanner afterwards.
|
||||
|
||||
class AnimatedURPage extends BasePage {
|
||||
final bool isAll;
|
||||
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) {
|
||||
if (urQr == "export-outputs") {
|
||||
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false);
|
||||
} else if (urQr == "export-outputs-all") {
|
||||
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
|
||||
} else {
|
||||
this.urQr = urQr;
|
||||
AnimatedURPage(this.animatedURmodel, {
|
||||
required Map<String, String> this.urQr,
|
||||
this.isAll = false,
|
||||
}) {
|
||||
if (urQr.keys.first.startsWith("export-outputs") && animatedURmodel.wallet.type == WalletType.monero) {
|
||||
if (animatedURmodel.wallet.type == WalletType.monero) {
|
||||
urQr = monero!.exportOutputsUR(animatedURmodel.wallet);
|
||||
} else {
|
||||
throw UnimplementedError("unable to handle UR: ${urQr.keys.first}");
|
||||
}
|
||||
}
|
||||
for (var key in urQr.keys) {
|
||||
if (key.isEmpty || urQr[key]!.isEmpty) {
|
||||
urQr.remove(key);
|
||||
continue;
|
||||
}
|
||||
urQr[key] = urQr[key]!.trim();
|
||||
}
|
||||
}
|
||||
|
||||
late String urQr;
|
||||
Map<String, String> urQr = {};
|
||||
|
||||
|
||||
final AnimatedURModel animatedURmodel;
|
||||
|
||||
String get urQrType {
|
||||
final first = urQr.trim().split("\n")[0];
|
||||
if (urQr.values.first.trim().substring(0, 2) == BBQR.header) {
|
||||
return BBQR.header;
|
||||
}
|
||||
if (urQr.isEmpty) return "unknown";
|
||||
final first = urQr.values.first.trim().split("\n")[0];
|
||||
return first.split('/')[0];
|
||||
}
|
||||
|
||||
@@ -43,11 +63,11 @@ class AnimatedURPage extends BasePage {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 64.0),
|
||||
child: URQR(
|
||||
frames: urQr.trim().split("\n"),
|
||||
urqr: urQr,
|
||||
),
|
||||
),
|
||||
if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header].contains(urQrType)) ...{
|
||||
SizedBox(height: 32),
|
||||
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
@@ -60,30 +80,8 @@ class AnimatedURPage extends BasePage {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
if (urQrType == "ur:xmr-output" && !isAll) Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: PrimaryButton(
|
||||
onPressed: () => _exportAll(context),
|
||||
text: "Export all",
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _exportAll(BuildContext context) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +104,11 @@ class AnimatedURPage extends BasePage {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
break;
|
||||
case "ur:psbt": // psbt
|
||||
final ur = await presentQRScanner(context);
|
||||
if (ur == null) return;
|
||||
await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n"));
|
||||
Navigator.of(context).pop(true);
|
||||
default:
|
||||
throw UnimplementedError("unable to handle UR: ${urQrType}");
|
||||
}
|
||||
@@ -124,9 +127,9 @@ class AnimatedURPage extends BasePage {
|
||||
}
|
||||
|
||||
class URQR extends StatefulWidget {
|
||||
URQR({super.key, required this.frames});
|
||||
URQR({super.key, required this.urqr});
|
||||
|
||||
List<String> frames;
|
||||
final Map<String, String> urqr;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
@@ -160,6 +163,24 @@ class _URQRState extends State<URQR> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first;
|
||||
int selectedInt = 0;
|
||||
|
||||
List<String> get frames {
|
||||
return widget.urqr[selected]?.split("\n") ?? [];
|
||||
}
|
||||
|
||||
late String nextLabel = widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length] ;
|
||||
|
||||
void next() {
|
||||
final keys = widget.urqr.keys.toList();
|
||||
setState(() {
|
||||
selectedInt++;
|
||||
nextLabel = keys[(selectedInt + 1) % keys.length];
|
||||
selected = keys[(selectedInt) % keys.length];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -168,10 +189,37 @@ class _URQRState extends State<URQR> {
|
||||
children: [
|
||||
Center(
|
||||
child: QrImage(
|
||||
data: widget.frames[frame % widget.frames.length], version: -1,
|
||||
data: frames[frame % frames.length],
|
||||
version: -1,
|
||||
size: 400,
|
||||
),
|
||||
),
|
||||
if (widget.urqr.values.length > 1)
|
||||
SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: PrimaryButton(
|
||||
onPressed: next,
|
||||
text: nextLabel,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (FeatureFlag.hasDevOptions) ...{
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: """Current frame (${frame % frames.length}): ${frames[frame % frames.length]},
|
||||
All frames:
|
||||
- ${frames.join("\n - ")}"""));
|
||||
},
|
||||
child: Text("[dev] copy debug info"),
|
||||
),
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
|
||||
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
|
||||
isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
|
||||
|
||||
final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
|
||||
final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
|
||||
|
||||
_tabController = TabController(length: totalTabs, vsync: this);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
|
||||
dividerColor: Colors.transparent,
|
||||
padding: EdgeInsets.zero,
|
||||
tabs: [
|
||||
Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
|
||||
if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
|
||||
if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),),
|
||||
if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
|
||||
],
|
||||
@@ -137,10 +137,11 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
child: _buildSeedTab(context, false),
|
||||
),
|
||||
if (_hasSeeds)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
child: _buildSeedTab(context, false),
|
||||
),
|
||||
if (showKeyTab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/rounded_icon_button.dart';
|
||||
import 'package:cake_wallet/utils/image_utill.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
const CakePayTransactionSentBottomSheet({
|
||||
@@ -39,18 +41,36 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
final String paymentId;
|
||||
final String paymentIdValue;
|
||||
|
||||
TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
|
||||
TextStyle _titleStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.bodyLarge!;
|
||||
|
||||
TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
|
||||
TextStyle _valueStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
|
||||
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
|
||||
TextStyle _subStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(
|
||||
color: Theme
|
||||
.of(ctx)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
);
|
||||
|
||||
Widget _buildHeader(BuildContext ctx) => Column(
|
||||
Widget _buildHeader(BuildContext ctx) =>
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
@@ -58,7 +78,10 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(ctx).colorScheme.onSurface,
|
||||
color: Theme
|
||||
.of(ctx)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -69,11 +92,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
titleText,
|
||||
style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
style: Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -81,7 +108,8 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildBody(BuildContext context) => Padding(
|
||||
Widget _buildBody(BuildContext context) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -112,25 +140,49 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
itemSubTitleStyle: _subStyle(context),
|
||||
imagePath: output.parsedAddress.profileImageUrl,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_StandardTile(
|
||||
itemTitle: paymentId + ': \n' + paymentIdValue,
|
||||
titleStyle: _titleStyle(context).copyWith(
|
||||
fontSize: 14
|
||||
),
|
||||
itemValue: '',
|
||||
itemValueStyle: _valueStyle(context),
|
||||
copyButton: true,
|
||||
copyButtonOnPressed: () async =>
|
||||
await Clipboard.setData(ClipboardData(text: paymentIdValue)),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(paymentId + ': ' + paymentIdValue,
|
||||
style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 18),
|
||||
Image.asset('assets/images/envelope.png'),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
S.of(context).cake_pay_card_email_delivered_message,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
S
|
||||
.of(context)
|
||||
.cake_pay_card_email_delivered_message,
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
PrimaryButton(
|
||||
text: S.of(context).close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
text: S
|
||||
.of(context)
|
||||
.close,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
textColor: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
@@ -142,12 +194,18 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.9;
|
||||
final maxHeight = MediaQuery
|
||||
.of(context)
|
||||
.size
|
||||
.height * 0.9;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.surface,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
@@ -174,6 +232,8 @@ class _StandardTile extends StatelessWidget {
|
||||
this.itemSubTitle,
|
||||
this.itemSubTitleStyle,
|
||||
this.imagePath,
|
||||
this.copyButton = false,
|
||||
this.copyButtonOnPressed,
|
||||
});
|
||||
|
||||
final String itemTitle;
|
||||
@@ -183,6 +243,8 @@ class _StandardTile extends StatelessWidget {
|
||||
final String? itemSubTitle;
|
||||
final TextStyle? itemSubTitleStyle;
|
||||
final String? imagePath;
|
||||
final bool copyButton;
|
||||
final VoidCallback? copyButtonOnPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -193,12 +255,17 @@ class _StandardTile extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)),
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLowest
|
||||
.withAlpha(80)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: copyButton ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (imagePath != null)
|
||||
Padding(
|
||||
@@ -216,10 +283,19 @@ class _StandardTile extends StatelessWidget {
|
||||
itemTitle,
|
||||
style: titleStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
if (copyButton)
|
||||
RoundedIconButton(
|
||||
icon: Icons.copy_all_outlined,
|
||||
onPressed: copyButtonOnPressed ?? () {},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/rounded_icon_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
@@ -10,6 +10,7 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
@@ -45,7 +46,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
_currentTheme = currentTheme,
|
||||
super(
|
||||
titleText: titleText,
|
||||
maxHeight: 900,
|
||||
maxHeight: 900,
|
||||
titleIconPath: titleIconPath,
|
||||
currentTheme: currentTheme,
|
||||
footerType: footerType,
|
||||
@@ -115,6 +116,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
applyAddressFormatting: false,
|
||||
copyButton: true,
|
||||
)),
|
||||
),
|
||||
if (explanation != null)
|
||||
@@ -307,20 +309,20 @@ class StandardInfoTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AddressTile extends StatelessWidget {
|
||||
const AddressTile({
|
||||
super.key,
|
||||
required this.itemTitle,
|
||||
required this.itemTitleTextStyle,
|
||||
required this.address,
|
||||
required this.itemSubTitleTextStyle,
|
||||
required this.tileBackgroundColor,
|
||||
required this.walletType,
|
||||
this.amountTextStyle,
|
||||
this.applyAddressFormatting = true,
|
||||
this.imagePath,
|
||||
this.amount,
|
||||
this.itemSubTitle,
|
||||
});
|
||||
const AddressTile(
|
||||
{super.key,
|
||||
required this.itemTitle,
|
||||
required this.itemTitleTextStyle,
|
||||
required this.address,
|
||||
required this.itemSubTitleTextStyle,
|
||||
required this.tileBackgroundColor,
|
||||
required this.walletType,
|
||||
this.amountTextStyle,
|
||||
this.applyAddressFormatting = true,
|
||||
this.imagePath,
|
||||
this.amount,
|
||||
this.itemSubTitle,
|
||||
this.copyButton = false});
|
||||
|
||||
final String itemTitle;
|
||||
final TextStyle itemTitleTextStyle;
|
||||
@@ -333,6 +335,7 @@ class AddressTile extends StatelessWidget {
|
||||
final bool applyAddressFormatting;
|
||||
final String? imagePath;
|
||||
final String? itemSubTitle;
|
||||
final bool copyButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -387,9 +390,24 @@ class AddressTile extends StatelessWidget {
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
))
|
||||
: Text(
|
||||
address,
|
||||
style: itemTitleTextStyle,
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(address,
|
||||
style: copyButton
|
||||
? itemTitleTextStyle.copyWith(fontSize: 12)
|
||||
: itemTitleTextStyle),
|
||||
SizedBox(width: 8),
|
||||
if (copyButton)
|
||||
RoundedIconButton(
|
||||
icon: Icons.copy_all_outlined,
|
||||
onPressed: () async =>
|
||||
await Clipboard.setData(ClipboardData(text: address)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
itemSubTitle == null
|
||||
? Container()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TooltipSheet extends BaseBottomSheet {
|
||||
final String tooltip;
|
||||
|
||||
const TooltipSheet({
|
||||
required super.currentTheme,
|
||||
required super.titleText,
|
||||
super.titleIconPath,
|
||||
required this.tooltip,
|
||||
required super.footerType,
|
||||
super.maxHeight = 900,
|
||||
super.singleActionButtonText,
|
||||
super.onSingleActionButtonPressed,
|
||||
super.singleActionButtonKey,
|
||||
super.doubleActionLeftButtonText,
|
||||
super.doubleActionRightButtonText,
|
||||
super.onLeftActionButtonPressed,
|
||||
super.onRightActionButtonPressed,
|
||||
super.leftActionButtonKey,
|
||||
super.rightActionButtonKey,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: AutoSizeText(
|
||||
tooltip,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget footerWidget(BuildContext context) => SizedBox.shrink();
|
||||
}
|
||||
@@ -7,6 +7,7 @@ class OptionTile extends StatelessWidget {
|
||||
this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.tag,
|
||||
super.key,
|
||||
}) : assert(image != null || icon != null);
|
||||
|
||||
@@ -15,6 +16,7 @@ class OptionTile extends StatelessWidget {
|
||||
final Icon? icon;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -36,25 +38,34 @@ class OptionTile extends StatelessWidget {
|
||||
icon ?? image!,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
padding: EdgeInsets.only(left: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 220),
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
if (tag != null) Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||
margin: EdgeInsets.only(left: 5),
|
||||
child: Text(tag!))
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoundedIconButton extends StatelessWidget {
|
||||
const RoundedIconButton(
|
||||
{this.icon,
|
||||
this.iconWidget,
|
||||
required this.onPressed,
|
||||
this.shape,
|
||||
this.width,
|
||||
this.height,
|
||||
this.iconSize,
|
||||
this.fillColor});
|
||||
|
||||
final IconData? icon;
|
||||
final Widget? iconWidget;
|
||||
final VoidCallback onPressed;
|
||||
final ShapeBorder? shape;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final double? iconSize;
|
||||
final Color? fillColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return RawMaterialButton(
|
||||
onPressed: onPressed,
|
||||
fillColor: fillColor ?? colorScheme.surfaceContainerHighest,
|
||||
elevation: 0,
|
||||
constraints: BoxConstraints.tightFor(width: width ?? 30, height: height ?? 30),
|
||||
padding: EdgeInsets.zero,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
shape: shape ?? const CircleBorder(),
|
||||
child: icon != null
|
||||
? Icon(icon, size: iconSize ?? 14, color: colorScheme.onSurface)
|
||||
: iconWidget);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class SettingActions {
|
||||
name: (context) => S.of(context).export_outputs,
|
||||
image: 'assets/images/monero_menu.png',
|
||||
onTap: (BuildContext context) {
|
||||
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
|
||||
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: {'export-outputs': 'export-outputs'});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ abstract class SettingsStoreBase with Store {
|
||||
required this.mwebEnabled,
|
||||
required this.hasEnabledMwebBefore,
|
||||
required this.mwebNodeUri,
|
||||
required bool initialEnableAutomaticNodeSwitching,
|
||||
TransactionPriority? initialBitcoinTransactionPriority,
|
||||
TransactionPriority? initialMoneroTransactionPriority,
|
||||
TransactionPriority? initialWowneroTransactionPriority,
|
||||
@@ -186,6 +187,7 @@ abstract class SettingsStoreBase with Store {
|
||||
currentSyncMode = initialSyncMode,
|
||||
currentSyncAll = initialSyncAll,
|
||||
currentBuiltinTor = initialBuiltinTor,
|
||||
enableAutomaticNodeSwitching = initialEnableAutomaticNodeSwitching,
|
||||
priority = ObservableMap<WalletType, TransactionPriority>() {
|
||||
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
|
||||
|
||||
@@ -613,6 +615,11 @@ abstract class SettingsStoreBase with Store {
|
||||
(_) => mwebNodeUri,
|
||||
(String mwebNodeUri) =>
|
||||
_sharedPreferences.setString(PreferencesKey.mwebNodeUri, mwebNodeUri));
|
||||
|
||||
reaction(
|
||||
(_) => enableAutomaticNodeSwitching,
|
||||
(bool enableAutomaticNodeSwitching) => _sharedPreferences.setBool(
|
||||
PreferencesKey.enableAutomaticNodeSwitching, enableAutomaticNodeSwitching));
|
||||
|
||||
this.nodes.observe((change) {
|
||||
if (change.newValue != null && change.key != null) {
|
||||
@@ -859,6 +866,9 @@ abstract class SettingsStoreBase with Store {
|
||||
@observable
|
||||
String mwebNodeUri;
|
||||
|
||||
@observable
|
||||
bool enableAutomaticNodeSwitching;
|
||||
|
||||
final SecureStorage _secureStorage;
|
||||
final SharedPreferences _sharedPreferences;
|
||||
|
||||
@@ -1034,6 +1044,8 @@ abstract class SettingsStoreBase with Store {
|
||||
sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
|
||||
final mwebNodeUri = sharedPreferences.getString(PreferencesKey.mwebNodeUri) ??
|
||||
"ltc-electrum.cakewallet.com:9333";
|
||||
final enableAutomaticNodeSwitching =
|
||||
sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? true;
|
||||
|
||||
// If no value
|
||||
if (pinLength == null || pinLength == 0) {
|
||||
@@ -1335,6 +1347,7 @@ abstract class SettingsStoreBase with Store {
|
||||
mwebEnabled: mwebEnabled,
|
||||
mwebNodeUri: mwebNodeUri,
|
||||
hasEnabledMwebBefore: hasEnabledMwebBefore,
|
||||
initialEnableAutomaticNodeSwitching: enableAutomaticNodeSwitching,
|
||||
initialMoneroTransactionPriority: moneroTransactionPriority,
|
||||
initialWowneroTransactionPriority: wowneroTransactionPriority,
|
||||
initialZanoTransactionPriority: zanoTransactionPriority,
|
||||
@@ -1478,6 +1491,9 @@ abstract class SettingsStoreBase with Store {
|
||||
actionlistDisplayMode = ObservableList<ActionListDisplayMode>();
|
||||
actionlistDisplayMode.addAll(deserializeActionlistDisplayModes(
|
||||
sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode));
|
||||
enableAutomaticNodeSwitching =
|
||||
sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ??
|
||||
enableAutomaticNodeSwitching;
|
||||
var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength);
|
||||
// If no value
|
||||
if (pinLength == null || pinLength == 0) {
|
||||
|
||||
@@ -9,4 +9,5 @@ class FeatureFlag {
|
||||
static final bool isInAppTorEnabled = (Platform.isAndroid);
|
||||
static const int verificationWordsCount = kDebugMode ? 0 : 2;
|
||||
static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode);
|
||||
static const bool hasBitcoinViewOnly = true;
|
||||
}
|
||||
|
||||
@@ -536,7 +536,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet;
|
||||
bool get hasSilentPayments =>
|
||||
wallet.type == WalletType.bitcoin &&
|
||||
(bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isNotEmpty &&
|
||||
!wallet.isHardwareWallet;
|
||||
|
||||
@computed
|
||||
bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class ReceiveOptionViewModelBase with Store {
|
||||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
_options = [
|
||||
...bitcoin!.getBitcoinReceivePageOptions(),
|
||||
...bitcoin!.getBitcoinReceivePageOptions(_wallet),
|
||||
...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
|
||||
];
|
||||
break;
|
||||
|
||||
@@ -95,13 +95,9 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
@observable
|
||||
bool isSendable;
|
||||
|
||||
@computed
|
||||
String get extraInfo => switch (trade.from) {
|
||||
CryptoCurrency.xlm => '\n\n' + S.current.xlm_extra_info,
|
||||
CryptoCurrency.xrp => '\n\n' + S.current.xrp_extra_info,
|
||||
CryptoCurrency.ton => '\n\n' + S.current.ton_extra_info,
|
||||
_ => ''
|
||||
};
|
||||
String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty
|
||||
? '\n\n' + S.current.exchange_extra_info
|
||||
: '';
|
||||
|
||||
@computed
|
||||
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
|
||||
@@ -203,10 +199,9 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
),
|
||||
]);
|
||||
|
||||
if (trade.extraId != null) {
|
||||
final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton;
|
||||
final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
|
||||
|
||||
if (shouldAddExtraId) {
|
||||
if (isExtraIdExist) {
|
||||
final title = trade.from == CryptoCurrency.xrp
|
||||
? S.current.destination_tag
|
||||
: trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton
|
||||
@@ -215,14 +210,12 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
|
||||
items.add(
|
||||
ExchangeTradeItem(
|
||||
title: title,
|
||||
data: '${trade.extraId}',
|
||||
isCopied: true,
|
||||
isReceiveDetail: !shouldAddExtraId,
|
||||
isExternalSendDetail: shouldAddExtraId
|
||||
),
|
||||
title: title,
|
||||
data: trade.extraId ?? '',
|
||||
isCopied: true,
|
||||
isReceiveDetail: !isExtraIdExist,
|
||||
isExternalSendDetail: isExtraIdExist),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
items.add(
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/polygon/polygon.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/hardware/device_connection_type.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
@@ -30,10 +29,6 @@ abstract class LedgerViewModelBase with Store {
|
||||
late final sdk.LedgerInterface ledgerPlusUSB;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (!DeviceInfo.instance.isMobile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero, Platform.isIOS)
|
||||
@@ -65,6 +60,8 @@ abstract class LedgerViewModelBase with Store {
|
||||
if (bleIsEnabled && !_bleIsInitialized) {
|
||||
ledgerPlusBLE = sdk.LedgerInterface.ble(
|
||||
onPermissionRequest: (_) async {
|
||||
if (Platform.isMacOS) return true;
|
||||
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
|
||||
@@ -21,6 +21,8 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel;
|
||||
abstract class DEuroViewModelBase with Store {
|
||||
final AppStore _appStore;
|
||||
|
||||
static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000");
|
||||
|
||||
DEuroViewModelBase(
|
||||
this._appStore,
|
||||
this.balanceViewModel,
|
||||
@@ -44,39 +46,31 @@ abstract class DEuroViewModelBase with Store {
|
||||
@action
|
||||
void acceptDisclaimer() => _settingsStore.shouldShowDEuroDisclaimer = false;
|
||||
|
||||
@computed
|
||||
String get pendingTransactionFiatAmountFormatted =>
|
||||
isFiatDisabled ? '' : '$pendingTransactionFiatAmount ${fiat.title}';
|
||||
|
||||
@computed
|
||||
String get pendingTransactionFeeFiatAmountFormatted =>
|
||||
isFiatDisabled ? '' : '$pendingTransactionFeeFiatAmount ${fiat.title}';
|
||||
|
||||
FiatCurrency get fiat => _settingsStore.fiatCurrency;
|
||||
|
||||
@computed
|
||||
String get pendingTransactionFiatAmount =>
|
||||
transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted);
|
||||
|
||||
@computed
|
||||
String get pendingTransactionFeeFiatAmount {
|
||||
try {
|
||||
if (transaction != null) {
|
||||
final currency = CryptoCurrency.eth;
|
||||
return calculateFiatAmount(
|
||||
price: _fiatConversationStore.prices[currency]!,
|
||||
cryptoAmount: transaction!.feeFormattedValue,
|
||||
);
|
||||
} else {
|
||||
return '0.00';
|
||||
}
|
||||
} catch (_) {
|
||||
return '0.00';
|
||||
}
|
||||
String get pendingTransactionFiatAmountFormatted {
|
||||
final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted);
|
||||
return isFiatDisabled ? '' : '$amount ${fiat.title}';
|
||||
}
|
||||
|
||||
@computed
|
||||
String get accountBalance {
|
||||
String get pendingTransactionFeeFiatAmountFormatted {
|
||||
var amount = '0.00';
|
||||
try {
|
||||
if (transaction != null) {
|
||||
final feeCurrency = CryptoCurrency.eth;
|
||||
amount = calculateFiatAmount(
|
||||
price: _fiatConversationStore.prices[feeCurrency]!,
|
||||
cryptoAmount: transaction!.feeFormattedValue,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
return isFiatDisabled ? '' : '$amount ${fiat.title}';
|
||||
}
|
||||
|
||||
@computed
|
||||
String get accountBalanceFormated {
|
||||
final dEuroKey = balanceViewModel.balances.keys
|
||||
.firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title);
|
||||
if (dEuroKey == null) return '0.00';
|
||||
@@ -84,22 +78,30 @@ abstract class DEuroViewModelBase with Store {
|
||||
}
|
||||
|
||||
@observable
|
||||
String savingsBalance = '0.00';
|
||||
BigInt savingsBalance = BigInt.zero;
|
||||
|
||||
@computed
|
||||
String get fiatSavingsBalance => _getDEuroFiatAmount(savingsBalance);
|
||||
String get savingsBalanceFormated =>
|
||||
ethereum!.formatterEthereumAmountToDouble(amount: savingsBalance).toStringAsFixed(6);
|
||||
|
||||
@computed
|
||||
String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated);
|
||||
|
||||
@observable
|
||||
ExecutionState state = InitialExecutionState();
|
||||
|
||||
@observable
|
||||
String interestRate = '0';
|
||||
String interestRateFormated = '0';
|
||||
|
||||
@observable
|
||||
String accruedInterest = '0.00';
|
||||
BigInt accruedInterest = BigInt.zero;
|
||||
|
||||
@computed
|
||||
String get fiatAccruedInterest => _getDEuroFiatAmount(accruedInterest);
|
||||
String get accruedInterestFormated =>
|
||||
ethereum!.formatterEthereumAmountToDouble(amount: accruedInterest).toStringAsFixed(6);
|
||||
|
||||
@computed
|
||||
String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated);
|
||||
|
||||
@observable
|
||||
BigInt approvedTokens = BigInt.zero;
|
||||
@@ -107,6 +109,9 @@ abstract class DEuroViewModelBase with Store {
|
||||
@computed
|
||||
bool get isEnabled => approvedTokens > BigInt.zero;
|
||||
|
||||
@computed
|
||||
bool get isSavingsActionsEnabled => isEnabled && accruedInterest >= MIN_ACCRUED_INTEREST;
|
||||
|
||||
@observable
|
||||
bool isLoading = true;
|
||||
|
||||
@@ -121,17 +126,9 @@ abstract class DEuroViewModelBase with Store {
|
||||
|
||||
@action
|
||||
Future<void> reloadSavingsUserData() async {
|
||||
final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!);
|
||||
final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
|
||||
|
||||
approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!);
|
||||
|
||||
savingsBalance = ethereum!
|
||||
.formatterEthereumAmountToDouble(amount: await savingsBalanceRaw)
|
||||
.toStringAsFixed(6);
|
||||
accruedInterest = ethereum!
|
||||
.formatterEthereumAmountToDouble(amount: await accruedInterestRaw)
|
||||
.toStringAsFixed(6);
|
||||
savingsBalance = await ethereum!.getDEuroSavingsBalance(_appStore.wallet!);
|
||||
accruedInterest = await ethereum!.getDEuroAccruedInterest(_appStore.wallet!);
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -139,7 +136,7 @@ abstract class DEuroViewModelBase with Store {
|
||||
Future<void> reloadInterestRate() async {
|
||||
final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!);
|
||||
|
||||
interestRate = (interestRateRaw / BigInt.from(10000)).toString();
|
||||
interestRateFormated = (interestRateRaw / BigInt.from(10000)).toString();
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -175,7 +172,7 @@ abstract class DEuroViewModelBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false);
|
||||
Future<void> prepareCollectInterest() => prepareSavingsEdit(accruedInterestFormated, false);
|
||||
|
||||
Future<void> prepareReinvestInterest() async {
|
||||
try {
|
||||
@@ -230,13 +227,13 @@ abstract class DEuroViewModelBase with Store {
|
||||
|
||||
String _getDEuroFiatAmount(String amount) {
|
||||
try {
|
||||
CryptoCurrency deuro = CryptoCurrency.deuro;
|
||||
var dEuro = CryptoCurrency.deuro;
|
||||
final keys = _fiatConversationStore.prices.keys.toList();
|
||||
for (final key in keys) {
|
||||
if (key.title == "DEURO") deuro = key;
|
||||
if (key.title == "DEURO") dEuro = key;
|
||||
}
|
||||
return calculateFiatAmount(
|
||||
price: _fiatConversationStore.prices[deuro]!,
|
||||
price: _fiatConversationStore.prices[dEuro]!,
|
||||
cryptoAmount: amount,
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
@@ -26,6 +26,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
login = '',
|
||||
password = '',
|
||||
trusted = false,
|
||||
isEnabledForAutoSwitching = false,
|
||||
useSocksProxy = false,
|
||||
socksProxyAddress = '';
|
||||
|
||||
@@ -56,6 +57,9 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
@observable
|
||||
bool trusted;
|
||||
|
||||
@observable
|
||||
bool isEnabledForAutoSwitching;
|
||||
|
||||
@observable
|
||||
bool useSocksProxy;
|
||||
|
||||
@@ -118,6 +122,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
password = '';
|
||||
useSSL = false;
|
||||
trusted = false;
|
||||
isEnabledForAutoSwitching = false;
|
||||
useSocksProxy = false;
|
||||
socksProxyAddress = '';
|
||||
}
|
||||
@@ -143,6 +148,9 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
@action
|
||||
void setTrusted(bool val) => trusted = val;
|
||||
|
||||
@action
|
||||
void setIsEnabledForAutoSwitching(bool val) => isEnabledForAutoSwitching = val;
|
||||
|
||||
@action
|
||||
void setSocksProxy(bool val) => useSocksProxy = val;
|
||||
|
||||
@@ -159,6 +167,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
password: password,
|
||||
useSSL: useSSL,
|
||||
trusted: trusted,
|
||||
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
|
||||
socksProxyAddress: socksProxyAddress);
|
||||
try {
|
||||
state = IsExecutingState();
|
||||
@@ -190,6 +199,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
password: password,
|
||||
useSSL: useSSL,
|
||||
trusted: trusted,
|
||||
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
|
||||
socksProxyAddress: socksProxyAddress);
|
||||
try {
|
||||
connectionState = IsExecutingState();
|
||||
|
||||
@@ -41,6 +41,13 @@ abstract class NodeListViewModelBase with Store {
|
||||
S.current.change_current_node(uri) +
|
||||
'${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}';
|
||||
|
||||
@computed
|
||||
bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
|
||||
|
||||
@action
|
||||
void setEnableAutomaticNodeSwitching(bool value) =>
|
||||
settingsStore.enableAutomaticNodeSwitching = value;
|
||||
|
||||
final ObservableList<Node> nodes;
|
||||
final SettingsStore settingsStore;
|
||||
final Box<Node> _nodeSource;
|
||||
|
||||
@@ -41,6 +41,13 @@ abstract class PowNodeListViewModelBase with Store {
|
||||
S.current.change_current_node(uri) +
|
||||
'${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}';
|
||||
|
||||
@computed
|
||||
bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
|
||||
|
||||
@action
|
||||
void setEnableAutomaticNodeSwitching(bool value) =>
|
||||
settingsStore.enableAutomaticNodeSwitching = value;
|
||||
|
||||
final ObservableList<Node> nodes;
|
||||
final SettingsStore settingsStore;
|
||||
final Box<Node> _nodeSource;
|
||||
|
||||
@@ -35,7 +35,7 @@ class RestoredWallet {
|
||||
|
||||
factory RestoredWallet.fromKey(Map<String, dynamic> json) {
|
||||
try {
|
||||
final codeParsed = jsonDecode(json['raw_qr'].toString());
|
||||
final codeParsed = jsonDecode(json['raw_qr'].toString());
|
||||
if (codeParsed["version"] == 0) {
|
||||
json['address'] = codeParsed["primaryAddress"];
|
||||
json['view_key'] = codeParsed["privateViewKey"];
|
||||
@@ -44,6 +44,7 @@ class RestoredWallet {
|
||||
} catch (e) {
|
||||
// fine, we don't care, it is only for monero anyway
|
||||
}
|
||||
json['view_key'] ??= json['xpub'];
|
||||
final height = json['height'] as String?;
|
||||
return RestoredWallet(
|
||||
restoreMode: json['mode'] as WalletRestoreMode,
|
||||
@@ -51,7 +52,7 @@ class RestoredWallet {
|
||||
address: json['address'] as String?,
|
||||
spendKey: json['spend_key'] as String?,
|
||||
viewKey: json['view_key'] as String?,
|
||||
height: height != null ? int.tryParse(height)??0 : 0,
|
||||
height: height != null ? int.tryParse(height) ?? 0 : 0,
|
||||
privateKey: json['private_key'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ class WalletRestoreFromQRCode {
|
||||
|
||||
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
|
||||
|
||||
if (code.startsWith("xpub")) return WalletType.bitcoin;
|
||||
|
||||
if (extracted == null) {
|
||||
// Special case for view-only monero wallet
|
||||
try {
|
||||
@@ -117,11 +119,15 @@ class WalletRestoreFromQRCode {
|
||||
|
||||
formattedUri = seedPhrase != null
|
||||
? '$walletType:?seed=$seedPhrase'
|
||||
: throw Exception('Failed to determine valid seed phrase');
|
||||
: code.startsWith('xpub')
|
||||
? '$walletType:?xpub=$code'
|
||||
: throw Exception('Failed to determine valid seed phrase');
|
||||
} else {
|
||||
final index = code.indexOf(':');
|
||||
final query = code.substring(index + 1).replaceAll('?', '&');
|
||||
formattedUri = '$walletType:?$query';
|
||||
formattedUri = code.startsWith('xpub')
|
||||
? '$walletType:?xpub=$code'
|
||||
:'$walletType:?$query';
|
||||
}
|
||||
|
||||
final uri = Uri.parse(formattedUri);
|
||||
@@ -158,6 +164,10 @@ class WalletRestoreFromQRCode {
|
||||
throw Exception('Unexpected restore mode: tx_payment_id is invalid');
|
||||
}
|
||||
|
||||
if (credentials.containsKey("xpub")) {
|
||||
return WalletRestoreMode.keys;
|
||||
}
|
||||
|
||||
if (credentials['seed'] != null) {
|
||||
final seedValue = credentials['seed'] as String;
|
||||
final words = SeedValidator.getWordList(type: type, language: 'english');
|
||||
|
||||
@@ -289,6 +289,15 @@ abstract class OutputBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> get extra {
|
||||
final fields = <String, dynamic>{};
|
||||
if (parsedAddress.parseFrom == ParseFrom.bip353) {
|
||||
fields['bip353_name'] = parsedAddress.name;
|
||||
fields['bip353_proof'] = parsedAddress.bip353DnsProof;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
void _setCryptoNumMaximumFractionDigits() {
|
||||
var maximumFractionDigits = 0;
|
||||
|
||||
|
||||
@@ -182,9 +182,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
if (pendingTransaction != null) {
|
||||
final currency = pendingTransactionFeeCurrency(walletType);
|
||||
final fiat = calculateFiatAmount(
|
||||
price: _fiatConversationStore.prices[currency]!,
|
||||
cryptoAmount: pendingTransaction!.feeFormattedValue,
|
||||
);
|
||||
price: _fiatConversationStore.prices[currency]!,
|
||||
cryptoAmount: pendingTransaction!.feeFormattedValue,
|
||||
);
|
||||
return fiat;
|
||||
} else {
|
||||
return '0.00';
|
||||
@@ -210,8 +210,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
CryptoCurrency get currency => wallet.currency;
|
||||
|
||||
Validator<String> get amountValidator =>
|
||||
AmountValidator(currency: walletTypeToCryptoCurrency(wallet.type));
|
||||
Validator<String> amountValidator(Output output) => AmountValidator(
|
||||
currency: walletTypeToCryptoCurrency(wallet.type),
|
||||
minValue: isSendToSilentPayments(output)
|
||||
?
|
||||
// TODO: get from server
|
||||
// bitcoin!.silentPaymentsMinAmount
|
||||
'0.00001'
|
||||
: null,
|
||||
);
|
||||
|
||||
Validator<String> get allAmountValidator => AllAmountValidator();
|
||||
|
||||
@@ -289,6 +296,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
// If silent payments scanning, can still send payments
|
||||
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
|
||||
|
||||
bool isSendToSilentPayments(Output output) =>
|
||||
wallet.type == WalletType.bitcoin &&
|
||||
(RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) ||
|
||||
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
|
||||
.hasMatch(output.extractedAddress) ||
|
||||
(output.parsedAddress.addresses.isNotEmpty &&
|
||||
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
|
||||
.hasMatch(output.parsedAddress.addresses[0])));
|
||||
|
||||
@computed
|
||||
List<Template> get templates => sendTemplateViewModel.templates
|
||||
.where((template) => _isEqualCurrency(template.cryptoCurrency))
|
||||
|
||||
@@ -603,6 +603,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
bool get isSilentPayments =>
|
||||
wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet);
|
||||
|
||||
@computed
|
||||
bool get isCupcake =>
|
||||
wallet.type == WalletType.bitcoin &&
|
||||
(bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isEmpty;
|
||||
|
||||
@computed
|
||||
bool get isAutoGenerateSubaddressEnabled =>
|
||||
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled &&
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
@@ -162,18 +163,21 @@ abstract class WalletKeysViewModelBase with Store {
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
|
||||
|
||||
items.addAll([
|
||||
if ((keys['wif']??'').isNotEmpty)
|
||||
StandartListItem(title: "WIF", value: keys['wif']!),
|
||||
if ((keys['privateKey']??'').isNotEmpty)
|
||||
StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
|
||||
if (keys['publicKey'] != null)
|
||||
StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
|
||||
if (keys['xpub'] != null)
|
||||
StandartListItem(title: "xPub", value: keys['xpub']!),
|
||||
]);
|
||||
break;
|
||||
case WalletType.none:
|
||||
case WalletType.haven:
|
||||
// final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
|
||||
//
|
||||
// items.addAll([
|
||||
// if (keys['wif'] != null)
|
||||
// StandartListItem(title: "WIF", value: keys['wif']!),
|
||||
// if (keys['privateKey'] != null)
|
||||
// StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
|
||||
// if (keys['publicKey'] != null)
|
||||
// StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
|
||||
// ]);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/tron/tron.dart';
|
||||
import 'package:cake_wallet/decred/decred.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
|
||||
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
|
||||
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
|
||||
@@ -32,17 +33,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService,
|
||||
Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel,
|
||||
{required WalletType type, this.restoredWallet})
|
||||
: hasSeedLanguageSelector =
|
||||
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
|
||||
hasBlockchainHeightLanguageSelector =
|
||||
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
|
||||
hasRestoreFromPrivateKey = type == WalletType.ethereum ||
|
||||
type == WalletType.polygon ||
|
||||
type == WalletType.nano ||
|
||||
type == WalletType.banano ||
|
||||
type == WalletType.solana ||
|
||||
type == WalletType.tron,
|
||||
isButtonEnabled = false,
|
||||
: isButtonEnabled = restoredWallet != null,
|
||||
hasPassphrase = false,
|
||||
mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed,
|
||||
super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel,
|
||||
@@ -60,9 +51,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.decred:
|
||||
case WalletType.bitcoin:
|
||||
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.zano:
|
||||
@@ -82,9 +73,32 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
static const decredSeedMnemonicLength = 15;
|
||||
|
||||
late List<WalletRestoreMode> availableModes;
|
||||
final bool hasSeedLanguageSelector;
|
||||
final bool hasBlockchainHeightLanguageSelector;
|
||||
final bool hasRestoreFromPrivateKey;
|
||||
late final bool hasSeedLanguageSelector = [
|
||||
WalletType.monero,
|
||||
WalletType.haven,
|
||||
WalletType.wownero
|
||||
].contains(type);
|
||||
|
||||
late final bool hasBlockchainHeightSelector = [
|
||||
WalletType.monero,
|
||||
WalletType.haven,
|
||||
WalletType.wownero
|
||||
].contains(type);
|
||||
|
||||
late final bool hasRestoreFromPrivateKey = [
|
||||
WalletType.ethereum,
|
||||
WalletType.polygon,
|
||||
WalletType.nano,
|
||||
WalletType.banano,
|
||||
WalletType.solana,
|
||||
WalletType.tron
|
||||
].contains(type);
|
||||
|
||||
late final bool onlyViewKeyRestore = [
|
||||
if (FeatureFlag.hasBitcoinViewOnly) WalletType.bitcoin,
|
||||
WalletType.decred
|
||||
].contains(type);
|
||||
|
||||
final RestoredWallet? restoredWallet;
|
||||
|
||||
@observable
|
||||
@@ -198,6 +212,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
final address = options['address'] as String?;
|
||||
|
||||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!.createBitcoinWalletFromKeys(
|
||||
name: name,
|
||||
password: password,
|
||||
xpub: viewKey!,
|
||||
);
|
||||
|
||||
case WalletType.monero:
|
||||
return monero!.createMoneroRestoreWalletFromKeysCredentials(
|
||||
name: name,
|
||||
@@ -276,8 +297,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
case WalletType.litecoin:
|
||||
String? mnemonic = credentials['seed'] as String?;
|
||||
String? passphrase = credentials['passphrase'] as String?;
|
||||
if (mnemonic == null) break;
|
||||
return bitcoin!.getDerivationsFromMnemonic(
|
||||
mnemonic: mnemonic!,
|
||||
mnemonic: mnemonic,
|
||||
node: node,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user