mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-26 21:39:21 +00:00
1244 lines
49 KiB
Dart
1244 lines
49 KiB
Dart
/*
|
|
* This file is part of Stack Wallet.
|
|
*
|
|
* Copyright (c) 2023 Cypher Stack
|
|
* All Rights Reserved.
|
|
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
|
* Generated by Cypher Stack on 2023-05-26
|
|
*
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:event_bus/event_bus.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:isar/isar.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
import '../../app_config.dart';
|
|
import '../../frost_route_generator.dart';
|
|
import '../../models/isar/exchange_cache/currency.dart';
|
|
import '../../notifications/show_flush_bar.dart';
|
|
import '../../providers/global/active_wallet_provider.dart';
|
|
import '../../providers/global/auto_swb_service_provider.dart';
|
|
import '../../providers/global/paynym_api_provider.dart';
|
|
import '../../providers/providers.dart';
|
|
import '../../providers/ui/transaction_filter_provider.dart';
|
|
import '../../providers/ui/unread_notifications_provider.dart';
|
|
import '../../providers/wallet/my_paynym_account_state_provider.dart';
|
|
import '../../services/event_bus/events/global/node_connection_status_changed_event.dart';
|
|
import '../../services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
|
import '../../services/event_bus/global_event_bus.dart';
|
|
import '../../services/exchange/exchange_data_loading_service.dart';
|
|
import '../../themes/coin_icon_provider.dart';
|
|
import '../../themes/stack_colors.dart';
|
|
import '../../themes/theme_providers.dart';
|
|
import '../../utilities/amount/amount.dart';
|
|
import '../../utilities/assets.dart';
|
|
import '../../utilities/clipboard_interface.dart';
|
|
import '../../utilities/constants.dart';
|
|
import '../../utilities/enums/backup_frequency_type.dart';
|
|
import '../../utilities/enums/sync_type_enum.dart';
|
|
import '../../utilities/logger.dart';
|
|
import '../../utilities/show_loading.dart';
|
|
import '../../utilities/text_styles.dart';
|
|
import '../../wallets/crypto_currency/crypto_currency.dart';
|
|
import '../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
|
import '../../wallets/isar/providers/wallet_info_provider.dart';
|
|
import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
|
import '../../wallets/wallet/impl/firo_wallet.dart';
|
|
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
|
import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
|
import '../../widgets/background.dart';
|
|
import '../../widgets/conditional_parent.dart';
|
|
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
|
import '../../widgets/custom_buttons/blue_text_button.dart';
|
|
import '../../widgets/custom_loading_overlay.dart';
|
|
import '../../widgets/desktop/secondary_button.dart';
|
|
import '../../widgets/frost_scaffold.dart';
|
|
import '../../widgets/loading_indicator.dart';
|
|
import '../../widgets/small_tor_icon.dart';
|
|
import '../../widgets/stack_dialog.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/churn_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart';
|
|
import '../../widgets/wallet_navigation_bar/components/wallet_navigation_bar_item.dart';
|
|
import '../../widgets/wallet_navigation_bar/wallet_navigation_bar.dart';
|
|
import '../buy_view/buy_in_wallet_view.dart';
|
|
import '../cashfusion/cashfusion_view.dart';
|
|
import '../churning/churning_view.dart';
|
|
import '../coin_control/coin_control_view.dart';
|
|
import '../exchange_view/wallet_initiated_exchange_view.dart';
|
|
import '../monkey/monkey_view.dart';
|
|
import '../notification_views/notifications_view.dart';
|
|
import '../ordinals/ordinals_view.dart';
|
|
import '../paynym/paynym_claim_view.dart';
|
|
import '../paynym/paynym_home_view.dart';
|
|
import '../receive_view/receive_view.dart';
|
|
import '../send_view/frost_ms/frost_send_view.dart';
|
|
import '../send_view/send_view.dart';
|
|
import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
|
|
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
|
|
import '../special/firo_rescan_recovery_error_dialog.dart';
|
|
import '../token_view/my_tokens_view.dart';
|
|
import 'sub_widgets/transactions_list.dart';
|
|
import 'sub_widgets/wallet_summary.dart';
|
|
import 'transaction_views/all_transactions_view.dart';
|
|
import 'transaction_views/tx_v2/all_transactions_v2_view.dart';
|
|
import 'transaction_views/tx_v2/transaction_v2_list.dart';
|
|
|
|
/// [eventBus] should only be set during testing
|
|
class WalletView extends ConsumerStatefulWidget {
|
|
const WalletView({
|
|
super.key,
|
|
required this.walletId,
|
|
this.eventBus,
|
|
this.clipboardInterface = const ClipboardWrapper(),
|
|
});
|
|
|
|
static const String routeName = "/wallet";
|
|
static const double navBarHeight = 65.0;
|
|
|
|
final String walletId;
|
|
final EventBus? eventBus;
|
|
|
|
final ClipboardInterface clipboardInterface;
|
|
|
|
@override
|
|
ConsumerState<WalletView> createState() => _WalletViewState();
|
|
}
|
|
|
|
class _WalletViewState extends ConsumerState<WalletView> {
|
|
late final EventBus eventBus;
|
|
late final String walletId;
|
|
late final CryptoCurrency coin;
|
|
|
|
late final bool isSparkWallet;
|
|
|
|
// late final bool _shouldDisableAutoSyncOnLogOut;
|
|
|
|
late WalletSyncStatus _currentSyncStatus;
|
|
late NodeConnectionStatus _currentNodeStatus;
|
|
|
|
late StreamSubscription<dynamic> _syncStatusSubscription;
|
|
late StreamSubscription<dynamic> _nodeStatusSubscription;
|
|
|
|
bool _rescanningOnOpen = false;
|
|
bool _lelantusRescanRecovery = false;
|
|
|
|
Future<void> _firoRescanRecovery() async {
|
|
final success = await (ref.read(pWallets).getWallet(walletId) as FiroWallet)
|
|
.firoRescanRecovery();
|
|
|
|
if (success) {
|
|
// go into wallet
|
|
WidgetsBinding.instance.addPostFrameCallback(
|
|
(_) => setState(() {
|
|
_rescanningOnOpen = false;
|
|
_lelantusRescanRecovery = false;
|
|
}),
|
|
);
|
|
} else {
|
|
// show error message dialog w/ options
|
|
if (mounted) {
|
|
final shouldRetry = await Navigator.of(context).pushNamed(
|
|
FiroRescanRecoveryErrorView.routeName,
|
|
arguments: walletId,
|
|
);
|
|
|
|
if (shouldRetry is bool && shouldRetry) {
|
|
await _firoRescanRecovery();
|
|
}
|
|
} else {
|
|
return await _firoRescanRecovery();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
walletId = widget.walletId;
|
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
|
coin = wallet.info.coin;
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
ref.read(currentWalletIdProvider.notifier).state = wallet.walletId;
|
|
});
|
|
|
|
if (!wallet.shouldAutoSync) {
|
|
// enable auto sync if it wasn't enabled when loading wallet
|
|
wallet.shouldAutoSync = true;
|
|
// _shouldDisableAutoSyncOnLogOut = true;
|
|
// } else {
|
|
// _shouldDisableAutoSyncOnLogOut = false;
|
|
}
|
|
|
|
isSparkWallet = wallet is SparkInterface;
|
|
|
|
if (coin is Firo && (wallet as FiroWallet).lelantusCoinIsarRescanRequired) {
|
|
_rescanningOnOpen = true;
|
|
_lelantusRescanRecovery = true;
|
|
_firoRescanRecovery();
|
|
} else {
|
|
wallet.refresh();
|
|
}
|
|
|
|
if (wallet.refreshMutex.isLocked) {
|
|
_currentSyncStatus = WalletSyncStatus.syncing;
|
|
_currentNodeStatus = NodeConnectionStatus.connected;
|
|
} else {
|
|
_currentSyncStatus = WalletSyncStatus.synced;
|
|
if (wallet.isConnected) {
|
|
_currentNodeStatus = NodeConnectionStatus.connected;
|
|
} else {
|
|
_currentNodeStatus = NodeConnectionStatus.disconnected;
|
|
_currentSyncStatus = WalletSyncStatus.unableToSync;
|
|
}
|
|
}
|
|
|
|
eventBus =
|
|
widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance;
|
|
|
|
_syncStatusSubscription =
|
|
eventBus.on<WalletSyncStatusChangedEvent>().listen(
|
|
(event) async {
|
|
if (event.walletId == widget.walletId) {
|
|
// switch (event.newStatus) {
|
|
// case WalletSyncStatus.unableToSync:
|
|
// break;
|
|
// case WalletSyncStatus.synced:
|
|
// break;
|
|
// case WalletSyncStatus.syncing:
|
|
// break;
|
|
// }
|
|
setState(() {
|
|
_currentSyncStatus = event.newStatus;
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
_nodeStatusSubscription =
|
|
eventBus.on<NodeConnectionStatusChangedEvent>().listen(
|
|
(event) async {
|
|
if (event.walletId == widget.walletId) {
|
|
// switch (event.newStatus) {
|
|
// case NodeConnectionStatus.disconnected:
|
|
// break;
|
|
// case NodeConnectionStatus.connected:
|
|
// break;
|
|
// }
|
|
setState(() {
|
|
_currentNodeStatus = event.newStatus;
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nodeStatusSubscription.cancel();
|
|
_syncStatusSubscription.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
// DateTime? _cachedTime;
|
|
|
|
Future<bool> _onWillPop() async {
|
|
if (_rescanningOnOpen || _lelantusRescanRecovery) {
|
|
return false;
|
|
}
|
|
|
|
_logout();
|
|
|
|
return true;
|
|
// final now = DateTime.now();
|
|
// const timeout = Duration(milliseconds: 1500);
|
|
// if (_cachedTime == null || now.difference(_cachedTime!) > timeout) {
|
|
// _cachedTime = now;
|
|
// unawaited(
|
|
// showDialog<dynamic>(
|
|
// context: context,
|
|
// barrierDismissible: false,
|
|
// builder: (_) => WillPopScope(
|
|
// onWillPop: () async {
|
|
// Navigator.of(context).popUntil(
|
|
// ModalRoute.withName(HomeView.routeName),
|
|
// );
|
|
// _logout();
|
|
// return false;
|
|
// },
|
|
// child: const StackDialog(title: "Tap back again to exit wallet"),
|
|
// ),
|
|
// ).timeout(
|
|
// timeout,
|
|
// onTimeout: () => Navigator.of(context).popUntil(
|
|
// ModalRoute.withName(WalletView.routeName),
|
|
// ),
|
|
// ),
|
|
// );
|
|
// }
|
|
// return false;
|
|
}
|
|
|
|
void _logout() async {
|
|
// if (_shouldDisableAutoSyncOnLogOut) {
|
|
// // disable auto sync if it was enabled only when loading wallet
|
|
ref.read(pWallets).getWallet(walletId).shouldAutoSync = false;
|
|
// }
|
|
|
|
ref.read(currentWalletIdProvider.notifier).state = null;
|
|
ref.read(transactionFilterProvider.state).state = null;
|
|
if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled &&
|
|
ref.read(prefsChangeNotifierProvider).backupFrequencyType ==
|
|
BackupFrequencyType.afterClosingAWallet) {
|
|
unawaited(ref.read(autoSWBServiceProvider).doBackup());
|
|
}
|
|
|
|
// Close the wallet according to syncing preferences.
|
|
switch (ref.read(prefsChangeNotifierProvider).syncType) {
|
|
case SyncingType.currentWalletOnly:
|
|
// Close the wallet.
|
|
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
|
// unawaited so we don't lag the UI.
|
|
case SyncingType.selectedWalletsAtStartup:
|
|
// Close if this wallet is not in the list to be synced.
|
|
if (!ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.walletIdsSyncOnStartup
|
|
.contains(widget.walletId)) {
|
|
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
|
// unawaited so we don't lag the UI.
|
|
}
|
|
case SyncingType.allWalletsOnStartup:
|
|
// Do nothing.
|
|
break;
|
|
}
|
|
}
|
|
|
|
Widget _buildNetworkIcon(WalletSyncStatus status) {
|
|
switch (status) {
|
|
case WalletSyncStatus.unableToSync:
|
|
return SvgPicture.asset(
|
|
Assets.svg.radioProblem,
|
|
color: Theme.of(context).extension<StackColors>()!.accentColorRed,
|
|
width: 20,
|
|
height: 20,
|
|
);
|
|
case WalletSyncStatus.synced:
|
|
return SvgPicture.asset(
|
|
Assets.svg.radio,
|
|
color: Theme.of(context).extension<StackColors>()!.accentColorGreen,
|
|
width: 20,
|
|
height: 20,
|
|
);
|
|
case WalletSyncStatus.syncing:
|
|
return SvgPicture.asset(
|
|
Assets.svg.radioSyncing,
|
|
color: Theme.of(context).extension<StackColors>()!.accentColorYellow,
|
|
width: 20,
|
|
height: 20,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _onFrostSignPressed(BuildContext context) async {
|
|
final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
|
|
ref.read(pFrostScaffoldArgs.state).state = (
|
|
info: (
|
|
walletName: wallet.info.name,
|
|
frostCurrency: wallet.cryptoCurrency,
|
|
),
|
|
walletId: walletId,
|
|
stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes,
|
|
parentNav: Navigator.of(context),
|
|
frostInterruptionDialogType:
|
|
FrostInterruptionDialogType.transactionCreation,
|
|
callerRouteName: WalletView.routeName,
|
|
);
|
|
|
|
await Navigator.of(context).pushNamed(
|
|
FrostStepScaffold.routeName,
|
|
);
|
|
}
|
|
|
|
Future<void> _onExchangePressed(BuildContext context) async {
|
|
final CryptoCurrency coin = ref.read(pWalletCoin(walletId));
|
|
|
|
if (coin.network.isTestNet) {
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (_) => const StackOkDialog(
|
|
title: "Exchange not available for test net coins",
|
|
),
|
|
);
|
|
} else {
|
|
Future<Currency?> _future;
|
|
try {
|
|
_future = ExchangeDataLoadingService.instance.isar.currencies
|
|
.where()
|
|
.tickerEqualToAnyExchangeNameName(coin.ticker)
|
|
.findFirst();
|
|
} catch (_) {
|
|
_future = ExchangeDataLoadingService.instance.loadAll().then(
|
|
(_) => ExchangeDataLoadingService.instance.isar.currencies
|
|
.where()
|
|
.tickerEqualToAnyExchangeNameName(coin.ticker)
|
|
.findFirst(),
|
|
);
|
|
}
|
|
|
|
final currency = await showLoading(
|
|
whileFuture: _future,
|
|
context: context,
|
|
message: "Loading...",
|
|
);
|
|
|
|
if (context.mounted) {
|
|
unawaited(
|
|
Navigator.of(context).pushNamed(
|
|
WalletInitiatedExchangeView.routeName,
|
|
arguments: Tuple2(
|
|
walletId,
|
|
currency == null ? Bitcoin(CryptoCurrencyNetwork.main) : coin,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _onBuyPressed(BuildContext context) async {
|
|
final CryptoCurrency coin = ref.read(pWalletCoin(walletId));
|
|
|
|
if (coin.network.isTestNet) {
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (_) => const StackOkDialog(
|
|
title: "Buy not available for test net coins",
|
|
),
|
|
);
|
|
} else {
|
|
if (mounted) {
|
|
unawaited(
|
|
Navigator.of(context).pushNamed(
|
|
BuyInWalletView.routeName,
|
|
arguments:
|
|
coin.hasBuySupport ? coin : Bitcoin(CryptoCurrencyNetwork.main),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> attemptAnonymize() async {
|
|
bool shouldPop = false;
|
|
unawaited(
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => WillPopScope(
|
|
child: const CustomLoadingOverlay(
|
|
message: "Anonymizing balance",
|
|
eventBus: null,
|
|
),
|
|
onWillPop: () async => shouldPop,
|
|
),
|
|
),
|
|
);
|
|
final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet;
|
|
|
|
final Amount publicBalance = firoWallet.info.cachedBalance.spendable;
|
|
if (publicBalance <= Amount.zero) {
|
|
shouldPop = true;
|
|
if (mounted) {
|
|
Navigator.of(context).popUntil(
|
|
ModalRoute.withName(WalletView.routeName),
|
|
);
|
|
unawaited(
|
|
showFloatingFlushBar(
|
|
type: FlushBarType.info,
|
|
message: "No funds available to anonymize!",
|
|
context: context,
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// await firoWallet.anonymizeAllLelantus();
|
|
await firoWallet.anonymizeAllSpark();
|
|
shouldPop = true;
|
|
if (mounted) {
|
|
Navigator.of(context).popUntil(
|
|
ModalRoute.withName(WalletView.routeName),
|
|
);
|
|
unawaited(
|
|
showFloatingFlushBar(
|
|
type: FlushBarType.success,
|
|
message: "Anonymize transaction submitted",
|
|
context: context,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
shouldPop = true;
|
|
if (mounted) {
|
|
Navigator.of(context).popUntil(
|
|
ModalRoute.withName(WalletView.routeName),
|
|
);
|
|
await showDialog<dynamic>(
|
|
context: context,
|
|
builder: (_) => StackOkDialog(
|
|
title: "Anonymize all failed",
|
|
message: "Reason: $e",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
debugPrint("BUILD: $runtimeType");
|
|
|
|
final coin = ref.watch(pWalletCoin(walletId));
|
|
|
|
final prefs = ref.watch(prefsChangeNotifierProvider);
|
|
final showExchange = prefs.enableExchange;
|
|
|
|
final wallet = ref.watch(pWallets).getWallet(walletId);
|
|
|
|
final viewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
|
|
|
|
return ConditionalParent(
|
|
condition: _rescanningOnOpen,
|
|
builder: (child) {
|
|
return WillPopScope(
|
|
onWillPop: () async => !_rescanningOnOpen,
|
|
child: Stack(
|
|
children: [
|
|
child,
|
|
Background(
|
|
child: CustomLoadingOverlay(
|
|
message:
|
|
"Migration in progress\nThis could take a while\nPlease don't leave this screen",
|
|
subMessage: "This only needs to run once per wallet",
|
|
eventBus: null,
|
|
textColor:
|
|
Theme.of(context).extension<StackColors>()!.textDark,
|
|
actionButton: _lelantusRescanRecovery
|
|
? null
|
|
: SecondaryButton(
|
|
label: "Cancel",
|
|
onPressed: () async {
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (context) => StackDialog(
|
|
title: "Warning!",
|
|
message: "Skipping this process can completely"
|
|
" break your wallet. It is only meant to be done in"
|
|
" emergency situations where the migration fails"
|
|
" and will not let you continue. Still skip?",
|
|
leftButton: SecondaryButton(
|
|
label: "Cancel",
|
|
onPressed:
|
|
Navigator.of(context, rootNavigator: true)
|
|
.pop,
|
|
),
|
|
rightButton: SecondaryButton(
|
|
label: "Ok",
|
|
onPressed: () {
|
|
Navigator.of(context, rootNavigator: true)
|
|
.pop();
|
|
setState(() => _rescanningOnOpen = false);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
child: WillPopScope(
|
|
onWillPop: _onWillPop,
|
|
child: Background(
|
|
child: Stack(
|
|
children: [
|
|
Scaffold(
|
|
backgroundColor:
|
|
Theme.of(context).extension<StackColors>()!.background,
|
|
appBar: AppBar(
|
|
leading: AppBarBackButton(
|
|
onPressed: () {
|
|
_logout();
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
titleSpacing: 0,
|
|
title: Row(
|
|
children: [
|
|
SvgPicture.file(
|
|
File(
|
|
ref.watch(coinIconProvider(coin)),
|
|
),
|
|
width: 24,
|
|
height: 24,
|
|
),
|
|
const SizedBox(
|
|
width: 16,
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
ref.watch(pWalletName(walletId)),
|
|
style: STextStyles.navBarTitle(context),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
const Padding(
|
|
padding: EdgeInsets.only(
|
|
top: 10,
|
|
bottom: 10,
|
|
right: 10,
|
|
),
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: SmallTorIcon(),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 10,
|
|
bottom: 10,
|
|
right: 10,
|
|
),
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: AppBarIconButton(
|
|
semanticsLabel:
|
|
"Network Button. Takes To Network Status Page.",
|
|
key: const Key("walletViewRadioButton"),
|
|
size: 36,
|
|
shadows: const [],
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.background,
|
|
icon: _buildNetworkIcon(_currentSyncStatus),
|
|
onPressed: () {
|
|
Navigator.of(context).pushNamed(
|
|
WalletNetworkSettingsView.routeName,
|
|
arguments: Tuple3(
|
|
walletId,
|
|
_currentSyncStatus,
|
|
_currentNodeStatus,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 10,
|
|
bottom: 10,
|
|
right: 10,
|
|
),
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: AppBarIconButton(
|
|
semanticsLabel:
|
|
"Notifications Button. Takes To Notifications Page.",
|
|
key: const Key("walletViewAlertsButton"),
|
|
size: 36,
|
|
shadows: const [],
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.background,
|
|
icon: ref.watch(
|
|
notificationsProvider.select(
|
|
(value) =>
|
|
value.hasUnreadNotificationsFor(walletId),
|
|
),
|
|
)
|
|
? SvgPicture.file(
|
|
File(
|
|
ref.watch(
|
|
themeProvider.select(
|
|
(value) => value.assets.bellNew,
|
|
),
|
|
),
|
|
),
|
|
width: 20,
|
|
height: 20,
|
|
color: ref.watch(
|
|
notificationsProvider.select(
|
|
(value) =>
|
|
value.hasUnreadNotificationsFor(
|
|
walletId,
|
|
),
|
|
),
|
|
)
|
|
? null
|
|
: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.topNavIconPrimary,
|
|
)
|
|
: SvgPicture.asset(
|
|
Assets.svg.bell,
|
|
width: 20,
|
|
height: 20,
|
|
color: ref.watch(
|
|
notificationsProvider.select(
|
|
(value) =>
|
|
value.hasUnreadNotificationsFor(
|
|
walletId,
|
|
),
|
|
),
|
|
)
|
|
? null
|
|
: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.topNavIconPrimary,
|
|
),
|
|
onPressed: () {
|
|
// reset unread state
|
|
ref.refresh(unreadNotificationsStateProvider);
|
|
|
|
Navigator.of(context)
|
|
.pushNamed(
|
|
NotificationsView.routeName,
|
|
arguments: walletId,
|
|
)
|
|
.then((_) {
|
|
final Set<int> unreadNotificationIds = ref
|
|
.read(unreadNotificationsStateProvider.state)
|
|
.state;
|
|
if (unreadNotificationIds.isEmpty) return;
|
|
|
|
final List<Future<dynamic>> futures = [];
|
|
for (int i = 0;
|
|
i < unreadNotificationIds.length - 1;
|
|
i++) {
|
|
futures.add(
|
|
ref.read(notificationsProvider).markAsRead(
|
|
unreadNotificationIds.elementAt(i),
|
|
false,
|
|
),
|
|
);
|
|
}
|
|
|
|
// wait for multiple to update if any
|
|
Future.wait(futures).then((_) {
|
|
// only notify listeners once
|
|
ref.read(notificationsProvider).markAsRead(
|
|
unreadNotificationIds.last,
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 10,
|
|
bottom: 10,
|
|
right: 10,
|
|
),
|
|
child: AspectRatio(
|
|
aspectRatio: 1,
|
|
child: AppBarIconButton(
|
|
semanticsLabel:
|
|
"Settings Button. Takes To Wallet Settings Page.",
|
|
key: const Key("walletViewSettingsButton"),
|
|
size: 36,
|
|
shadows: const [],
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.background,
|
|
icon: SvgPicture.asset(
|
|
Assets.svg.bars,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.accentColorDark,
|
|
width: 20,
|
|
height: 20,
|
|
),
|
|
onPressed: () {
|
|
//todo: check if print needed
|
|
// debugPrint("wallet view settings tapped");
|
|
Navigator.of(context).pushNamed(
|
|
WalletSettingsView.routeName,
|
|
arguments: Tuple4(
|
|
walletId,
|
|
coin,
|
|
_currentSyncStatus,
|
|
_currentNodeStatus,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: Container(
|
|
color:
|
|
Theme.of(context).extension<StackColors>()!.background,
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(
|
|
height: 10,
|
|
),
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: WalletSummary(
|
|
walletId: walletId,
|
|
aspectRatio: 1.75,
|
|
initialSyncStatus: ref
|
|
.watch(pWallets)
|
|
.getWallet(walletId)
|
|
.refreshMutex
|
|
.isLocked
|
|
? WalletSyncStatus.syncing
|
|
: WalletSyncStatus.synced,
|
|
),
|
|
),
|
|
),
|
|
if (isSparkWallet)
|
|
const SizedBox(
|
|
height: 10,
|
|
),
|
|
if (isSparkWallet)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextButton(
|
|
style: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getSecondaryEnabledButtonStyle(
|
|
context,
|
|
),
|
|
onPressed: () async {
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (context) => StackDialog(
|
|
title: "Attention!",
|
|
message:
|
|
"You're about to anonymize all of your public funds.",
|
|
leftButton: TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(
|
|
"Cancel",
|
|
style: STextStyles.button(context)
|
|
.copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.accentColorDark,
|
|
),
|
|
),
|
|
),
|
|
rightButton: TextButton(
|
|
onPressed: () async {
|
|
Navigator.of(context).pop();
|
|
|
|
unawaited(attemptAnonymize());
|
|
},
|
|
style: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getPrimaryEnabledButtonStyle(
|
|
context,
|
|
),
|
|
child: Text(
|
|
"Continue",
|
|
style:
|
|
STextStyles.button(context),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Text(
|
|
"Anonymize funds",
|
|
style:
|
|
STextStyles.button(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.buttonTextSecondary,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 20,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"Transactions",
|
|
style:
|
|
STextStyles.itemSubtitle(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark3,
|
|
),
|
|
),
|
|
CustomTextButton(
|
|
text: "See all",
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
ref
|
|
.read(pWallets)
|
|
.getWallet(widget.walletId)
|
|
.isarTransactionVersion ==
|
|
2
|
|
? AllTransactionsV2View.routeName
|
|
: AllTransactionsView.routeName,
|
|
arguments: walletId,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(
|
|
Constants.size.circularBorderRadius,
|
|
),
|
|
bottom: Radius.circular(
|
|
// WalletView.navBarHeight / 2.0,
|
|
Constants.size.circularBorderRadius,
|
|
),
|
|
),
|
|
child: ShaderMask(
|
|
blendMode: BlendMode.dstOut,
|
|
shaderCallback: (Rect bounds) {
|
|
return const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.transparent,
|
|
Colors.white,
|
|
],
|
|
stops: [
|
|
0.0,
|
|
0.8,
|
|
1.0,
|
|
],
|
|
).createShader(bounds);
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(
|
|
Constants.size.circularBorderRadius,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.stretch,
|
|
children: [
|
|
Expanded(
|
|
child: ref
|
|
.read(pWallets)
|
|
.getWallet(widget.walletId)
|
|
.isarTransactionVersion ==
|
|
2
|
|
? TransactionsV2List(
|
|
walletId: widget.walletId,
|
|
)
|
|
: TransactionsList(
|
|
walletId: walletId,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
WalletNavigationBar(
|
|
items: [
|
|
WalletNavigationBarItemData(
|
|
label: "Receive",
|
|
icon: const ReceiveNavIcon(),
|
|
onTap: () {
|
|
if (mounted) {
|
|
unawaited(
|
|
Navigator.of(context).pushNamed(
|
|
ReceiveView.routeName,
|
|
arguments: walletId,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
if (ref.watch(pWalletCoin(walletId)) is FrostCurrency)
|
|
WalletNavigationBarItemData(
|
|
label: "Sign",
|
|
icon: const FrostSignNavIcon(),
|
|
onTap: () => _onFrostSignPressed(context),
|
|
),
|
|
if (!viewOnly)
|
|
WalletNavigationBarItemData(
|
|
label: "Send",
|
|
icon: const SendNavIcon(),
|
|
onTap: () {
|
|
// not sure what this is supposed to accomplish?
|
|
// switch (ref
|
|
// .read(walletBalanceToggleStateProvider.state)
|
|
// .state) {
|
|
// case WalletBalanceToggleState.full:
|
|
// ref
|
|
// .read(publicPrivateBalanceStateProvider.state)
|
|
// .state = "Public";
|
|
// break;
|
|
// case WalletBalanceToggleState.available:
|
|
// ref
|
|
// .read(publicPrivateBalanceStateProvider.state)
|
|
// .state = "Private";
|
|
// break;
|
|
// }
|
|
Navigator.of(context).pushNamed(
|
|
wallet is BitcoinFrostWallet
|
|
? FrostSendView.routeName
|
|
: SendView.routeName,
|
|
arguments: (
|
|
walletId: walletId,
|
|
coin: coin,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (!viewOnly &&
|
|
Constants.enableExchange &&
|
|
ref.watch(pWalletCoin(walletId)) is! FrostCurrency &&
|
|
AppConfig.hasFeature(AppFeature.swap) &&
|
|
showExchange)
|
|
WalletNavigationBarItemData(
|
|
label: "Swap",
|
|
icon: const ExchangeNavIcon(),
|
|
onTap: () => _onExchangePressed(context),
|
|
),
|
|
if (Constants.enableExchange &&
|
|
ref.watch(pWalletCoin(walletId)) is! FrostCurrency &&
|
|
AppConfig.hasFeature(AppFeature.buy) &&
|
|
showExchange)
|
|
WalletNavigationBarItemData(
|
|
label: "Buy",
|
|
icon: const BuyNavIcon(),
|
|
onTap: () => _onBuyPressed(context),
|
|
),
|
|
],
|
|
moreItems: [
|
|
if (ref.watch(
|
|
pWallets.select(
|
|
(value) => value
|
|
.getWallet(widget.walletId)
|
|
.cryptoCurrency
|
|
.hasTokenSupport,
|
|
),
|
|
))
|
|
WalletNavigationBarItemData(
|
|
label: "Tokens",
|
|
icon: const CoinControlNavIcon(),
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
MyTokensView.routeName,
|
|
arguments: walletId,
|
|
);
|
|
},
|
|
),
|
|
if (coin is Banano)
|
|
WalletNavigationBarItemData(
|
|
icon: SvgPicture.asset(
|
|
Assets.svg.monkey,
|
|
height: 20,
|
|
width: 20,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.bottomNavIconIcon,
|
|
),
|
|
label: "MonKey",
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
MonkeyView.routeName,
|
|
arguments: widget.walletId,
|
|
);
|
|
},
|
|
),
|
|
if (wallet is CoinControlInterface &&
|
|
ref.watch(
|
|
prefsChangeNotifierProvider.select(
|
|
(value) => value.enableCoinControl,
|
|
),
|
|
))
|
|
WalletNavigationBarItemData(
|
|
label: "Coin control",
|
|
icon: const CoinControlNavIcon(),
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
CoinControlView.routeName,
|
|
arguments: Tuple2(
|
|
widget.walletId,
|
|
CoinControlViewType.manage,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (!viewOnly && wallet is PaynymInterface)
|
|
WalletNavigationBarItemData(
|
|
label: "PayNym",
|
|
icon: const PaynymNavIcon(),
|
|
onTap: () async {
|
|
unawaited(
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => const LoadingIndicator(
|
|
width: 100,
|
|
),
|
|
),
|
|
);
|
|
|
|
final wallet =
|
|
ref.read(pWallets).getWallet(widget.walletId);
|
|
|
|
final paynymInterface = wallet as PaynymInterface;
|
|
|
|
final code = await paynymInterface.getPaymentCode(
|
|
isSegwit: false,
|
|
);
|
|
|
|
final account = await ref
|
|
.read(paynymAPIProvider)
|
|
.nym(code.toString());
|
|
|
|
Logging.instance.log(
|
|
"my nym account: $account",
|
|
level: LogLevel.Info,
|
|
);
|
|
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop();
|
|
|
|
// check if account exists and for matching code to see if claimed
|
|
if (account.value != null &&
|
|
account.value!.nonSegwitPaymentCode.claimed
|
|
// &&
|
|
// account.value!.segwit
|
|
) {
|
|
ref.read(myPaynymAccountStateProvider.state).state =
|
|
account.value!;
|
|
|
|
await Navigator.of(context).pushNamed(
|
|
PaynymHomeView.routeName,
|
|
arguments: widget.walletId,
|
|
);
|
|
} else {
|
|
await Navigator.of(context).pushNamed(
|
|
PaynymClaimView.routeName,
|
|
arguments: widget.walletId,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
if (ref.watch(
|
|
pWallets.select(
|
|
(value) =>
|
|
value.getWallet(widget.walletId) is OrdinalsInterface,
|
|
),
|
|
))
|
|
WalletNavigationBarItemData(
|
|
label: "Ordinals",
|
|
icon: const OrdinalsNavIcon(),
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
OrdinalsView.routeName,
|
|
arguments: widget.walletId,
|
|
);
|
|
},
|
|
),
|
|
if (wallet is CashFusionInterface && !viewOnly)
|
|
WalletNavigationBarItemData(
|
|
label: "Fusion",
|
|
icon: const FusionNavIcon(),
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
CashFusionView.routeName,
|
|
arguments: walletId,
|
|
);
|
|
},
|
|
),
|
|
if (wallet is LibMoneroWallet && !viewOnly)
|
|
WalletNavigationBarItemData(
|
|
label: "Churn",
|
|
icon: const ChurnNavIcon(),
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed(
|
|
ChurningView.routeName,
|
|
arguments: walletId,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|