mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-19 18:51:04 +00:00
commit
e641238657
65 changed files with 6448 additions and 2214 deletions
4
assets/svg/enabled-button.svg
Normal file
4
assets/svg/enabled-button.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.2 KiB |
4
assets/svg/framed-address-book.svg
Normal file
4
assets/svg/framed-address-book.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
|
||||||
|
<path d="M26 8H12.5C10.843 8 9.5 9.34297 9.5 11V29C9.5 30.657 10.843 32 12.5 32H26C27.657 32 29 30.657 29 29V11C29 9.34297 27.6547 8 26 8ZM19.25 14C20.907 14 22.25 15.343 22.25 17C22.25 18.657 20.907 20 19.25 20C17.5934 20 16.25 18.657 16.25 17C16.25 15.343 17.5953 14 19.25 14ZM23.75 26H14.75C14.3375 26 14 25.6625 14 25.25C14 23.1781 15.6781 21.5 17.75 21.5H20.75C22.8209 21.5 24.5 23.1791 24.5 25.25C24.5 25.6625 24.1625 26 23.75 26ZM31.25 11H30.5V15.5H31.25C31.6625 15.5 32 15.1625 32 14.75V11.75C32 11.3356 31.6625 11 31.25 11ZM31.25 17H30.5V21.5H31.25C31.6625 21.5 32 21.1625 32 20.75V17.75C32 17.3375 31.6625 17 31.25 17ZM31.25 23H30.5V27.5H31.25C31.6642 27.5 32 27.1642 32 26.75V23.75C32 23.3375 31.6625 23 31.25 23Z" fill="#232323"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 899 B |
4
assets/svg/framed-gear.svg
Normal file
4
assets/svg/framed-gear.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="40" height="40" rx="8" fill="#E0E3E3"/>
|
||||||
|
<path d="M30.6765 16.1586C30.8183 16.5281 30.698 16.9449 30.4058 17.2156L28.5452 18.9086C28.5925 19.2652 28.6183 19.6305 28.6183 19.9613C28.6183 20.3695 28.5925 20.7348 28.5452 21.0914L30.4058 22.7844C30.698 23.0551 30.8183 23.4676 30.6765 23.8414C30.4874 24.3527 30.2597 24.8469 30.0019 25.3152L29.7999 25.6633C29.5163 26.1359 29.1984 26.5828 28.8503 27.0082C28.5925 27.3133 28.1757 27.4207 27.7976 27.3004L25.4042 26.5355C24.8284 26.9781 24.1538 27.3477 23.5136 27.6313L22.9765 30.0848C22.8906 30.4715 22.5898 30.7465 22.1945 30.8496C21.6015 30.9484 20.9913 31 20.3296 31C19.7452 31 19.1351 30.9484 18.5421 30.8496C18.1468 30.7465 17.846 30.4715 17.7601 30.0848L17.223 27.6313C16.5441 27.3477 15.9081 26.9781 15.3323 26.5355L12.9407 27.3004C12.5609 27.4207 12.1419 27.3133 11.8875 27.0082C11.5391 26.5828 11.2211 26.1359 10.9375 25.6633L10.7364 25.3152C10.4756 24.8469 10.2487 24.3527 10.0584 23.8414C9.91914 23.4719 10.0364 23.0551 10.3312 22.7844L12.19 21.0914C12.1428 20.7348 12.1183 20.3695 12.1183 20C12.1183 19.6305 12.1428 19.2652 12.19 18.9086L10.3312 17.2156C10.0364 16.9449 9.91914 16.5324 10.0584 16.1586C10.2487 15.6473 10.476 15.1531 10.7364 14.6848L10.9371 14.3367C11.2207 13.8641 11.5391 13.4172 11.8875 12.9939C12.1419 12.6867 12.5609 12.5802 12.9407 12.7013L15.3323 13.4645C15.9081 13.0202 16.5441 12.6506 17.223 12.37L17.7601 9.91652C17.846 9.52637 18.1468 9.21656 18.5421 9.15082C19.1351 9.05161 19.7452 9 20.3296 9C20.9913 9 21.6015 9.05161 22.1945 9.15082C22.5898 9.21656 22.8906 9.52637 22.9765 9.91652L23.5136 12.37C24.1538 12.6506 24.8284 13.0202 25.4042 13.4645L27.7976 12.7013C28.1757 12.5802 28.5925 12.6867 28.8503 12.9939C29.1984 13.4172 29.5163 13.8641 29.7999 14.3367L30.0019 14.6848C30.2597 15.1531 30.4874 15.6473 30.6765 16.1586ZM20.3683 23.4375C22.2675 23.4375 23.8058 21.8992 23.8058 19.9613C23.8058 18.1008 22.2675 16.5238 20.3683 16.5238C18.4691 16.5238 16.9308 18.1008 16.9308 19.9613C16.9308 21.8992 18.4691 23.4375 20.3683 23.4375Z" fill="#232323"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -1 +1 @@
|
||||||
Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42
|
Subproject commit 2da77438527732dfaa5398aa391eab5253dabe19
|
|
@ -30,7 +30,8 @@ import 'package:stackwallet/pages/loading_view.dart';
|
||||||
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
|
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
|
||||||
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
|
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/desktop_login_view.dart';
|
||||||
|
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
|
||||||
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
|
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
|
||||||
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
|
import 'package:stackwallet/providers/global/base_currencies_provider.dart';
|
||||||
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
|
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
|
||||||
|
@ -207,6 +208,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
|
||||||
late final Completer<void> loadingCompleter;
|
late final Completer<void> loadingCompleter;
|
||||||
|
|
||||||
bool didLoad = false;
|
bool didLoad = false;
|
||||||
|
bool _desktopHasPassword = false;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
try {
|
try {
|
||||||
|
@ -218,6 +220,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
|
||||||
await DB.instance.init();
|
await DB.instance.init();
|
||||||
await _prefs.init();
|
await _prefs.init();
|
||||||
|
|
||||||
|
if (Util.isDesktop) {
|
||||||
|
_desktopHasPassword =
|
||||||
|
await ref.read(storageCryptoHandlerProvider).hasPassword();
|
||||||
|
}
|
||||||
|
|
||||||
_notificationsService = ref.read(notificationsProvider);
|
_notificationsService = ref.read(notificationsProvider);
|
||||||
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
|
_nodeService = ref.read(nodeServiceChangeNotifierProvider);
|
||||||
_tradesService = ref.read(tradesServiceProvider);
|
_tradesService = ref.read(tradesServiceProvider);
|
||||||
|
@ -545,21 +552,23 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
|
||||||
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
|
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
// FlutterNativeSplash.remove();
|
// FlutterNativeSplash.remove();
|
||||||
if (_wallets.hasWallets || _prefs.hasPin) {
|
if (Util.isDesktop &&
|
||||||
// return HomeView();
|
(_wallets.hasWallets || _desktopHasPassword)) {
|
||||||
|
|
||||||
String? startupWalletId;
|
String? startupWalletId;
|
||||||
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
|
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
|
||||||
startupWalletId =
|
startupWalletId =
|
||||||
ref.read(prefsChangeNotifierProvider).startupWalletId;
|
ref.read(prefsChangeNotifierProvider).startupWalletId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO proper desktop auth view
|
return DesktopLoginView(startupWalletId: startupWalletId);
|
||||||
if (Util.isDesktop) {
|
} else if (!Util.isDesktop &&
|
||||||
Future<void>.delayed(Duration.zero).then((value) =>
|
(_wallets.hasWallets || _prefs.hasPin)) {
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
// return HomeView();
|
||||||
DesktopHomeView.routeName, (route) => false));
|
|
||||||
return Container();
|
String? startupWalletId;
|
||||||
|
if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) {
|
||||||
|
startupWalletId =
|
||||||
|
ref.read(prefsChangeNotifierProvider).startupWalletId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return LockscreenView(
|
return LockscreenView(
|
||||||
|
|
|
@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: isDesktop ? 40 : 24,
|
height: isDesktop ? 40 : 24,
|
||||||
),
|
),
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
Text(
|
Text(
|
||||||
"Choose start date",
|
"Choose start date",
|
||||||
style: isDesktop
|
style: isDesktop
|
||||||
|
@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
||||||
: STextStyles.smallMed12(context),
|
: STextStyles.smallMed12(context),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: isDesktop ? 16 : 8,
|
height: isDesktop ? 16 : 8,
|
||||||
),
|
),
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
|
|
||||||
// if (!isDesktop)
|
// if (!isDesktop)
|
||||||
RestoreFromDatePicker(
|
RestoreFromDatePicker(
|
||||||
|
@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
||||||
|
|
||||||
// if (isDesktop)
|
// if (isDesktop)
|
||||||
// // TODO desktop date picker
|
// // TODO desktop date picker
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (coin == Coin.monero || coin == Coin.epicCash)
|
if (coin == Coin.monero ||
|
||||||
|
coin == Coin.epicCash ||
|
||||||
|
(coin == Coin.wownero &&
|
||||||
|
ref.watch(mnemonicWordCountStateProvider.state).state ==
|
||||||
|
25))
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: isDesktop ? 24 : 16,
|
height: isDesktop ? 24 : 16,
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:bip39/src/wordlists/english.dart' as bip39wordlist;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_libmonero/monero/monero.dart';
|
import 'package:flutter_libmonero/monero/monero.dart';
|
||||||
|
import 'package:flutter_libmonero/wownero/wownero.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
|
@ -149,12 +150,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: check for wownero wordlist?
|
||||||
bool _isValidMnemonicWord(String word) {
|
bool _isValidMnemonicWord(String word) {
|
||||||
// TODO: get the actual language
|
// TODO: get the actual language
|
||||||
if (widget.coin == Coin.monero) {
|
if (widget.coin == Coin.monero) {
|
||||||
var moneroWordList = monero.getMoneroWordList("English");
|
var moneroWordList = monero.getMoneroWordList("English");
|
||||||
return moneroWordList.contains(word);
|
return moneroWordList.contains(word);
|
||||||
}
|
}
|
||||||
|
if (widget.coin == Coin.wownero) {
|
||||||
|
var wowneroWordList = wownero.getWowneroWordList("English",
|
||||||
|
seedWordsLength: widget.seedWordsLength);
|
||||||
|
return wowneroWordList.contains(word);
|
||||||
|
}
|
||||||
return _wordListHashSet.contains(word);
|
return _wordListHashSet.contains(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +187,13 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
||||||
|
|
||||||
if (widget.coin == Coin.monero) {
|
if (widget.coin == Coin.monero) {
|
||||||
height = monero.getHeigthByDate(date: widget.restoreFromDate);
|
height = monero.getHeigthByDate(date: widget.restoreFromDate);
|
||||||
|
} else if (widget.coin == Coin.wownero) {
|
||||||
|
height = wownero.getHeightByDate(date: widget.restoreFromDate);
|
||||||
}
|
}
|
||||||
|
// todo: wait until this implemented
|
||||||
|
// else if (widget.coin == Coin.wownero) {
|
||||||
|
// height = wownero.getHeightByDate(date: widget.restoreFromDate);
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
|
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
|
||||||
if (widget.coin == Coin.epicCash) {
|
if (widget.coin == Coin.epicCash) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
|
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/route_generator.dart';
|
import 'package:stackwallet/route_generator.dart';
|
||||||
|
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/format.dart';
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
this.routeOnSuccessName = WalletView.routeName,
|
this.routeOnSuccessName = WalletView.routeName,
|
||||||
required this.trade,
|
required this.trade,
|
||||||
|
this.shouldSendPublicFiroFunds,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const String routeName = "/confirmChangeNowSend";
|
static const String routeName = "/confirmChangeNowSend";
|
||||||
|
@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget {
|
||||||
final String walletId;
|
final String walletId;
|
||||||
final String routeOnSuccessName;
|
final String routeOnSuccessName;
|
||||||
final Trade trade;
|
final Trade trade;
|
||||||
|
final bool? shouldSendPublicFiroFunds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConfirmChangeNowSendView> createState() =>
|
ConsumerState<ConfirmChangeNowSendView> createState() =>
|
||||||
|
@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState
|
||||||
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
ref.read(walletsChangeNotifierProvider).getManager(walletId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final txid = await manager.confirmSend(txData: transactionInfo);
|
late final String txid;
|
||||||
|
|
||||||
|
if (widget.shouldSendPublicFiroFunds == true) {
|
||||||
|
txid = await (manager.wallet as FiroWallet)
|
||||||
|
.confirmSendPublic(txData: transactionInfo);
|
||||||
|
} else {
|
||||||
|
txid = await manager.confirmSend(txData: transactionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
unawaited(manager.refresh());
|
unawaited(manager.refresh());
|
||||||
|
|
||||||
// save note
|
// save note
|
||||||
|
|
|
@ -10,6 +10,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
|
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
|
||||||
import 'package:stackwallet/providers/providers.dart';
|
import 'package:stackwallet/providers/providers.dart';
|
||||||
import 'package:stackwallet/route_generator.dart';
|
import 'package:stackwallet/route_generator.dart';
|
||||||
|
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
||||||
|
import 'package:stackwallet/services/coins/manager.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
@ -18,7 +20,9 @@ import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
import 'package:stackwallet/widgets/animated_text.dart';
|
import 'package:stackwallet/widgets/animated_text.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/expandable.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
@ -162,45 +166,14 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
late final String address;
|
late final String address;
|
||||||
late final Trade trade;
|
late final Trade trade;
|
||||||
|
|
||||||
@override
|
Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async {
|
||||||
void initState() {
|
|
||||||
walletId = widget.walletId;
|
|
||||||
amount = widget.amount;
|
|
||||||
address = widget.address;
|
|
||||||
trade = widget.trade;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final manager = ref.watch(ref
|
|
||||||
.watch(walletsChangeNotifierProvider.notifier)
|
|
||||||
.getManagerProvider(walletId));
|
|
||||||
|
|
||||||
final locale = ref.watch(
|
|
||||||
localeServiceChangeNotifierProvider.select((value) => value.locale));
|
|
||||||
|
|
||||||
final coin = manager.coin;
|
|
||||||
|
|
||||||
return RoundedWhiteContainer(
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
child: MaterialButton(
|
|
||||||
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
|
||||||
key: Key("walletsSheetItemButtonKey_$walletId"),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Constants.size.circularBorderRadius,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
final _amount = Format.decimalAmountToSatoshis(amount);
|
final _amount = Format.decimalAmountToSatoshis(amount);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bool wasCancelled = false;
|
bool wasCancelled = false;
|
||||||
|
|
||||||
unawaited(showDialog<dynamic>(
|
unawaited(
|
||||||
|
showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: false,
|
useSafeArea: false,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
|
@ -213,9 +186,14 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final txData = await manager.prepareSend(
|
late Map<String, dynamic> txData;
|
||||||
|
|
||||||
|
// if not firo then do normal send
|
||||||
|
if (shouldSendPublicFiroFunds == null) {
|
||||||
|
txData = await manager.prepareSend(
|
||||||
address: address,
|
address: address,
|
||||||
satoshiAmount: _amount,
|
satoshiAmount: _amount,
|
||||||
args: {
|
args: {
|
||||||
|
@ -223,6 +201,29 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
// ref.read(feeRateTypeStateProvider)
|
// ref.read(feeRateTypeStateProvider)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
final firoWallet = manager.wallet as FiroWallet;
|
||||||
|
// otherwise do firo send based on balance selected
|
||||||
|
if (shouldSendPublicFiroFunds) {
|
||||||
|
txData = await firoWallet.prepareSendPublic(
|
||||||
|
address: address,
|
||||||
|
satoshiAmount: _amount,
|
||||||
|
args: {
|
||||||
|
"feeRate": FeeRateType.average,
|
||||||
|
// ref.read(feeRateTypeStateProvider)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
txData = await firoWallet.prepareSend(
|
||||||
|
address: address,
|
||||||
|
satoshiAmount: _amount,
|
||||||
|
args: {
|
||||||
|
"feeRate": FeeRateType.average,
|
||||||
|
// ref.read(feeRateTypeStateProvider)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!wasCancelled) {
|
if (!wasCancelled) {
|
||||||
// pop building dialog
|
// pop building dialog
|
||||||
|
@ -244,6 +245,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
routeOnSuccessName: HomeView.routeName,
|
routeOnSuccessName: HomeView.routeName,
|
||||||
trade: trade,
|
trade: trade,
|
||||||
|
shouldSendPublicFiroFunds: shouldSendPublicFiroFunds,
|
||||||
),
|
),
|
||||||
settings: const RouteSettings(
|
settings: const RouteSettings(
|
||||||
name: ConfirmChangeNowSendView.routeName,
|
name: ConfirmChangeNowSendView.routeName,
|
||||||
|
@ -286,7 +288,225 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
);
|
);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
walletId = widget.walletId;
|
||||||
|
amount = widget.amount;
|
||||||
|
address = widget.address;
|
||||||
|
trade = widget.trade;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final manager = ref.watch(ref
|
||||||
|
.watch(walletsChangeNotifierProvider.notifier)
|
||||||
|
.getManagerProvider(walletId));
|
||||||
|
|
||||||
|
final locale = ref.watch(
|
||||||
|
localeServiceChangeNotifierProvider.select((value) => value.locale));
|
||||||
|
|
||||||
|
final coin = manager.coin;
|
||||||
|
|
||||||
|
final isFiro = coin == Coin.firoTestNet || coin == Coin.firo;
|
||||||
|
|
||||||
|
return RoundedWhiteContainer(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: isFiro,
|
||||||
|
builder: (child) => Expandable(
|
||||||
|
header: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MaterialButton(
|
||||||
|
splashColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
|
key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"),
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _send(
|
||||||
|
manager,
|
||||||
|
shouldSendPublicFiroFunds: false,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 6,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Use private balance",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: (manager.wallet as FiroWallet)
|
||||||
|
.availablePrivateBalance(),
|
||||||
|
builder: (builderContext,
|
||||||
|
AsyncSnapshot<Decimal> snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
return Text(
|
||||||
|
"${Format.localizedStringAsFixed(
|
||||||
|
value: snapshot.data!,
|
||||||
|
locale: locale,
|
||||||
|
decimalPlaces: Constants.decimalPlaces,
|
||||||
|
)} ${coin.ticker}",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return AnimatedText(
|
||||||
|
stringsToLoopThrough: const [
|
||||||
|
"Loading balance",
|
||||||
|
"Loading balance.",
|
||||||
|
"Loading balance..",
|
||||||
|
"Loading balance..."
|
||||||
|
],
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.chevronRight,
|
||||||
|
height: 14,
|
||||||
|
width: 7,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemLabel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MaterialButton(
|
||||||
|
splashColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
|
key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"),
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _send(
|
||||||
|
manager,
|
||||||
|
shouldSendPublicFiroFunds: true,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 6,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Use public balance",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: (manager.wallet as FiroWallet)
|
||||||
|
.availablePublicBalance(),
|
||||||
|
builder: (builderContext,
|
||||||
|
AsyncSnapshot<Decimal> snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
return Text(
|
||||||
|
"${Format.localizedStringAsFixed(
|
||||||
|
value: snapshot.data!,
|
||||||
|
locale: locale,
|
||||||
|
decimalPlaces: Constants.decimalPlaces,
|
||||||
|
)} ${coin.ticker}",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return AnimatedText(
|
||||||
|
stringsToLoopThrough: const [
|
||||||
|
"Loading balance",
|
||||||
|
"Loading balance.",
|
||||||
|
"Loading balance..",
|
||||||
|
"Loading balance..."
|
||||||
|
],
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.chevronRight,
|
||||||
|
height: 14,
|
||||||
|
width: 7,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemLabel,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 6,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !isFiro,
|
||||||
|
builder: (child) => MaterialButton(
|
||||||
|
splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
|
key: Key("walletsSheetItemButtonKey_$walletId"),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () => _send(manager),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
@ -320,13 +540,17 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
manager.walletName,
|
manager.walletName,
|
||||||
style: STextStyles.titleBold12(context),
|
style: STextStyles.titleBold12(context),
|
||||||
),
|
),
|
||||||
|
if (!isFiro)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 2,
|
height: 2,
|
||||||
),
|
),
|
||||||
|
if (!isFiro)
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: manager.totalBalance,
|
future: manager.totalBalance,
|
||||||
builder: (builderContext, AsyncSnapshot<Decimal> snapshot) {
|
builder:
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
(builderContext, AsyncSnapshot<Decimal> snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done &&
|
||||||
snapshot.hasData) {
|
snapshot.hasData) {
|
||||||
return Text(
|
return Text(
|
||||||
"${Format.localizedStringAsFixed(
|
"${Format.localizedStringAsFixed(
|
||||||
|
@ -359,6 +583,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
|
import 'package:stackwallet/utilities/address_utils.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/clipboard_interface.dart';
|
import 'package:stackwallet/utilities/clipboard_interface.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
@ -101,26 +102,29 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String query = "";
|
Map<String, String> queryParams = {};
|
||||||
|
|
||||||
if (amountString.isNotEmpty) {
|
if (amountString.isNotEmpty) {
|
||||||
query += "amount=$amountString";
|
queryParams["amount"] = amountString;
|
||||||
}
|
}
|
||||||
if (noteString.isNotEmpty) {
|
if (noteString.isNotEmpty) {
|
||||||
if (query.isNotEmpty) {
|
queryParams["message"] = noteString;
|
||||||
query += "&";
|
|
||||||
}
|
|
||||||
query += "message=$noteString";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final uri = Uri(
|
String receivingAddress = widget.receivingAddress;
|
||||||
scheme: widget.coin.uriScheme,
|
if ((widget.coin == Coin.bitcoincash ||
|
||||||
host: widget.receivingAddress,
|
widget.coin == Coin.bitcoincashTestnet) &&
|
||||||
query: query.isNotEmpty ? query : null,
|
receivingAddress.contains(":")) {
|
||||||
|
// remove cash addr prefix
|
||||||
|
receivingAddress = receivingAddress.split(":").sublist(1).join();
|
||||||
|
}
|
||||||
|
|
||||||
|
final uriString = AddressUtils.buildUriString(
|
||||||
|
widget.coin,
|
||||||
|
receivingAddress,
|
||||||
|
queryParams,
|
||||||
);
|
);
|
||||||
|
|
||||||
final uriString = uri.toString().replaceFirst("://", ":");
|
|
||||||
|
|
||||||
Logging.instance.log("Generated receiving QR code for: $uriString",
|
Logging.instance.log("Generated receiving QR code for: $uriString",
|
||||||
level: LogLevel.Info);
|
level: LogLevel.Info);
|
||||||
|
|
||||||
|
@ -229,10 +233,21 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
isDesktop = Util.isDesktop;
|
isDesktop = Util.isDesktop;
|
||||||
_uriString = Uri(
|
|
||||||
scheme: widget.coin.uriScheme,
|
String receivingAddress = widget.receivingAddress;
|
||||||
host: widget.receivingAddress,
|
if ((widget.coin == Coin.bitcoincash ||
|
||||||
).toString().replaceFirst("://", ":");
|
widget.coin == Coin.bitcoincashTestnet) &&
|
||||||
|
receivingAddress.contains(":")) {
|
||||||
|
// remove cash addr prefix
|
||||||
|
receivingAddress = receivingAddress.split(":").sublist(1).join();
|
||||||
|
}
|
||||||
|
|
||||||
|
_uriString = AddressUtils.buildUriString(
|
||||||
|
widget.coin,
|
||||||
|
receivingAddress,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
amountController = TextEditingController();
|
amountController = TextEditingController();
|
||||||
noteController = TextEditingController();
|
noteController = TextEditingController();
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
|
@ -87,13 +87,13 @@ class _ConfirmTransactionViewState
|
||||||
txid = await manager.confirmSend(txData: transactionInfo);
|
txid = await manager.confirmSend(txData: transactionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(manager.refresh());
|
|
||||||
|
|
||||||
// save note
|
// save note
|
||||||
await ref
|
await ref
|
||||||
.read(notesServiceChangeNotifierProvider(walletId))
|
.read(notesServiceChangeNotifierProvider(walletId))
|
||||||
.editOrAddNote(txid: txid, note: note);
|
.editOrAddNote(txid: txid, note: note);
|
||||||
|
|
||||||
|
unawaited(manager.refresh());
|
||||||
|
|
||||||
// pop back to wallet
|
// pop back to wallet
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
|
Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName));
|
||||||
|
|
|
@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
||||||
ref.read(nodeFormDataProvider).useSSL = false;
|
ref.read(nodeFormDataProvider).useSSL = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.cert != null) {
|
||||||
|
if (mounted) {
|
||||||
|
final shouldAllowBadCert = await showBadX509CertificateDialog(
|
||||||
|
response.cert!,
|
||||||
|
response.url!,
|
||||||
|
response.port!,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldAllowBadCert) {
|
||||||
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString), true);
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
||||||
|
|
||||||
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.cert != null) {
|
||||||
|
if (mounted) {
|
||||||
|
final shouldAllowBadCert = await showBadX509CertificateDialog(
|
||||||
|
response.cert!,
|
||||||
|
response.url!,
|
||||||
|
response.port!,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldAllowBadCert) {
|
||||||
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString), true);
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -15,7 +15,10 @@ import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
import 'package:stackwallet/utilities/util.dart';
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
import 'package:stackwallet/widgets/progress_bar.dart';
|
import 'package:stackwallet/widgets/progress_bar.dart';
|
||||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
@ -93,8 +96,14 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: !isDesktop,
|
||||||
|
builder: (child) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarBackButton(
|
leading: AppBarBackButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
@ -122,6 +131,36 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
minHeight: constraints.maxHeight,
|
minHeight: constraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: isDesktop,
|
||||||
|
builder: (child) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text(
|
||||||
|
"Choose file location",
|
||||||
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
@ -139,8 +178,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
await stackFileSystem.prepareStorage();
|
await stackFileSystem.prepareStorage();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
await stackFileSystem
|
await stackFileSystem.pickDir(context);
|
||||||
.pickDir(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
@ -150,8 +188,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s",
|
Logging.instance
|
||||||
level: LogLevel.Error);
|
.log("$e\n$s", level: LogLevel.Error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: fileLocationController,
|
controller: fileLocationController,
|
||||||
|
@ -180,8 +218,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
key: const Key(
|
key:
|
||||||
"createBackupSaveToFileLocationTextFieldKey"),
|
const Key("createBackupSaveToFileLocationTextFieldKey"),
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
toolbarOptions: const ToolbarOptions(
|
toolbarOptions: const ToolbarOptions(
|
||||||
copy: true,
|
copy: true,
|
||||||
|
@ -196,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
if (!Platform.isAndroid)
|
if (!Platform.isAndroid)
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8,
|
height: !isDesktop ? 8 : 24,
|
||||||
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: Text(
|
||||||
|
"Create a passphrase",
|
||||||
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
@ -216,6 +267,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
passwordFocusNode,
|
passwordFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -231,9 +284,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
hidePassword
|
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
|
||||||
? Assets.svg.eye
|
|
||||||
: Assets.svg.eyeSlash,
|
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark3,
|
.textDark3,
|
||||||
|
@ -257,8 +308,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
}
|
}
|
||||||
final result = zxcvbn.evaluate(newValue);
|
final result = zxcvbn.evaluate(newValue);
|
||||||
String suggestionsAndTips = "";
|
String suggestionsAndTips = "";
|
||||||
for (var sug
|
for (var sug in result.feedback.suggestions!.toSet()) {
|
||||||
in result.feedback.suggestions!.toSet()) {
|
|
||||||
suggestionsAndTips += "$sug\n";
|
suggestionsAndTips += "$sug\n";
|
||||||
}
|
}
|
||||||
suggestionsAndTips += result.feedback.warning!;
|
suggestionsAndTips += result.feedback.warning!;
|
||||||
|
@ -275,8 +325,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedback.endsWith("\n")) {
|
if (feedback.endsWith("\n")) {
|
||||||
feedback =
|
feedback = feedback.substring(0, feedback.length - 2);
|
||||||
feedback.substring(0, feedback.length - 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -328,9 +377,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
backgroundColor: Theme.of(context)
|
backgroundColor: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.buttonBackSecondary,
|
.buttonBackSecondary,
|
||||||
percent: passwordStrength < 0.25
|
percent: passwordStrength < 0.25 ? 0.03 : passwordStrength,
|
||||||
? 0.03
|
|
||||||
: passwordStrength,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -353,6 +400,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
passwordRepeatFocusNode,
|
passwordRepeatFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -368,9 +417,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
hidePassword
|
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
|
||||||
? Assets.svg.eye
|
|
||||||
: Assets.svg.eyeSlash,
|
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark3,
|
.textDark3,
|
||||||
|
@ -394,8 +441,9 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (!isDesktop) const Spacer(),
|
||||||
TextButton(
|
!isDesktop
|
||||||
|
? TextButton(
|
||||||
style: shouldEnableCreate
|
style: shouldEnableCreate
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
|
@ -403,6 +451,114 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
: Theme.of(context)
|
: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.getPrimaryDisabledButtonColor(context),
|
.getPrimaryDisabledButtonColor(context),
|
||||||
|
onPressed: !shouldEnableCreate
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final String pathToSave =
|
||||||
|
fileLocationController.text;
|
||||||
|
final String passphrase = passwordController.text;
|
||||||
|
final String repeatPassphrase =
|
||||||
|
passwordRepeatController.text;
|
||||||
|
|
||||||
|
if (pathToSave.isEmpty) {
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Directory not chosen",
|
||||||
|
context: context,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await Directory(pathToSave).exists())) {
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Directory does not exist",
|
||||||
|
context: context,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passphrase.isEmpty) {
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "A passphrase is required",
|
||||||
|
context: context,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passphrase != repeatPassphrase) {
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Passphrase does not match",
|
||||||
|
context: context,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const StackDialog(
|
||||||
|
title: "Encrypting backup",
|
||||||
|
message: "This shouldn't take long",
|
||||||
|
),
|
||||||
|
));
|
||||||
|
// make sure the dialog is able to be displayed for at least 1 second
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final String fileToSave =
|
||||||
|
"$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb";
|
||||||
|
|
||||||
|
final backup = await SWB.createStackWalletJSON();
|
||||||
|
|
||||||
|
bool result =
|
||||||
|
await SWB.encryptStackWalletWithPassphrase(
|
||||||
|
fileToSave,
|
||||||
|
passphrase,
|
||||||
|
jsonEncode(backup),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// pop encryption progress dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => Platform.isAndroid
|
||||||
|
? StackOkDialog(
|
||||||
|
title: "Backup saved to:",
|
||||||
|
message: fileToSave,
|
||||||
|
)
|
||||||
|
: const StackOkDialog(
|
||||||
|
title: "Backup creation succeeded"),
|
||||||
|
);
|
||||||
|
passwordController.text = "";
|
||||||
|
passwordRepeatController.text = "";
|
||||||
|
setState(() {});
|
||||||
|
} else {
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const StackOkDialog(
|
||||||
|
title: "Backup creation failed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Create backup",
|
||||||
|
style: STextStyles.button(context),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Create backup",
|
||||||
|
enabled: shouldEnableCreate,
|
||||||
onPressed: !shouldEnableCreate
|
onPressed: !shouldEnableCreate
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
@ -502,17 +658,19 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
|
||||||
"Create backup",
|
|
||||||
style: STextStyles.button(context),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Cancel",
|
||||||
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -8,6 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart';
|
||||||
|
// import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart';
|
||||||
import 'package:stackwallet/route_generator.dart';
|
import 'package:stackwallet/route_generator.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
@ -15,13 +17,17 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import 'package:stackwallet/utilities/util.dart';
|
|
||||||
|
|
||||||
class RestoreFromFileView extends ConsumerStatefulWidget {
|
class RestoreFromFileView extends ConsumerStatefulWidget {
|
||||||
const RestoreFromFileView({Key? key}) : super(key: key);
|
const RestoreFromFileView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -42,6 +48,17 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
|
|
||||||
bool hidePassword = true;
|
bool hidePassword = true;
|
||||||
|
|
||||||
|
Future<void> restoreBackupPopup(BuildContext context) async {
|
||||||
|
// await showDialog<dynamic>(
|
||||||
|
// context: context,
|
||||||
|
// useSafeArea: false,
|
||||||
|
// barrierDismissible: true,
|
||||||
|
// builder: (context) {
|
||||||
|
// return const RestoreBackupDialog();
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
stackFileSystem = StackFileSystem();
|
stackFileSystem = StackFileSystem();
|
||||||
|
@ -65,14 +82,21 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: !isDesktop,
|
||||||
|
builder: (child) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarBackButton(
|
leading: AppBarBackButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (FocusScope.of(context).hasFocus) {
|
if (FocusScope.of(context).hasFocus) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 75));
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -94,6 +118,37 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
minHeight: constraints.maxHeight,
|
minHeight: constraints.maxHeight,
|
||||||
),
|
),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: isDesktop,
|
||||||
|
builder: (child) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: Text(
|
||||||
|
"Choose file location",
|
||||||
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
@ -114,8 +169,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log("$e\n$s", level: LogLevel.Error);
|
||||||
.log("$e\n$s", level: LogLevel.Error);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: fileLocationController,
|
controller: fileLocationController,
|
||||||
|
@ -154,8 +208,21 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
),
|
),
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 8,
|
height: !isDesktop ? 8 : 24,
|
||||||
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
|
child: Text(
|
||||||
|
"Enter passphrase",
|
||||||
|
style: STextStyles.desktopTextExtraExtraSmall(context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
@ -174,6 +241,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
passwordFocusNode,
|
passwordFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
|
labelStyle:
|
||||||
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -214,8 +283,9 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (!isDesktop) const Spacer(),
|
||||||
TextButton(
|
!isDesktop
|
||||||
|
? TextButton(
|
||||||
style: passwordController.text.isEmpty ||
|
style: passwordController.text.isEmpty ||
|
||||||
fileLocationController.text.isEmpty
|
fileLocationController.text.isEmpty
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
|
@ -230,8 +300,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
: () async {
|
: () async {
|
||||||
final String fileToRestore =
|
final String fileToRestore =
|
||||||
fileLocationController.text;
|
fileLocationController.text;
|
||||||
final String passphrase =
|
final String passphrase = passwordController.text;
|
||||||
passwordController.text;
|
|
||||||
|
|
||||||
if (FocusScope.of(context).hasFocus) {
|
if (FocusScope.of(context).hasFocus) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
@ -240,7 +309,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await File(fileToRestore).exists())) {
|
if (!(await File(fileToRestore).exists())) {
|
||||||
showFloatingFlushBar(
|
await showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Backup file does not exist",
|
message: "Backup file does not exist",
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -249,6 +318,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldPop = false;
|
bool shouldPop = false;
|
||||||
|
unawaited(
|
||||||
showDialog<dynamic>(
|
showDialog<dynamic>(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -288,6 +358,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final String? jsonString = await compute(
|
final String? jsonString = await compute(
|
||||||
|
@ -304,7 +375,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
passwordController.text = "";
|
passwordController.text = "";
|
||||||
|
|
||||||
if (jsonString == null) {
|
if (jsonString == null) {
|
||||||
showFloatingFlushBar(
|
await showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Failed to decrypt backup file",
|
message: "Failed to decrypt backup file",
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -312,7 +383,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
RouteGenerator.getRoute(
|
RouteGenerator.getRoute(
|
||||||
builder: (_) => StackRestoreProgressView(
|
builder: (_) => StackRestoreProgressView(
|
||||||
jsonString: jsonString,
|
jsonString: jsonString,
|
||||||
|
@ -325,6 +396,169 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
"Restore",
|
"Restore",
|
||||||
style: STextStyles.button(context),
|
style: STextStyles.button(context),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Restore",
|
||||||
|
enabled: !(passwordController.text.isEmpty ||
|
||||||
|
fileLocationController.text.isEmpty),
|
||||||
|
onPressed: passwordController.text.isEmpty ||
|
||||||
|
fileLocationController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final String fileToRestore =
|
||||||
|
fileLocationController.text;
|
||||||
|
final String passphrase =
|
||||||
|
passwordController.text;
|
||||||
|
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await File(fileToRestore).exists())) {
|
||||||
|
await showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Backup file does not exist",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldPop = false;
|
||||||
|
unawaited(
|
||||||
|
showDialog<dynamic>(
|
||||||
|
barrierDismissible: false,
|
||||||
|
context: context,
|
||||||
|
builder: (_) => WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
return shouldPop;
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"Decrypting Stack backup file",
|
||||||
|
style:
|
||||||
|
STextStyles.pageTitleH2(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textWhite,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: LoadingIndicator(
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final String? jsonString = await compute(
|
||||||
|
SWB.decryptStackWalletWithPassphrase,
|
||||||
|
Tuple2(fileToRestore, passphrase),
|
||||||
|
debugLabel:
|
||||||
|
"stack wallet decryption compute",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// pop LoadingIndicator
|
||||||
|
shouldPop = true;
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
rootNavigator: true,
|
||||||
|
).pop();
|
||||||
|
|
||||||
|
passwordController.text = "";
|
||||||
|
|
||||||
|
if (jsonString == null) {
|
||||||
|
await showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message:
|
||||||
|
"Failed to decrypt backup file",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
useSafeArea: false,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (context) {
|
||||||
|
return DesktopDialog(
|
||||||
|
maxHeight: 750,
|
||||||
|
maxWidth: 600,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight:
|
||||||
|
constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment
|
||||||
|
.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment
|
||||||
|
.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets
|
||||||
|
.all(32),
|
||||||
|
child: Text(
|
||||||
|
"Restoring Stack Wallet",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopH3(
|
||||||
|
context),
|
||||||
|
textAlign:
|
||||||
|
TextAlign
|
||||||
|
.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const DesktopDialogCloseButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 30,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets
|
||||||
|
.symmetric(
|
||||||
|
horizontal:
|
||||||
|
32),
|
||||||
|
child:
|
||||||
|
StackRestoreProgressView(
|
||||||
|
jsonString:
|
||||||
|
jsonString,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -333,7 +567,24 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
width: 183,
|
||||||
|
desktopMed: true,
|
||||||
|
label: "Cancel",
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart';
|
||||||
|
@ -17,6 +16,8 @@ import 'package:stackwallet/utilities/enums/stack_restoring_status.dart';
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
|
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
|
||||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||||
|
@ -39,6 +40,8 @@ class StackRestoreProgressView extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _StackRestoreProgressViewState
|
class _StackRestoreProgressViewState
|
||||||
extends ConsumerState<StackRestoreProgressView> {
|
extends ConsumerState<StackRestoreProgressView> {
|
||||||
|
bool isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
Future<void> _cancel() async {
|
Future<void> _cancel() async {
|
||||||
bool shouldPop = false;
|
bool shouldPop = false;
|
||||||
unawaited(showDialog<void>(
|
unawaited(showDialog<void>(
|
||||||
|
@ -79,10 +82,15 @@ class _StackRestoreProgressViewState
|
||||||
|
|
||||||
await SWB.cancelRestore();
|
await SWB.cancelRestore();
|
||||||
shouldPop = true;
|
shouldPop = true;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile
|
!isDesktop
|
||||||
|
? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile
|
||||||
? RestoreFromEncryptedStringView.routeName
|
? RestoreFromEncryptedStringView.routeName
|
||||||
: StackBackupView.routeName));
|
: StackBackupView.routeName))
|
||||||
|
: Navigator.of(context).popUntil((_) => count++ >= 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,16 +187,23 @@ class _StackRestoreProgressViewState
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
bool isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: !isDesktop,
|
||||||
|
builder: (child) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: _onWillPop,
|
onWillPop: _onWillPop,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarBackButton(
|
leading: AppBarBackButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (FocusScope.of(context).hasFocus) {
|
if (FocusScope.of(context).hasFocus) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 75));
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
}
|
}
|
||||||
if (_success) {
|
if (_success) {
|
||||||
_addWalletsToHomeView();
|
_addWalletsToHomeView();
|
||||||
|
@ -213,6 +228,11 @@ class _StackRestoreProgressViewState
|
||||||
top: 12,
|
top: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
),
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
|
@ -422,21 +442,14 @@ class _StackRestoreProgressViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 80,
|
height: 30,
|
||||||
),
|
),
|
||||||
],
|
SizedBox(
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (_success) {
|
if (_success) {
|
||||||
_addWalletsToHomeView();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context)
|
|
||||||
.popUntil(ModalRoute.withName(HomeView.routeName));
|
|
||||||
} else {
|
} else {
|
||||||
if (await _requestCancel()) {
|
if (await _requestCancel()) {
|
||||||
await _cancel();
|
await _cancel();
|
||||||
|
@ -456,6 +469,9 @@ class _StackRestoreProgressViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@ import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
@ -18,10 +21,16 @@ class SupportView extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: !isDesktop,
|
||||||
|
builder: (child) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarBackButton(
|
leading: AppBarBackButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -35,6 +44,10 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
@ -44,7 +57,11 @@ class SupportView extends StatelessWidget {
|
||||||
style: STextStyles.smallMed12(context),
|
style: STextStyles.smallMed12(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
isDesktop
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
|
@ -58,10 +75,12 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://t.me/stackwallet"),
|
Uri.parse("https://t.me/stackwallet"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -69,6 +88,9 @@ class SupportView extends StatelessWidget {
|
||||||
vertical: 20,
|
vertical: 20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(
|
||||||
Assets.socials.telegram,
|
Assets.socials.telegram,
|
||||||
|
@ -88,6 +110,17 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "@stackwallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://t.me/stackwallet"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -105,10 +138,12 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://discord.gg/RZMG3yUm"),
|
Uri.parse("https://discord.gg/RZMG3yUm"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -116,6 +151,9 @@ class SupportView extends StatelessWidget {
|
||||||
vertical: 20,
|
vertical: 20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(
|
||||||
Assets.socials.discord,
|
Assets.socials.discord,
|
||||||
|
@ -135,6 +173,18 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "Stack Wallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://discord.gg/RZMG3yUm"), //expired link?
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -152,10 +202,12 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -163,6 +215,9 @@ class SupportView extends StatelessWidget {
|
||||||
vertical: 20,
|
vertical: 20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(
|
||||||
Assets.socials.reddit,
|
Assets.socials.reddit,
|
||||||
|
@ -182,6 +237,17 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "r/stackwallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://www.reddit.com/r/stackwallet/"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -199,10 +265,12 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("https://twitter.com/stack_wallet"),
|
Uri.parse("https://twitter.com/stack_wallet"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -210,6 +278,9 @@ class SupportView extends StatelessWidget {
|
||||||
vertical: 20,
|
vertical: 20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(
|
||||||
Assets.socials.twitter,
|
Assets.socials.twitter,
|
||||||
|
@ -229,6 +300,17 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "@stack_wallet" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("https://twitter.com/stack_wallet"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -246,10 +328,12 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!isDesktop) {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
Uri.parse("mailto://support@stackwallet.com"),
|
Uri.parse("mailto://support@stackwallet.com"),
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -257,6 +341,9 @@ class SupportView extends StatelessWidget {
|
||||||
vertical: 20,
|
vertical: 20,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset(
|
||||||
Assets.svg.envelope,
|
Assets.svg.envelope,
|
||||||
|
@ -276,12 +363,22 @@ class SupportView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: isDesktop ? "support@stackwallet.com" : "",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse("mailto://support@stackwallet.com"),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -471,7 +471,8 @@ class _TransactionDetailsViewState
|
||||||
MainAxisAlignment.spaceBetween,
|
MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -484,7 +485,8 @@ class _TransactionDetailsViewState
|
||||||
? STextStyles
|
? STextStyles
|
||||||
.desktopTextExtraExtraSmall(
|
.desktopTextExtraExtraSmall(
|
||||||
context)
|
context)
|
||||||
: STextStyles.itemSubtitle(context),
|
: STextStyles.itemSubtitle(
|
||||||
|
context),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
|
@ -500,7 +502,8 @@ class _TransactionDetailsViewState
|
||||||
String addressOrContactName =
|
String addressOrContactName =
|
||||||
_transaction.address;
|
_transaction.address;
|
||||||
if (snapshot.connectionState ==
|
if (snapshot.connectionState ==
|
||||||
ConnectionState.done &&
|
ConnectionState
|
||||||
|
.done &&
|
||||||
snapshot.hasData) {
|
snapshot.hasData) {
|
||||||
addressOrContactName =
|
addressOrContactName =
|
||||||
snapshot.data!;
|
snapshot.data!;
|
||||||
|
@ -536,11 +539,13 @@ class _TransactionDetailsViewState
|
||||||
StackColors>()!
|
StackColors>()!
|
||||||
.textDark,
|
.textDark,
|
||||||
)
|
)
|
||||||
: STextStyles.itemSubtitle12(
|
: STextStyles
|
||||||
|
.itemSubtitle12(
|
||||||
context),
|
context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (isDesktop)
|
if (isDesktop)
|
||||||
IconCopyButton(
|
IconCopyButton(
|
||||||
data: _transaction.address,
|
data: _transaction.address,
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
||||||
|
@ -18,7 +20,7 @@ import 'package:stackwallet/widgets/progress_bar.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
import 'package:zxcvbn/zxcvbn.dart';
|
import 'package:zxcvbn/zxcvbn.dart';
|
||||||
|
|
||||||
class CreatePasswordView extends StatefulWidget {
|
class CreatePasswordView extends ConsumerStatefulWidget {
|
||||||
const CreatePasswordView({
|
const CreatePasswordView({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.secureStore = const SecureStorageWrapper(
|
this.secureStore = const SecureStorageWrapper(
|
||||||
|
@ -31,10 +33,10 @@ class CreatePasswordView extends StatefulWidget {
|
||||||
final FlutterSecureStorageInterface secureStore;
|
final FlutterSecureStorageInterface secureStore;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreatePasswordView> createState() => _CreatePasswordViewState();
|
ConsumerState<CreatePasswordView> createState() => _CreatePasswordViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreatePasswordViewState extends State<CreatePasswordView> {
|
class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
|
||||||
late final TextEditingController passwordController;
|
late final TextEditingController passwordController;
|
||||||
late final TextEditingController passwordRepeatController;
|
late final TextEditingController passwordRepeatController;
|
||||||
|
|
||||||
|
@ -76,8 +78,16 @@ class _CreatePasswordViewState extends State<CreatePasswordView> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await widget.secureStore
|
try {
|
||||||
.write(key: "stackDesktopPassword", value: passphrase);
|
await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase);
|
||||||
|
} catch (e) {
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Error: $e",
|
||||||
|
context: context,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
unawaited(Navigator.of(context)
|
unawaited(Navigator.of(context)
|
||||||
|
|
184
lib/pages_desktop_specific/desktop_login_view.dart
Normal file
184
lib/pages_desktop_specific/desktop_login_view.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
|
||||||
|
class DesktopLoginView extends StatefulWidget {
|
||||||
|
const DesktopLoginView({
|
||||||
|
Key? key,
|
||||||
|
this.startupWalletId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/desktopLogin";
|
||||||
|
|
||||||
|
final String? startupWalletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DesktopLoginView> createState() => _DesktopLoginViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopLoginViewState extends State<DesktopLoginView> {
|
||||||
|
late final TextEditingController passwordController;
|
||||||
|
|
||||||
|
late final FocusNode passwordFocusNode;
|
||||||
|
|
||||||
|
bool hidePassword = true;
|
||||||
|
bool _continueEnabled = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
passwordController = TextEditingController();
|
||||||
|
passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
passwordController.dispose();
|
||||||
|
passwordFocusNode.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DesktopScaffold(
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.stackIcon(context),
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Stack Wallet",
|
||||||
|
style: STextStyles.desktopH1(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 350,
|
||||||
|
child: Text(
|
||||||
|
"Open source multicoin wallet for everyone",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: STextStyles.desktopSubtitleH1(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("desktopLoginPasswordFieldKey"),
|
||||||
|
focusNode: passwordFocusNode,
|
||||||
|
controller: passwordController,
|
||||||
|
style: STextStyles.desktopTextMedium(context).copyWith(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
obscureText: hidePassword,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter password",
|
||||||
|
passwordFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
suffixIcon: UnconstrainedBox(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
key: const Key(
|
||||||
|
"restoreFromFilePasswordFieldShowPasswordButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
hidePassword = !hidePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {
|
||||||
|
_continueEnabled = passwordController.text.isNotEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
enabled: _continueEnabled,
|
||||||
|
onPressed: () {
|
||||||
|
// todo auth
|
||||||
|
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
DesktopHomeView.routeName,
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: "Forgot password?",
|
||||||
|
textSize: 20,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
ForgotPasswordDesktopView.routeName,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal file
101
lib/pages_desktop_specific/forgot_password_desktop_view.dart
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
|
|
||||||
|
class ForgotPasswordDesktopView extends StatefulWidget {
|
||||||
|
const ForgotPasswordDesktopView({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/forgotPasswordDesktop";
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ForgotPasswordDesktopView> createState() =>
|
||||||
|
_ForgotPasswordDesktopViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DesktopScaffold(
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
isCompactHeight: false,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.stackIcon(context),
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 42,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Stack Wallet",
|
||||||
|
style: STextStyles.desktopH1(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Text(
|
||||||
|
"Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: STextStyles.desktopTextSmall(context).copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textSubtitle1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Create new wallet",
|
||||||
|
onPressed: () {
|
||||||
|
// // todo delete everything and start fresh?
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
label: "Restore from backup",
|
||||||
|
onPressed: () {
|
||||||
|
// todo SWB restore
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: kDesktopAppBarHeight,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class DesktopAddressBook extends ConsumerStatefulWidget {
|
||||||
|
const DesktopAddressBook({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/desktopAddressBook";
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DesktopAddressBook> createState() => _DesktopAddressBook();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> {
|
||||||
|
late final TextEditingController _searchController;
|
||||||
|
|
||||||
|
late final FocusNode _searchFocusNode;
|
||||||
|
|
||||||
|
String filter = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_searchController = TextEditingController();
|
||||||
|
_searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DesktopAppBar(
|
||||||
|
isCompactHeight: true,
|
||||||
|
leading: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Address Book",
|
||||||
|
style: STextStyles.desktopH3(context),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 53),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 60,
|
||||||
|
width: 489,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _searchController,
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
onChanged: (newString) {
|
||||||
|
setState(() => filter = newString);
|
||||||
|
},
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Search...",
|
||||||
|
_searchFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
labelStyle: STextStyles.fieldLabel(context)
|
||||||
|
.copyWith(fontSize: 16),
|
||||||
|
prefixIcon: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.search,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
TextFieldIconButton(
|
||||||
|
child: const XIcon(),
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_searchController.text = "";
|
||||||
|
filter = "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Expanded(
|
||||||
|
// child: hasWallets ? const MyWallets() : const EmptyWallets(),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
|
||||||
import 'package:stackwallet/route_generator.dart';
|
import 'package:stackwallet/route_generator.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
|
||||||
|
@ -29,19 +32,25 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> {
|
||||||
Container(
|
Container(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
Container(
|
const Navigator(
|
||||||
color: Colors.orange,
|
key: Key("desktopAddressBookHomeKey"),
|
||||||
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
|
initialRoute: DesktopAddressBook.routeName,
|
||||||
),
|
),
|
||||||
const Navigator(
|
const Navigator(
|
||||||
key: Key("desktopSettingHomeKey"),
|
key: Key("desktopSettingHomeKey"),
|
||||||
onGenerateRoute: RouteGenerator.generateRoute,
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
initialRoute: DesktopSettingsView.routeName,
|
initialRoute: DesktopSettingsView.routeName,
|
||||||
),
|
),
|
||||||
Container(
|
const Navigator(
|
||||||
color: Colors.blue,
|
key: Key("desktopSupportHomeKey"),
|
||||||
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
|
initialRoute: DesktopSupportView.routeName,
|
||||||
),
|
),
|
||||||
Container(
|
const Navigator(
|
||||||
color: Colors.pink,
|
key: Key("desktopAboutHomeKey"),
|
||||||
|
onGenerateRoute: RouteGenerator.generateRoute,
|
||||||
|
initialRoute: DesktopAboutView.routeName,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:stackwallet/models/isar/models/log.dart';
|
import 'package:stackwallet/models/isar/models/log.dart';
|
||||||
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
import 'package:stackwallet/providers/global/debug_service_provider.dart';
|
import 'package:stackwallet/providers/global/debug_service_provider.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
||||||
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
|
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
@ -105,7 +109,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 24,
|
// flex: 24,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
floatHeaderSlivers: true,
|
floatHeaderSlivers: true,
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||||
|
@ -314,7 +318,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
// const Spacer(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -322,7 +326,18 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SecondaryButton(
|
child: SecondaryButton(
|
||||||
label: "Clear logs",
|
label: "Clear logs",
|
||||||
onPressed: () {},
|
onPressed: () async {
|
||||||
|
await ref.read(debugServiceProvider).deleteAllMessages();
|
||||||
|
await ref.read(debugServiceProvider).updateRecentLogs();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
unawaited(showFloatingFlushBar(
|
||||||
|
type: FlushBarType.info,
|
||||||
|
context: context,
|
||||||
|
message: 'Logs cleared!'));
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stackwallet/hive/db.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/ui/color_theme_provider.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/color_theme.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/dark_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/light_colors.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
|
||||||
import '../../../providers/global/prefs_provider.dart';
|
|
||||||
import '../../../utilities/constants.dart';
|
|
||||||
import '../../../widgets/custom_buttons/draggable_switch_button.dart';
|
|
||||||
|
|
||||||
class AppearanceOptionSettings extends ConsumerStatefulWidget {
|
class AppearanceOptionSettings extends ConsumerStatefulWidget {
|
||||||
const AppearanceOptionSettings({Key? key}) : super(key: key);
|
const AppearanceOptionSettings({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -23,6 +26,19 @@ class AppearanceOptionSettings extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _AppearanceOptionSettings
|
class _AppearanceOptionSettings
|
||||||
extends ConsumerState<AppearanceOptionSettings> {
|
extends ConsumerState<AppearanceOptionSettings> {
|
||||||
|
// late bool isLight;
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// void initState() {
|
||||||
|
//
|
||||||
|
// super.initState();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void dispose() {
|
||||||
|
// super.dispose();
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
@ -128,10 +144,7 @@ class _AppearanceOptionSettings
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.all(10),
|
||||||
left: 10,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
child: ThemeToggle(),
|
child: ThemeToggle(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -143,7 +156,7 @@ class _AppearanceOptionSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemeToggle extends StatefulWidget {
|
class ThemeToggle extends ConsumerStatefulWidget {
|
||||||
const ThemeToggle({
|
const ThemeToggle({
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -152,47 +165,113 @@ class ThemeToggle extends StatefulWidget {
|
||||||
// final void Function(bool)? onChanged;
|
// final void Function(bool)? onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ThemeToggle();
|
ConsumerState<ThemeToggle> createState() => _ThemeToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThemeToggle extends State<ThemeToggle> {
|
class _ThemeToggle extends ConsumerState<ThemeToggle> {
|
||||||
// late bool externalCallsEnabled;
|
// late bool externalCallsEnabled;
|
||||||
|
|
||||||
|
late String _selectedTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_selectedTheme =
|
||||||
|
DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme")
|
||||||
|
as String? ??
|
||||||
|
"light";
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
MaterialButton(
|
||||||
padding: const EdgeInsets.all(8.0),
|
splashColor: Colors.transparent,
|
||||||
child: RawMaterialButton(
|
|
||||||
elevation: 0,
|
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius * 2,
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {}, //onPressed
|
onPressed: () {
|
||||||
child: Stack(
|
DB.instance.put<dynamic>(
|
||||||
|
boxName: DB.boxNameTheme,
|
||||||
|
key: "colorScheme",
|
||||||
|
value: ThemeType.light.name,
|
||||||
|
);
|
||||||
|
ref.read(colorThemeProvider.state).state =
|
||||||
|
StackColors.fromStackColorTheme(
|
||||||
|
LightColors(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedTheme = "light";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
decoration: BoxDecoration(
|
||||||
children: [
|
border: Border.all(
|
||||||
Padding(
|
width: 2.5,
|
||||||
padding: const EdgeInsets.only(
|
color: _selectedTheme == "light"
|
||||||
left: 24,
|
? Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemIcons
|
||||||
|
: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
Assets.svg.themeLight,
|
Assets.svg.themeLight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
const SizedBox(
|
||||||
padding: const EdgeInsets.only(
|
height: 12,
|
||||||
left: 50,
|
|
||||||
top: 12,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: Radio(
|
||||||
|
activeColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.radioButtonIconEnabled,
|
||||||
|
value: "light",
|
||||||
|
groupValue: _selectedTheme,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue is String && newValue == "light") {
|
||||||
|
DB.instance.put<dynamic>(
|
||||||
|
boxName: DB.boxNameTheme,
|
||||||
|
key: "colorScheme",
|
||||||
|
value: ThemeType.light.name,
|
||||||
|
);
|
||||||
|
ref.read(colorThemeProvider.state).state =
|
||||||
|
StackColors.fromStackColorTheme(
|
||||||
|
LightColors(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedTheme = "light";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
"Light",
|
"Light",
|
||||||
style:
|
style:
|
||||||
STextStyles.desktopTextExtraSmall(context).copyWith(
|
STextStyles.desktopTextExtraSmall(context).copyWith(
|
||||||
|
@ -201,114 +280,114 @@ class _ThemeToggle extends State<ThemeToggle> {
|
||||||
.textDark,
|
.textDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// if (externalCallsEnabled)
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 6,
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
Assets.svg.checkCircle,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.infoItemIcons,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// if (!externalCallsEnabled)
|
|
||||||
// Positioned(
|
|
||||||
// top: 4,
|
|
||||||
// right: 4,
|
|
||||||
// child: Container(
|
|
||||||
// width: 20,
|
|
||||||
// height: 20,
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// borderRadius: BorderRadius.circular(1000),
|
|
||||||
// color: Theme.of(context)
|
|
||||||
// .extension<StackColors>()!
|
|
||||||
// .textFieldDefaultBG,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 1,
|
width: 20,
|
||||||
),
|
),
|
||||||
Expanded(
|
MaterialButton(
|
||||||
child: Padding(
|
splashColor: Colors.transparent,
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: RawMaterialButton(
|
|
||||||
elevation: 0,
|
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius * 2,
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {}, //onPressed
|
onPressed: () {
|
||||||
child: Stack(
|
DB.instance.put<dynamic>(
|
||||||
|
boxName: DB.boxNameTheme,
|
||||||
|
key: "colorScheme",
|
||||||
|
value: ThemeType.dark.name,
|
||||||
|
);
|
||||||
|
ref.read(colorThemeProvider.state).state =
|
||||||
|
StackColors.fromStackColorTheme(
|
||||||
|
DarkColors(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedTheme = "dark";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
decoration: BoxDecoration(
|
||||||
children: [
|
border: Border.all(
|
||||||
SvgPicture.asset(
|
width: 2.5,
|
||||||
|
color: _selectedTheme == "dark"
|
||||||
|
? Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.infoItemIcons
|
||||||
|
: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SvgPicture.asset(
|
||||||
Assets.svg.themeDark,
|
Assets.svg.themeDark,
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 50,
|
|
||||||
top: 12,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: Radio(
|
||||||
|
activeColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.radioButtonIconEnabled,
|
||||||
|
value: "dark",
|
||||||
|
groupValue: _selectedTheme,
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue is String && newValue == "dark") {
|
||||||
|
DB.instance.put<dynamic>(
|
||||||
|
boxName: DB.boxNameTheme,
|
||||||
|
key: "colorScheme",
|
||||||
|
value: ThemeType.dark.name,
|
||||||
|
);
|
||||||
|
ref.read(colorThemeProvider.state).state =
|
||||||
|
StackColors.fromStackColorTheme(
|
||||||
|
DarkColors(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedTheme = "dark";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
"Dark",
|
"Dark",
|
||||||
style: STextStyles.desktopTextExtraSmall(context)
|
style:
|
||||||
.copyWith(
|
STextStyles.desktopTextExtraSmall(context).copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark,
|
.textDark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// if (externalCallsEnabled)
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
Assets.svg.checkCircle,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.infoItemIcons,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// if (!externalCallsEnabled)
|
|
||||||
// Positioned(
|
|
||||||
// top: 4,
|
|
||||||
// right: 4,
|
|
||||||
// child: Container(
|
|
||||||
// width: 20,
|
|
||||||
// height: 20,
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// borderRadius: BorderRadius.circular(1000),
|
|
||||||
// color: Theme.of(context)
|
|
||||||
// .extension<StackColors>()!
|
|
||||||
// .textFieldDefaultBG,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,28 @@ import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart';
|
import 'package:stackwallet/providers/global/locale_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||||
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../../../providers/global/auto_swb_service_provider.dart';
|
||||||
|
import '../../../../widgets/custom_buttons/blue_text_button.dart';
|
||||||
|
|
||||||
class BackupRestoreSettings extends ConsumerStatefulWidget {
|
class BackupRestoreSettings extends ConsumerStatefulWidget {
|
||||||
const BackupRestoreSettings({Key? key}) : super(key: key);
|
const BackupRestoreSettings({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -21,206 +35,52 @@ class BackupRestoreSettings extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
|
class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
|
||||||
@override
|
late bool createBackup = false;
|
||||||
Widget build(BuildContext context) {
|
late bool restoreBackup = false;
|
||||||
debugPrint("BUILD: $runtimeType");
|
|
||||||
return ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 30,
|
|
||||||
),
|
|
||||||
child: RoundedWhiteContainer(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.svg.backupAuto,
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: RichText(
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: "Auto Backup",
|
|
||||||
style: STextStyles.desktopTextSmall(context),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data."
|
|
||||||
"To ensure maximum security, we recommend using a unique password that you haven't used anywhere "
|
|
||||||
"else on the internet before. Your password is not stored.",
|
|
||||||
style:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"\n\nFor more information, please see our website ",
|
|
||||||
style:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: "stackwallet.com",
|
|
||||||
style: STextStyles.richLink(context)
|
|
||||||
.copyWith(fontSize: 14),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () {
|
|
||||||
launchUrl(
|
|
||||||
Uri.parse("https://stackwallet.com/"),
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: const [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
child: AutoBackupButton(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 25,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 30,
|
|
||||||
),
|
|
||||||
child: RoundedWhiteContainer(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.svg.backupAdd,
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: RichText(
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: "Manual Backup",
|
|
||||||
style: STextStyles.desktopTextSmall(context),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"\n\nCreate manual backup to easily transfer your data between devices. "
|
|
||||||
"You will create a backup file that can be later used in the Restore option. "
|
|
||||||
"Use a strong password to encrypt your data.",
|
|
||||||
style:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: const [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
child: ManualBackupButton(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 25,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 30,
|
|
||||||
),
|
|
||||||
child: RoundedWhiteContainer(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.svg.backupRestore,
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: RichText(
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: "Restore Backup",
|
|
||||||
style: STextStyles.desktopTextSmall(context),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"\n\nUse your Stack Wallet backup file to restore your wallets, address book "
|
|
||||||
"and wallet preferences.",
|
|
||||||
style:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: const [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
child: RestoreBackupButton(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AutoBackupButton extends ConsumerWidget {
|
final toggleController = DSBController();
|
||||||
const AutoBackupButton({
|
|
||||||
Key? key,
|
late final TextEditingController fileLocationController;
|
||||||
}) : super(key: key);
|
late final TextEditingController passwordController;
|
||||||
@override
|
late final TextEditingController frequencyController;
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
Future<void> enableAutoBackup() async {
|
late final FocusNode fileLocationFocusNode;
|
||||||
|
late final FocusNode passwordFocusNode;
|
||||||
|
|
||||||
|
String prettySinceLastBackupString(DateTime? time) {
|
||||||
|
if (time == null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
final difference = DateTime.now().difference(time);
|
||||||
|
int value;
|
||||||
|
String postfix;
|
||||||
|
if (difference < const Duration(seconds: 60)) {
|
||||||
|
value = difference.inSeconds;
|
||||||
|
postfix = "seconds";
|
||||||
|
} else if (difference < const Duration(minutes: 60)) {
|
||||||
|
value = difference.inMinutes;
|
||||||
|
postfix = "minutes";
|
||||||
|
} else if (difference < const Duration(hours: 24)) {
|
||||||
|
value = difference.inHours;
|
||||||
|
postfix = "hours";
|
||||||
|
} else if (difference.inDays < 8) {
|
||||||
|
value = difference.inDays;
|
||||||
|
postfix = "days";
|
||||||
|
} else {
|
||||||
|
// if greater than a week return the actual date
|
||||||
|
return DateFormat.yMMMMd(
|
||||||
|
ref.read(localeServiceChangeNotifierProvider).locale)
|
||||||
|
.format(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == 1) {
|
||||||
|
postfix = postfix.substring(0, postfix.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$value $postfix ago";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enableAutoBackup(BuildContext context) async {
|
||||||
await showDialog<dynamic>(
|
await showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: false,
|
useSafeArea: false,
|
||||||
|
@ -231,80 +91,460 @@ class AutoBackupButton extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
Future<void> createAutoBackup() async {
|
||||||
width: 200,
|
|
||||||
height: 48,
|
|
||||||
child: TextButton(
|
|
||||||
style: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.getPrimaryEnabledButtonColor(context),
|
|
||||||
onPressed: () {
|
|
||||||
enableAutoBackup();
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Enable auto backup",
|
|
||||||
style: STextStyles.button(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManualBackupButton extends ConsumerWidget {
|
|
||||||
const ManualBackupButton({
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return SizedBox(
|
|
||||||
width: 200,
|
|
||||||
height: 48,
|
|
||||||
child: TextButton(
|
|
||||||
style: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.getPrimaryEnabledButtonColor(context),
|
|
||||||
onPressed: () {},
|
|
||||||
child: Text(
|
|
||||||
"Create manual backup",
|
|
||||||
style: STextStyles.button(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestoreBackupButton extends ConsumerWidget {
|
|
||||||
const RestoreBackupButton({
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
Future<void> restoreBackup() async {
|
|
||||||
await showDialog<dynamic>(
|
await showDialog<dynamic>(
|
||||||
context: context,
|
context: context,
|
||||||
useSafeArea: false,
|
useSafeArea: false,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return const RestoreBackupDialog();
|
return CreateAutoBackup();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
Future<void> attemptDisable() async {
|
||||||
width: 200,
|
final result = await showDialog<bool?>(
|
||||||
height: 48,
|
context: context,
|
||||||
child: TextButton(
|
useSafeArea: false,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (context) {
|
||||||
|
return StackDialog(
|
||||||
|
title: "Disable Auto Backup",
|
||||||
|
message:
|
||||||
|
"You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.",
|
||||||
|
leftButton: TextButton(
|
||||||
|
style: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.getSecondaryEnabledButtonColor(context),
|
||||||
|
child: Text(
|
||||||
|
"Back",
|
||||||
|
style: STextStyles.button(context).copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(context).extension<StackColors>()!.accentColorDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
rightButton: TextButton(
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.getPrimaryEnabledButtonColor(context),
|
.getPrimaryEnabledButtonColor(context),
|
||||||
onPressed: () {
|
|
||||||
restoreBackup();
|
|
||||||
},
|
|
||||||
child: Text(
|
child: Text(
|
||||||
"Restore",
|
"Disable",
|
||||||
style: STextStyles.button(context),
|
style: STextStyles.button(context),
|
||||||
),
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
setState(() {
|
||||||
|
ref.watch(prefsChangeNotifierProvider).isAutoBackupEnabled =
|
||||||
|
false;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
if (result is bool && result) {
|
||||||
|
ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = false;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
toggleController.activate?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
fileLocationController = TextEditingController();
|
||||||
|
passwordController = TextEditingController();
|
||||||
|
frequencyController = TextEditingController();
|
||||||
|
|
||||||
|
passwordController.text = "---------------";
|
||||||
|
fileLocationController.text =
|
||||||
|
ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? " ";
|
||||||
|
frequencyController.text = Format.prettyFrequencyType(
|
||||||
|
ref.read(prefsChangeNotifierProvider).backupFrequencyType);
|
||||||
|
|
||||||
|
fileLocationFocusNode = FocusNode();
|
||||||
|
passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
// _toggle = ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
fileLocationController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
frequencyController.dispose();
|
||||||
|
|
||||||
|
fileLocationFocusNode.dispose();
|
||||||
|
passwordFocusNode.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
|
||||||
|
bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider
|
||||||
|
.select((value) => value.isAutoBackupEnabled));
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
prefsChangeNotifierProvider
|
||||||
|
.select((value) => value.backupFrequencyType),
|
||||||
|
(previous, BackupFrequencyType next) {
|
||||||
|
frequencyController.text = Format.prettyFrequencyType(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 30,
|
||||||
|
),
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.svg.backupAuto,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
isEnabledAutoBackup
|
||||||
|
? SvgPicture.asset(
|
||||||
|
Assets.svg.enableButton,
|
||||||
|
)
|
||||||
|
: SvgPicture.asset(
|
||||||
|
Assets.svg.disableButton,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: RichText(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: "Auto Backup",
|
||||||
|
style: STextStyles.desktopTextSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data."
|
||||||
|
"To ensure maximum security, we recommend using a unique password that you haven't used anywhere "
|
||||||
|
"else on the internet before. Your password is not stored.",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"\n\nFor more information, please see our website ",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: "stackwallet.com",
|
||||||
|
style: STextStyles.richLink(context)
|
||||||
|
.copyWith(fontSize: 14),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://stackwallet.com/"),
|
||||||
|
mode: LaunchMode
|
||||||
|
.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: !isEnabledAutoBackup
|
||||||
|
? PrimaryButton(
|
||||||
|
desktopMed: true,
|
||||||
|
width: 200,
|
||||||
|
label: "Enable auto backup",
|
||||||
|
onPressed: () {
|
||||||
|
enableAutoBackup(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 403,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment
|
||||||
|
.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}",
|
||||||
|
style: STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text: "Back up now",
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
autoSWBServiceProvider)
|
||||||
|
.doBackup();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
desktopMed: true,
|
||||||
|
width: 190,
|
||||||
|
label: "Disable auto backup",
|
||||||
|
onPressed: () {
|
||||||
|
attemptDisable();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
SecondaryButton(
|
||||||
|
desktopMed: true,
|
||||||
|
width: 190,
|
||||||
|
label: "Edit auto backup",
|
||||||
|
onPressed: () {
|
||||||
|
createAutoBackup();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 30,
|
||||||
|
),
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.backupAdd,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: RichText(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: "Manual Backup",
|
||||||
|
style: STextStyles.desktopTextSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"\n\nCreate manual backup to easily transfer your data between devices. "
|
||||||
|
"You will create a backup file that can be later used in the Restore option. "
|
||||||
|
"Use a strong password to encrypt your data.",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
child: createBackup
|
||||||
|
? const SizedBox(
|
||||||
|
width: 512,
|
||||||
|
child: CreateBackupView(),
|
||||||
|
)
|
||||||
|
: PrimaryButton(
|
||||||
|
desktopMed: true,
|
||||||
|
width: 200,
|
||||||
|
label: "Create manual backup",
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
createBackup = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 30,
|
||||||
|
bottom: 40,
|
||||||
|
),
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.backupRestore,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: RichText(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: "Restore Backup",
|
||||||
|
style: STextStyles.desktopTextSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"\n\nUse your Stack Wallet backup file to restore your wallets, address book "
|
||||||
|
"and wallet preferences.",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
child: restoreBackup
|
||||||
|
? const SizedBox(
|
||||||
|
width: 512,
|
||||||
|
child: RestoreFromFileView(),
|
||||||
|
)
|
||||||
|
: PrimaryButton(
|
||||||
|
desktopMed: true,
|
||||||
|
width: 200,
|
||||||
|
label: "Restore backup",
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
restoreBackup = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +1,111 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
|
||||||
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
import 'package:stackwallet/utilities/assets.dart';
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
import 'package:stackwallet/utilities/constants.dart';
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/progress_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:zxcvbn/zxcvbn.dart';
|
||||||
|
|
||||||
class CreateAutoBackup extends StatefulWidget {
|
class CreateAutoBackup extends ConsumerStatefulWidget {
|
||||||
const CreateAutoBackup({Key? key}) : super(key: key);
|
const CreateAutoBackup({
|
||||||
|
Key? key,
|
||||||
|
this.secureStore = const SecureStorageWrapper(
|
||||||
|
FlutterSecureStorage(),
|
||||||
|
),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final FlutterSecureStorageInterface secureStore;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _CreateAutoBackup();
|
ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreateAutoBackup extends State<CreateAutoBackup> {
|
class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
|
||||||
late final TextEditingController fileLocationController;
|
late final TextEditingController fileLocationController;
|
||||||
late final TextEditingController passphraseController;
|
late final TextEditingController passphraseController;
|
||||||
late final TextEditingController passphraseRepeatController;
|
late final TextEditingController passphraseRepeatController;
|
||||||
|
|
||||||
late final FocusNode chooseFileLocation;
|
late final FlutterSecureStorageInterface secureStore;
|
||||||
|
|
||||||
|
late final StackFileSystem stackFileSystem;
|
||||||
late final FocusNode passphraseFocusNode;
|
late final FocusNode passphraseFocusNode;
|
||||||
late final FocusNode passphraseRepeatFocusNode;
|
late final FocusNode passphraseRepeatFocusNode;
|
||||||
|
final zxcvbn = Zxcvbn();
|
||||||
|
|
||||||
bool shouldShowPasswordHint = true;
|
bool shouldShowPasswordHint = true;
|
||||||
bool hidePassword = true;
|
bool hidePassword = true;
|
||||||
|
|
||||||
|
String passwordFeedback =
|
||||||
|
"Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters.";
|
||||||
|
double passwordStrength = 0.0;
|
||||||
|
|
||||||
|
bool get shouldEnableCreate {
|
||||||
|
return fileLocationController.text.isNotEmpty &&
|
||||||
|
passphraseController.text.isNotEmpty &&
|
||||||
|
passphraseRepeatController.text.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
bool get fieldsMatch =>
|
bool get fieldsMatch =>
|
||||||
passphraseController.text == passphraseRepeatController.text;
|
passphraseController.text == passphraseRepeatController.text;
|
||||||
|
|
||||||
String _currentDropDownValue = "Every 10 minutes";
|
BackupFrequencyType _currentDropDownValue =
|
||||||
|
BackupFrequencyType.everyTenMinutes;
|
||||||
|
|
||||||
final List<String> _dropDownItems = [
|
final List<BackupFrequencyType> _dropDownItems = [
|
||||||
"Every 10 minutes",
|
BackupFrequencyType.everyTenMinutes,
|
||||||
"Every 20 minutes",
|
BackupFrequencyType.everyAppStart,
|
||||||
"Every 30 minutes",
|
BackupFrequencyType.afterClosingAWallet,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
secureStore = widget.secureStore;
|
||||||
|
stackFileSystem = StackFileSystem();
|
||||||
|
|
||||||
fileLocationController = TextEditingController();
|
fileLocationController = TextEditingController();
|
||||||
passphraseController = TextEditingController();
|
passphraseController = TextEditingController();
|
||||||
passphraseRepeatController = TextEditingController();
|
passphraseRepeatController = TextEditingController();
|
||||||
|
|
||||||
chooseFileLocation = FocusNode();
|
|
||||||
passphraseFocusNode = FocusNode();
|
passphraseFocusNode = FocusNode();
|
||||||
passphraseRepeatFocusNode = FocusNode();
|
passphraseRepeatFocusNode = FocusNode();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
|
final dir = await stackFileSystem.prepareStorage();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
fileLocationController.text = dir.path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +115,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
passphraseController.dispose();
|
passphraseController.dispose();
|
||||||
passphraseRepeatController.dispose();
|
passphraseRepeatController.dispose();
|
||||||
|
|
||||||
chooseFileLocation.dispose();
|
|
||||||
passphraseFocusNode.dispose();
|
passphraseFocusNode.dispose();
|
||||||
passphraseRepeatFocusNode.dispose();
|
passphraseRepeatFocusNode.dispose();
|
||||||
|
|
||||||
|
@ -70,10 +125,13 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint("BUILD: $runtimeType ");
|
debugPrint("BUILD: $runtimeType ");
|
||||||
|
|
||||||
String? selectedItem = "Every 10 minutes";
|
bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider
|
||||||
|
.select((value) => value.isAutoBackupEnabled));
|
||||||
|
|
||||||
|
String? selectedItem = "Every 10 minutes";
|
||||||
|
final isDesktop = Util.isDesktop;
|
||||||
return DesktopDialog(
|
return DesktopDialog(
|
||||||
maxHeight: 650,
|
maxHeight: 680,
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -127,89 +185,103 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
left: 32,
|
child: Column(
|
||||||
right: 32,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
),
|
children: [
|
||||||
child: ClipRRect(
|
if (!Platform.isAndroid)
|
||||||
borderRadius: BorderRadius.circular(
|
Consumer(builder: (context, ref, __) {
|
||||||
Constants.size.circularBorderRadius,
|
return Container(
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
key: const Key("backupChooseFileLocation"),
|
|
||||||
focusNode: chooseFileLocation,
|
|
||||||
controller: fileLocationController,
|
|
||||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
decoration: standardInputDecoration(
|
|
||||||
"Save to...",
|
|
||||||
chooseFileLocation,
|
|
||||||
context,
|
|
||||||
).copyWith(
|
|
||||||
labelStyle:
|
|
||||||
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
suffixIcon: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(1000),
|
child: TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
onTap: Platform.isAndroid
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
try {
|
||||||
|
await stackFileSystem.prepareStorage();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await stackFileSystem.pickDir(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
fileLocationController.text =
|
||||||
|
stackFileSystem.dirPath ?? "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance
|
||||||
|
.log("$e\n$s", level: LogLevel.Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: fileLocationController,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Save to...",
|
||||||
|
hintStyle: STextStyles.fieldLabel(context),
|
||||||
|
suffixIcon: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
),
|
),
|
||||||
height: 32,
|
SvgPicture.asset(
|
||||||
width: 32,
|
|
||||||
child: Center(
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
Assets.svg.folder,
|
Assets.svg.folder,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark3,
|
.textDark3,
|
||||||
width: 20,
|
width: 16,
|
||||||
height: 17.5,
|
height: 16,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
key: const Key(
|
||||||
|
"createBackupSaveToFileLocationTextFieldKey"),
|
||||||
|
readOnly: true,
|
||||||
|
toolbarOptions: const ToolbarOptions(
|
||||||
|
copy: true,
|
||||||
|
cut: false,
|
||||||
|
paste: false,
|
||||||
|
selectAll: false,
|
||||||
),
|
),
|
||||||
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
if (!Platform.isAndroid)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
),
|
),
|
||||||
Container(
|
if (isDesktop)
|
||||||
alignment: Alignment.centerLeft,
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 32),
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Create a passphrase",
|
"Create a passphrase",
|
||||||
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
style: STextStyles.desktopTextExtraSmall(context)
|
||||||
color: Theme.of(context).extension<StackColors>()!.textDark3,
|
.copyWith(
|
||||||
),
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
ClipRRect(
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 32,
|
|
||||||
right: 32,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius,
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
key: const Key("createBackupPassphrase"),
|
key: const Key("createBackupPasswordFieldKey1"),
|
||||||
focusNode: passphraseFocusNode,
|
focusNode: passphraseFocusNode,
|
||||||
controller: passphraseController,
|
controller: passphraseController,
|
||||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
style: STextStyles.field(context),
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
obscureText: hidePassword,
|
obscureText: hidePassword,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
|
@ -219,62 +291,133 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
labelStyle:
|
labelStyle:
|
||||||
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
isDesktop ? STextStyles.fieldLabel(context) : null,
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
key: const Key(
|
key: const Key(
|
||||||
"createDesktopAutoBackupShowPassphraseButton1"),
|
"createBackupPasswordFieldShowPasswordButtonKey"),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
hidePassword = !hidePassword;
|
hidePassword = !hidePassword;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(1000),
|
|
||||||
),
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: Center(
|
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark3,
|
.textDark3,
|
||||||
width: 20,
|
width: 16,
|
||||||
height: 17.5,
|
height: 16,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
width: 12,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
passwordFeedback = "";
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = zxcvbn.evaluate(newValue);
|
||||||
|
String suggestionsAndTips = "";
|
||||||
|
for (var sug in result.feedback.suggestions!.toSet()) {
|
||||||
|
suggestionsAndTips += "$sug\n";
|
||||||
|
}
|
||||||
|
suggestionsAndTips += result.feedback.warning!;
|
||||||
|
String feedback =
|
||||||
|
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
|
||||||
|
suggestionsAndTips;
|
||||||
|
|
||||||
|
passwordStrength = result.score! / 4;
|
||||||
|
|
||||||
|
// hack fix to format back string returned from zxcvbn
|
||||||
|
if (feedback.contains("phrasesNo need")) {
|
||||||
|
feedback = feedback.replaceFirst(
|
||||||
|
"phrasesNo need", "phrases\nNo need");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedback.endsWith("\n")) {
|
||||||
|
feedback = feedback.substring(0, feedback.length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
passwordFeedback = feedback;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (passphraseFocusNode.hasFocus ||
|
||||||
|
passphraseRepeatFocusNode.hasFocus ||
|
||||||
|
passphraseController.text.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: passwordFeedback.isNotEmpty ? 4 : 0,
|
||||||
|
),
|
||||||
|
child: passwordFeedback.isNotEmpty
|
||||||
|
? Text(
|
||||||
|
passwordFeedback,
|
||||||
|
style: STextStyles.infoSmall(context),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (passphraseFocusNode.hasFocus ||
|
||||||
|
passphraseRepeatFocusNode.hasFocus ||
|
||||||
|
passphraseController.text.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 32,
|
left: 12,
|
||||||
right: 32,
|
right: 12,
|
||||||
|
top: 10,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ProgressBar(
|
||||||
|
key: const Key("createStackBackUpProgressBar"),
|
||||||
|
width: 512,
|
||||||
|
height: 5,
|
||||||
|
fillColor: passwordStrength < 0.51
|
||||||
|
? Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorRed
|
||||||
|
: passwordStrength < 1
|
||||||
|
? Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorYellow
|
||||||
|
: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorGreen,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.buttonBackSecondary,
|
||||||
|
percent:
|
||||||
|
passwordStrength < 0.25 ? 0.03 : passwordStrength,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
Constants.size.circularBorderRadius,
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
key: const Key("createBackupPassphrase"),
|
key: const Key("createBackupPasswordFieldKey2"),
|
||||||
focusNode: passphraseRepeatFocusNode,
|
focusNode: passphraseRepeatFocusNode,
|
||||||
controller: passphraseRepeatController,
|
controller: passphraseRepeatController,
|
||||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
style: STextStyles.field(context),
|
||||||
height: 2,
|
|
||||||
),
|
|
||||||
obscureText: hidePassword,
|
obscureText: hidePassword,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
|
@ -283,42 +426,46 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
passphraseRepeatFocusNode,
|
passphraseRepeatFocusNode,
|
||||||
context,
|
context,
|
||||||
).copyWith(
|
).copyWith(
|
||||||
labelStyle:
|
labelStyle: STextStyles.fieldLabel(context),
|
||||||
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
suffixIcon: UnconstrainedBox(
|
suffixIcon: UnconstrainedBox(
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
key: const Key(
|
key: const Key(
|
||||||
"createDesktopAutoBackupShowPassphraseButton2"),
|
"createBackupPasswordFieldShowPasswordButtonKey"),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
hidePassword = !hidePassword;
|
hidePassword = !hidePassword;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(1000),
|
|
||||||
),
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: Center(
|
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
.textDark3,
|
.textDark3,
|
||||||
width: 20,
|
width: 16,
|
||||||
height: 17.5,
|
height: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setState(() {});
|
||||||
|
// TODO: ? check if passwords match?
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -343,39 +490,85 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
left: 32,
|
left: 32,
|
||||||
right: 32,
|
right: 32,
|
||||||
),
|
),
|
||||||
child: DropdownButtonFormField(
|
child: isDesktop
|
||||||
|
? DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2(
|
||||||
|
offset: Offset(0, -10),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
elevation: 0,
|
dropdownElevation: 0,
|
||||||
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
|
||||||
color: Theme.of(context).extension<StackColors>()!.textDark,
|
|
||||||
),
|
|
||||||
icon: SvgPicture.asset(
|
|
||||||
Assets.svg.chevronDown,
|
|
||||||
width: 10,
|
|
||||||
height: 5,
|
|
||||||
color: Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
dropdownColor:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textFieldActiveBG,
|
|
||||||
// focusColor: ,
|
|
||||||
value: _currentDropDownValue,
|
value: _currentDropDownValue,
|
||||||
items: _dropDownItems
|
items: [
|
||||||
.map(
|
..._dropDownItems.map(
|
||||||
(e) => DropdownMenuItem(
|
(e) {
|
||||||
|
String message = "";
|
||||||
|
switch (e) {
|
||||||
|
case BackupFrequencyType.everyTenMinutes:
|
||||||
|
message = "Every 10 minutes";
|
||||||
|
break;
|
||||||
|
case BackupFrequencyType.everyAppStart:
|
||||||
|
message = "Every app startup";
|
||||||
|
break;
|
||||||
|
case BackupFrequencyType.afterClosingAWallet:
|
||||||
|
message =
|
||||||
|
"After closing a cryptocurrency wallet";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DropdownMenuItem(
|
||||||
value: e,
|
value: e,
|
||||||
child: Text(e),
|
child: Text(message),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value is String) {
|
if (value is BackupFrequencyType) {
|
||||||
|
if (ref
|
||||||
|
.read(prefsChangeNotifierProvider)
|
||||||
|
.backupFrequencyType !=
|
||||||
|
value) {
|
||||||
|
ref
|
||||||
|
.read(prefsChangeNotifierProvider)
|
||||||
|
.backupFrequencyType = value;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentDropDownValue = value;
|
_currentDropDownValue = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
Assets.svg.chevronDown,
|
||||||
|
width: 10,
|
||||||
|
height: 5,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3,
|
||||||
|
),
|
||||||
|
buttonPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
buttonDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
dropdownDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -385,7 +578,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
Navigator.of(context).popUntil((_) => count++ >= 2);
|
!isEnabledAutoBackup
|
||||||
|
? Navigator.of(context).popUntil((_) => count++ >= 2)
|
||||||
|
: Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -395,8 +590,164 @@ class _CreateAutoBackup extends State<CreateAutoBackup> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
label: "Enable Auto Backup",
|
label: "Enable Auto Backup",
|
||||||
enabled: false,
|
enabled: shouldEnableCreate,
|
||||||
onPressed: () {},
|
onPressed: !shouldEnableCreate
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final String pathToSave =
|
||||||
|
fileLocationController.text;
|
||||||
|
final String passphrase = passphraseController.text;
|
||||||
|
final String repeatPassphrase =
|
||||||
|
passphraseRepeatController.text;
|
||||||
|
|
||||||
|
if (pathToSave.isEmpty) {
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Directory not chosen",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(await Directory(pathToSave).exists())) {
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Directory does not exist",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passphrase.isEmpty) {
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "A passphrase is required",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passphrase != repeatPassphrase) {
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "Passphrase does not match",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const StackDialog(
|
||||||
|
title: "Encrypting initial backup",
|
||||||
|
message: "This shouldn't take long",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// make sure the dialog is able to be displayed for at least some time
|
||||||
|
final fut = Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
String adkString;
|
||||||
|
int adkVersion;
|
||||||
|
try {
|
||||||
|
final adk =
|
||||||
|
await compute(generateAdk, passphrase);
|
||||||
|
adkString = Format.uint8listToString(adk.item2);
|
||||||
|
adkVersion = adk.item1;
|
||||||
|
} on Exception catch (e, s) {
|
||||||
|
String err = getErrorMessageFromSWBException(e);
|
||||||
|
Logging.instance
|
||||||
|
.log("$err\n$s", level: LogLevel.Error);
|
||||||
|
// pop encryption progress dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: err,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance
|
||||||
|
.log("$e\n$s", level: LogLevel.Error);
|
||||||
|
// pop encryption progress dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.warning,
|
||||||
|
message: "$e",
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await secureStore.write(
|
||||||
|
key: "auto_adk_string", value: adkString);
|
||||||
|
await secureStore.write(
|
||||||
|
key: "auto_adk_version_string",
|
||||||
|
value: adkVersion.toString());
|
||||||
|
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final String fileToSave =
|
||||||
|
createAutoBackupFilename(pathToSave, now);
|
||||||
|
|
||||||
|
final backup = await SWB.createStackWalletJSON();
|
||||||
|
|
||||||
|
bool result = await SWB.encryptStackWalletWithADK(
|
||||||
|
fileToSave,
|
||||||
|
adkString,
|
||||||
|
jsonEncode(backup),
|
||||||
|
adkVersion: adkVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
// this future should already be complete unless there was an error encrypting
|
||||||
|
await Future.wait([fut]);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// pop encryption progress dialog
|
||||||
|
int count = 0;
|
||||||
|
Navigator.of(context)
|
||||||
|
.popUntil((_) => count++ >= 2);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
ref
|
||||||
|
.read(prefsChangeNotifierProvider)
|
||||||
|
.autoBackupLocation = pathToSave;
|
||||||
|
ref
|
||||||
|
.read(prefsChangeNotifierProvider)
|
||||||
|
.lastAutoBackup = now;
|
||||||
|
|
||||||
|
ref
|
||||||
|
.read(prefsChangeNotifierProvider)
|
||||||
|
.isAutoBackupEnabled = true;
|
||||||
|
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => Platform.isAndroid
|
||||||
|
? StackOkDialog(
|
||||||
|
title:
|
||||||
|
"Stack Auto Backup enabled and saved to:",
|
||||||
|
message: fileToSave,
|
||||||
|
)
|
||||||
|
: const StackOkDialog(
|
||||||
|
title: "Stack Auto Backup enabled!"),
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
passphraseController.text = "";
|
||||||
|
passphraseRepeatController.text = "";
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
Navigator.of(context)
|
||||||
|
.popUntil((_) => count++ >= 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const StackOkDialog(
|
||||||
|
title: "Failed to enable Auto Backup"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget {
|
||||||
child: SecondaryButton(
|
child: SecondaryButton(
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
int count = 0;
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).popUntil((_) => count++ >= 2);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
|
||||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
|
||||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
|
||||||
|
|
||||||
class RestoreBackupDialog extends StatelessWidget {
|
|
||||||
const RestoreBackupDialog({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DesktopDialog(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Text(
|
|
||||||
"Restoring Stack Wallet",
|
|
||||||
style: STextStyles.desktopH3(context),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const DesktopDialogCloseButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 30,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 32,
|
|
||||||
right: 32,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Settings",
|
|
||||||
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).extension<StackColors>()!.textDark3,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// RoundedWhiteContainer(
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Row(),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
const Spacer(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(32),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: SecondaryButton(
|
|
||||||
label: "Cancel",
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: PrimaryButton(
|
|
||||||
label: "Continue",
|
|
||||||
onPressed: () {
|
|
||||||
// Navigator.of(context).pop();
|
|
||||||
// onConfirm.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,716 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
|
||||||
|
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
const kGithubAPI = "https://api.github.com";
|
||||||
|
const kGithubSearch = "/search/commits";
|
||||||
|
const kGithubHead = "/repos";
|
||||||
|
|
||||||
|
enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded }
|
||||||
|
|
||||||
|
Future<bool> doesCommitExist(
|
||||||
|
String organization,
|
||||||
|
String project,
|
||||||
|
String commit,
|
||||||
|
) async {
|
||||||
|
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||||
|
final Client client = Client();
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
|
||||||
|
|
||||||
|
final commitQuery = await client.get(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = jsonDecode(commitQuery.body.toString());
|
||||||
|
Logging.instance.log("doesCommitExist $project $commit $response",
|
||||||
|
level: LogLevel.Info);
|
||||||
|
bool isThereCommit;
|
||||||
|
try {
|
||||||
|
isThereCommit = response['sha'] == commit;
|
||||||
|
Logging.instance
|
||||||
|
.log("isThereCommit $isThereCommit", level: LogLevel.Info);
|
||||||
|
return isThereCommit;
|
||||||
|
} catch (e, s) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isHeadCommit(
|
||||||
|
String organization,
|
||||||
|
String project,
|
||||||
|
String branch,
|
||||||
|
String commit,
|
||||||
|
) async {
|
||||||
|
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||||
|
final Client client = Client();
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
|
||||||
|
|
||||||
|
final commitQuery = await client.get(
|
||||||
|
uri,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = jsonDecode(commitQuery.body.toString());
|
||||||
|
Logging.instance.log("isHeadCommit $project $commit $branch $response",
|
||||||
|
level: LogLevel.Info);
|
||||||
|
bool isHead;
|
||||||
|
try {
|
||||||
|
isHead = response['sha'] == commit;
|
||||||
|
Logging.instance.log("isHead $isHead", level: LogLevel.Info);
|
||||||
|
return isHead;
|
||||||
|
} catch (e, s) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesktopAboutView extends ConsumerWidget {
|
||||||
|
const DesktopAboutView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/desktopAboutView";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
String firoCommit = FIRO_VERSIONS.getPluginVersion();
|
||||||
|
String epicCashCommit = EPIC_VERSIONS.getPluginVersion();
|
||||||
|
String moneroCommit = MONERO_VERSIONS.getPluginVersion();
|
||||||
|
List<Future> futureFiroList = [
|
||||||
|
doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit),
|
||||||
|
isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit),
|
||||||
|
];
|
||||||
|
Future commitFiroFuture = Future.wait(futureFiroList);
|
||||||
|
List<Future> futureEpicList = [
|
||||||
|
doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit),
|
||||||
|
isHeadCommit(
|
||||||
|
"cypherstack", "flutter_libepiccash", "main", epicCashCommit),
|
||||||
|
];
|
||||||
|
Future commitEpicFuture = Future.wait(futureEpicList);
|
||||||
|
List<Future> futureMoneroList = [
|
||||||
|
doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit),
|
||||||
|
isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit),
|
||||||
|
];
|
||||||
|
Future commitMoneroFuture = Future.wait(futureMoneroList);
|
||||||
|
|
||||||
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
return DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: true,
|
||||||
|
leading: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"About",
|
||||||
|
style: STextStyles.desktopH3(context),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 10, 24, 35),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: RoundedWhiteContainer(
|
||||||
|
width: 929,
|
||||||
|
height: 411,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10, top: 10),
|
||||||
|
child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Stack Wallet",
|
||||||
|
style: STextStyles.desktopH3(context),
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
text: TextSpan(
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"By using Stack Wallet, you agree to the ",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: "Terms of service",
|
||||||
|
style: STextStyles.richLink(context)
|
||||||
|
.copyWith(fontSize: 14),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://stackwallet.com/terms-of-service.html"),
|
||||||
|
mode:
|
||||||
|
LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: " and ",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: "Privacy policy",
|
||||||
|
style: STextStyles.richLink(context)
|
||||||
|
.copyWith(fontSize: 14),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://stackwallet.com/privacy-policy.html"),
|
||||||
|
mode:
|
||||||
|
LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(right: 10, bottom: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FutureBuilder(
|
||||||
|
future: PackageInfo.fromPlatform(),
|
||||||
|
builder: (context,
|
||||||
|
AsyncSnapshot<PackageInfo> snapshot) {
|
||||||
|
String version = "";
|
||||||
|
String signature = "";
|
||||||
|
String build = "";
|
||||||
|
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
version = snapshot.data!.version;
|
||||||
|
build = snapshot.data!.buildNumber;
|
||||||
|
signature = snapshot.data!.buildSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Version",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
version,
|
||||||
|
style:
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 400,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Build number",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
build,
|
||||||
|
style:
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Build signature",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
signature,
|
||||||
|
style:
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 350,
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: commitFiroFuture,
|
||||||
|
builder: (context,
|
||||||
|
AsyncSnapshot<dynamic>
|
||||||
|
snapshot) {
|
||||||
|
bool commitExists = false;
|
||||||
|
bool isHead = false;
|
||||||
|
CommitStatus stateOfCommit =
|
||||||
|
CommitStatus.notLoaded;
|
||||||
|
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState
|
||||||
|
.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
commitExists = snapshot
|
||||||
|
.data![0] as bool;
|
||||||
|
isHead = snapshot.data![1]
|
||||||
|
as bool;
|
||||||
|
if (commitExists &&
|
||||||
|
isHead) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus.isHead;
|
||||||
|
} else if (commitExists) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.isOldCommit;
|
||||||
|
} else {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.notACommit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextStyle indicationStyle =
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context);
|
||||||
|
switch (stateOfCommit) {
|
||||||
|
case CommitStatus.isHead:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorGreen);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.isOldCommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorYellow);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.notACommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorRed);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment
|
||||||
|
.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Firo Build Commit",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
firoCommit,
|
||||||
|
style: indicationStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 35),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
FutureBuilder(
|
||||||
|
future: commitEpicFuture,
|
||||||
|
builder: (context,
|
||||||
|
AsyncSnapshot<dynamic>
|
||||||
|
snapshot) {
|
||||||
|
bool commitExists = false;
|
||||||
|
bool isHead = false;
|
||||||
|
CommitStatus stateOfCommit =
|
||||||
|
CommitStatus.notLoaded;
|
||||||
|
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState
|
||||||
|
.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
commitExists = snapshot
|
||||||
|
.data![0] as bool;
|
||||||
|
isHead = snapshot.data![1]
|
||||||
|
as bool;
|
||||||
|
if (commitExists &&
|
||||||
|
isHead) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus.isHead;
|
||||||
|
} else if (commitExists) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.isOldCommit;
|
||||||
|
} else {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.notACommit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextStyle indicationStyle =
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context);
|
||||||
|
switch (stateOfCommit) {
|
||||||
|
case CommitStatus.isHead:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorGreen);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.isOldCommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorYellow);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.notACommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorRed);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment
|
||||||
|
.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Epic Cash Build Commit",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
epicCashCommit,
|
||||||
|
style: indicationStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(
|
||||||
|
width: 105,
|
||||||
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: commitMoneroFuture,
|
||||||
|
builder: (context,
|
||||||
|
AsyncSnapshot<dynamic>
|
||||||
|
snapshot) {
|
||||||
|
bool commitExists = false;
|
||||||
|
bool isHead = false;
|
||||||
|
CommitStatus stateOfCommit =
|
||||||
|
CommitStatus.notLoaded;
|
||||||
|
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState
|
||||||
|
.done &&
|
||||||
|
snapshot.hasData) {
|
||||||
|
commitExists = snapshot
|
||||||
|
.data![0] as bool;
|
||||||
|
isHead = snapshot.data![1]
|
||||||
|
as bool;
|
||||||
|
if (commitExists &&
|
||||||
|
isHead) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus.isHead;
|
||||||
|
} else if (commitExists) {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.isOldCommit;
|
||||||
|
} else {
|
||||||
|
stateOfCommit =
|
||||||
|
CommitStatus
|
||||||
|
.notACommit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextStyle indicationStyle =
|
||||||
|
STextStyles.itemSubtitle(
|
||||||
|
context);
|
||||||
|
switch (stateOfCommit) {
|
||||||
|
case CommitStatus.isHead:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorGreen);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.isOldCommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorYellow);
|
||||||
|
break;
|
||||||
|
case CommitStatus
|
||||||
|
.notACommit:
|
||||||
|
indicationStyle = STextStyles
|
||||||
|
.itemSubtitle(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorRed);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment
|
||||||
|
.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Monero Build Commit",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
moneroCommit,
|
||||||
|
style: indicationStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 35),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Website",
|
||||||
|
style: STextStyles
|
||||||
|
.desktopTextExtraExtraSmall(
|
||||||
|
context)
|
||||||
|
.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.textDark),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
BlueTextButton(
|
||||||
|
text:
|
||||||
|
"https://stackwallet.com",
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
"https://stackwallet.com"),
|
||||||
|
mode: LaunchMode
|
||||||
|
.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
|
||||||
|
class DesktopSupportView extends ConsumerStatefulWidget {
|
||||||
|
const DesktopSupportView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/desktopSupportView";
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DesktopSupportView> createState() => _DesktopSupportView();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopSupportView extends ConsumerState<DesktopSupportView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
return DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: true,
|
||||||
|
leading: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Support",
|
||||||
|
style: STextStyles.desktopH3(context),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 10, 0, 0),
|
||||||
|
child: Row(
|
||||||
|
children: const [
|
||||||
|
SizedBox(
|
||||||
|
width: 576,
|
||||||
|
child: SupportView(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/utilities/desktop_password_service.dart';
|
||||||
|
|
||||||
|
final storageCryptoHandlerProvider = Provider<DPS>((ref) => DPS());
|
|
@ -85,6 +85,8 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear
|
||||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
|
import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart';
|
||||||
|
@ -99,6 +101,8 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_sett
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart';
|
import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart';
|
||||||
import 'package:stackwallet/services/coins/manager.dart';
|
import 'package:stackwallet/services/coins/manager.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||||
|
@ -996,6 +1000,12 @@ class RouteGenerator {
|
||||||
builder: (_) => const CreatePasswordView(),
|
builder: (_) => const CreatePasswordView(),
|
||||||
settings: RouteSettings(name: settings.name));
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
|
case ForgotPasswordDesktopView.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const ForgotPasswordDesktopView(),
|
||||||
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
case DesktopHomeView.routeName:
|
case DesktopHomeView.routeName:
|
||||||
return getRoute(
|
return getRoute(
|
||||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
@ -1084,6 +1094,24 @@ class RouteGenerator {
|
||||||
builder: (_) => const AdvancedSettings(),
|
builder: (_) => const AdvancedSettings(),
|
||||||
settings: RouteSettings(name: settings.name));
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
|
case DesktopSupportView.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const DesktopSupportView(),
|
||||||
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
|
case DesktopAboutView.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const DesktopAboutView(),
|
||||||
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
|
case DesktopAddressBook.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const DesktopAddressBook(),
|
||||||
|
settings: RouteSettings(name: settings.name));
|
||||||
|
|
||||||
case WalletKeysDesktopPopup.routeName:
|
case WalletKeysDesktopPopup.routeName:
|
||||||
if (args is List<String>) {
|
if (args is List<String>) {
|
||||||
return FadePageRoute(
|
return FadePageRoute(
|
||||||
|
|
|
@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier {
|
||||||
List<Contact> get contacts {
|
List<Contact> get contacts {
|
||||||
final keys = List<String>.from(
|
final keys = List<String>.from(
|
||||||
DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook));
|
DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook));
|
||||||
return keys
|
final _contacts = keys
|
||||||
.map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance
|
.map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance
|
||||||
.get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map)))
|
.get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map)))
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
_contacts
|
||||||
|
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
return _contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>>? _addressBookEntries;
|
Future<List<Contact>>? _addressBookEntries;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -174,9 +175,10 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1282,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final priceData =
|
||||||
|
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||||
|
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network);
|
return Address.validateAddress(address, _network);
|
||||||
|
@ -2660,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,12 @@ import 'dart:typed_data';
|
||||||
import 'package:bech32/bech32.dart';
|
import 'package:bech32/bech32.dart';
|
||||||
import 'package:bip32/bip32.dart' as bip32;
|
import 'package:bip32/bip32.dart' as bip32;
|
||||||
import 'package:bip39/bip39.dart' as bip39;
|
import 'package:bip39/bip39.dart' as bip39;
|
||||||
import 'package:bitbox/bitbox.dart' as Bitbox;
|
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||||
import 'package:bitcoindart/bitcoindart.dart';
|
import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -207,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
_getCurrentAddressForChain(0, DerivePathType.bip44);
|
_getCurrentAddressForChain(0, DerivePathType.bip44);
|
||||||
Future<String>? _currentReceivingAddressP2PKH;
|
Future<String>? _currentReceivingAddressP2PKH;
|
||||||
|
|
||||||
Future<String> get currentReceivingAddressP2SH =>
|
// Future<String> get currentReceivingAddressP2SH =>
|
||||||
_currentReceivingAddressP2SH ??=
|
// _currentReceivingAddressP2SH ??=
|
||||||
_getCurrentAddressForChain(0, DerivePathType.bip49);
|
// _getCurrentAddressForChain(0, DerivePathType.bip49);
|
||||||
Future<String>? _currentReceivingAddressP2SH;
|
Future<String>? _currentReceivingAddressP2SH;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -258,7 +259,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateStoredChainHeight({required int newHeight}) async {
|
Future<void> updateStoredChainHeight({required int newHeight}) async {
|
||||||
DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: "storedChainHeight", value: newHeight);
|
boxName: walletId, key: "storedChainHeight", value: newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,8 +267,13 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
Uint8List? decodeBase58;
|
Uint8List? decodeBase58;
|
||||||
Segwit? decodeBech32;
|
Segwit? decodeBech32;
|
||||||
try {
|
try {
|
||||||
if (Bitbox.Address.detectFormat(address) == 0) {
|
if (bitbox.Address.detectFormat(address) ==
|
||||||
address = Bitbox.Address.toLegacyAddress(address);
|
bitbox.Address.formatCashAddr) {
|
||||||
|
if (validateCashAddr(address)) {
|
||||||
|
address = bitbox.Address.toLegacyAddress(address);
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('$address is not currently supported');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {}
|
} catch (e, s) {}
|
||||||
try {
|
try {
|
||||||
|
@ -292,13 +298,16 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Bech32 decode fail
|
// Bech32 decode fail
|
||||||
}
|
}
|
||||||
if (_network.bech32 != decodeBech32!.hrp) {
|
|
||||||
|
if (decodeBech32 != null) {
|
||||||
|
if (_network.bech32 != decodeBech32.hrp) {
|
||||||
throw ArgumentError('Invalid prefix or Network mismatch');
|
throw ArgumentError('Invalid prefix or Network mismatch');
|
||||||
}
|
}
|
||||||
if (decodeBech32.version != 0) {
|
if (decodeBech32.version != 0) {
|
||||||
throw ArgumentError('Invalid address version');
|
throw ArgumentError('Invalid address version');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw ArgumentError('$address has no matching Script');
|
throw ArgumentError('$address has no matching Script');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -609,7 +618,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
|
|
||||||
// get address tx counts
|
// get address tx counts
|
||||||
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
|
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
|
||||||
|
if (kDebugMode) {
|
||||||
print("Counts $counts");
|
print("Counts $counts");
|
||||||
|
}
|
||||||
// check and add appropriate addresses
|
// check and add appropriate addresses
|
||||||
for (int k = 0; k < txCountBatchSize; k++) {
|
for (int k = 0; k < txCountBatchSize; k++) {
|
||||||
int count = counts["${_id}_$k"]!;
|
int count = counts["${_id}_$k"]!;
|
||||||
|
@ -745,6 +756,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
// notify on new incoming transaction
|
// notify on new incoming transaction
|
||||||
for (final tx in unconfirmedTxnsToNotifyPending) {
|
for (final tx in unconfirmedTxnsToNotifyPending) {
|
||||||
if (tx.txType == "Received") {
|
if (tx.txType == "Received") {
|
||||||
|
unawaited(
|
||||||
NotificationApi.showNotification(
|
NotificationApi.showNotification(
|
||||||
title: "Incoming transaction",
|
title: "Incoming transaction",
|
||||||
body: walletName,
|
body: walletName,
|
||||||
|
@ -756,9 +768,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
confirmations: tx.confirmations,
|
confirmations: tx.confirmations,
|
||||||
requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await txTracker.addNotifiedPending(tx.txid);
|
await txTracker.addNotifiedPending(tx.txid);
|
||||||
} else if (tx.txType == "Sent") {
|
} else if (tx.txType == "Sent") {
|
||||||
|
unawaited(
|
||||||
NotificationApi.showNotification(
|
NotificationApi.showNotification(
|
||||||
title: "Sending transaction",
|
title: "Sending transaction",
|
||||||
body: walletName,
|
body: walletName,
|
||||||
|
@ -770,6 +784,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
confirmations: tx.confirmations,
|
confirmations: tx.confirmations,
|
||||||
requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await txTracker.addNotifiedPending(tx.txid);
|
await txTracker.addNotifiedPending(tx.txid);
|
||||||
}
|
}
|
||||||
|
@ -778,6 +793,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
// notify on confirmed
|
// notify on confirmed
|
||||||
for (final tx in unconfirmedTxnsToNotifyConfirmed) {
|
for (final tx in unconfirmedTxnsToNotifyConfirmed) {
|
||||||
if (tx.txType == "Received") {
|
if (tx.txType == "Received") {
|
||||||
|
unawaited(
|
||||||
NotificationApi.showNotification(
|
NotificationApi.showNotification(
|
||||||
title: "Incoming transaction confirmed",
|
title: "Incoming transaction confirmed",
|
||||||
body: walletName,
|
body: walletName,
|
||||||
|
@ -786,10 +802,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
shouldWatchForUpdates: false,
|
shouldWatchForUpdates: false,
|
||||||
coinName: coin.name,
|
coinName: coin.name,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await txTracker.addNotifiedConfirmed(tx.txid);
|
await txTracker.addNotifiedConfirmed(tx.txid);
|
||||||
} else if (tx.txType == "Sent") {
|
} else if (tx.txType == "Sent") {
|
||||||
|
unawaited(
|
||||||
NotificationApi.showNotification(
|
NotificationApi.showNotification(
|
||||||
title: "Outgoing transaction confirmed",
|
title: "Outgoing transaction confirmed",
|
||||||
body: walletName,
|
body: walletName,
|
||||||
|
@ -798,6 +816,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
shouldWatchForUpdates: false,
|
shouldWatchForUpdates: false,
|
||||||
coinName: coin.name,
|
coinName: coin.name,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await txTracker.addNotifiedConfirmed(tx.txid);
|
await txTracker.addNotifiedConfirmed(tx.txid);
|
||||||
}
|
}
|
||||||
|
@ -862,7 +881,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
if (currentHeight != storedHeight) {
|
if (currentHeight != storedHeight) {
|
||||||
if (currentHeight != -1) {
|
if (currentHeight != -1) {
|
||||||
// -1 failed to fetch current height
|
// -1 failed to fetch current height
|
||||||
updateStoredChainHeight(newHeight: currentHeight);
|
await updateStoredChainHeight(newHeight: currentHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
|
||||||
|
@ -1143,14 +1162,82 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final priceData =
|
||||||
|
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||||
|
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateCashAddr(String cashAddr) {
|
||||||
|
String addr = cashAddr;
|
||||||
|
if (cashAddr.contains(":")) {
|
||||||
|
addr = cashAddr.split(":").last;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr.startsWith("q");
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
try {
|
try {
|
||||||
// 0 for bitcoincash: address scheme, 1 for legacy address
|
// 0 for bitcoincash: address scheme, 1 for legacy address
|
||||||
final format = Bitbox.Address.detectFormat(address);
|
final format = bitbox.Address.detectFormat(address);
|
||||||
|
if (kDebugMode) {
|
||||||
print("format $format");
|
print("format $format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_coin == Coin.bitcoincashTestnet) {
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
}
|
||||||
|
|
||||||
|
if (format == bitbox.Address.formatCashAddr) {
|
||||||
|
return validateCashAddr(address);
|
||||||
|
} else {
|
||||||
|
return address.startsWith("1");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1226,7 +1313,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
refresh();
|
unawaited(refresh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1522,12 +1609,15 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
print("Array key is ${jsonEncode(arrayKey)}");
|
print("Array key is ${jsonEncode(arrayKey)}");
|
||||||
|
}
|
||||||
final internalChainArray =
|
final internalChainArray =
|
||||||
DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
|
DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
|
||||||
if (derivePathType == DerivePathType.bip44) {
|
if (derivePathType == DerivePathType.bip44) {
|
||||||
if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) {
|
if (bitbox.Address.detectFormat(internalChainArray.last as String) ==
|
||||||
return Bitbox.Address.toCashAddress(internalChainArray.last as String);
|
bitbox.Address.formatLegacy) {
|
||||||
|
return bitbox.Address.toCashAddress(internalChainArray.last as String);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return internalChainArray.last as String;
|
return internalChainArray.last as String;
|
||||||
|
@ -1642,7 +1732,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
batches[batchNumber] = {};
|
batches[batchNumber] = {};
|
||||||
}
|
}
|
||||||
final scripthash = _convertToScriptHash(allAddresses[i], _network);
|
final scripthash = _convertToScriptHash(allAddresses[i], _network);
|
||||||
|
if (kDebugMode) {
|
||||||
print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash");
|
print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash");
|
||||||
|
}
|
||||||
batches[batchNumber]!.addAll({
|
batches[batchNumber]!.addAll({
|
||||||
scripthash: [scripthash]
|
scripthash: [scripthash]
|
||||||
});
|
});
|
||||||
|
@ -1818,20 +1910,28 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final Map<String, List<dynamic>> args = {};
|
final Map<String, List<dynamic>> args = {};
|
||||||
|
if (kDebugMode) {
|
||||||
print("Address $addresses");
|
print("Address $addresses");
|
||||||
|
}
|
||||||
for (final entry in addresses.entries) {
|
for (final entry in addresses.entries) {
|
||||||
args[entry.key] = [_convertToScriptHash(entry.value, _network)];
|
args[entry.key] = [_convertToScriptHash(entry.value, _network)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
print("Args ${jsonEncode(args)}");
|
print("Args ${jsonEncode(args)}");
|
||||||
|
}
|
||||||
|
|
||||||
final response = await electrumXClient.getBatchHistory(args: args);
|
final response = await electrumXClient.getBatchHistory(args: args);
|
||||||
|
if (kDebugMode) {
|
||||||
print("Response ${jsonEncode(response)}");
|
print("Response ${jsonEncode(response)}");
|
||||||
|
}
|
||||||
final Map<String, int> result = {};
|
final Map<String, int> result = {};
|
||||||
for (final entry in response.entries) {
|
for (final entry in response.entries) {
|
||||||
result[entry.key] = entry.value.length;
|
result[entry.key] = entry.value.length;
|
||||||
}
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
print("result ${jsonEncode(result)}");
|
print("result ${jsonEncode(result)}");
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
|
@ -1995,8 +2095,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
/// Returns the scripthash or throws an exception on invalid bch address
|
/// Returns the scripthash or throws an exception on invalid bch address
|
||||||
String _convertToScriptHash(String bchAddress, NetworkType network) {
|
String _convertToScriptHash(String bchAddress, NetworkType network) {
|
||||||
try {
|
try {
|
||||||
if (Bitbox.Address.detectFormat(bchAddress) == 0) {
|
if (bitbox.Address.detectFormat(bchAddress) ==
|
||||||
bchAddress = Bitbox.Address.toLegacyAddress(bchAddress);
|
bitbox.Address.formatCashAddr &&
|
||||||
|
validateCashAddr(bchAddress)) {
|
||||||
|
bchAddress = bitbox.Address.toLegacyAddress(bchAddress);
|
||||||
}
|
}
|
||||||
final output = Address.addressToOutputScript(bchAddress, network);
|
final output = Address.addressToOutputScript(bchAddress, network);
|
||||||
final hash = sha256.convert(output.toList(growable: false)).toString();
|
final hash = sha256.convert(output.toList(growable: false)).toString();
|
||||||
|
@ -2073,8 +2175,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
List<String> allAddressesOld = await _fetchAllOwnAddresses();
|
List<String> allAddressesOld = await _fetchAllOwnAddresses();
|
||||||
List<String> allAddresses = [];
|
List<String> allAddresses = [];
|
||||||
for (String address in allAddressesOld) {
|
for (String address in allAddressesOld) {
|
||||||
if (Bitbox.Address.detectFormat(address) == 1) {
|
if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy &&
|
||||||
allAddresses.add(Bitbox.Address.toCashAddress(address));
|
addressType(address: address) == DerivePathType.bip44) {
|
||||||
|
allAddresses.add(bitbox.Address.toCashAddress(address));
|
||||||
} else {
|
} else {
|
||||||
allAddresses.add(address);
|
allAddresses.add(address);
|
||||||
}
|
}
|
||||||
|
@ -2085,8 +2188,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
as List<dynamic>;
|
as List<dynamic>;
|
||||||
List<dynamic> changeAddressesP2PKH = [];
|
List<dynamic> changeAddressesP2PKH = [];
|
||||||
for (var address in changeAddressesP2PKHOld) {
|
for (var address in changeAddressesP2PKHOld) {
|
||||||
if (Bitbox.Address.detectFormat(address as String) == 1) {
|
if (bitbox.Address.detectFormat(address as String) ==
|
||||||
changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address));
|
bitbox.Address.formatLegacy) {
|
||||||
|
changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address));
|
||||||
} else {
|
} else {
|
||||||
changeAddressesP2PKH.add(address);
|
changeAddressesP2PKH.add(address);
|
||||||
}
|
}
|
||||||
|
@ -2108,21 +2212,27 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
unconfirmedCachedTransactions
|
unconfirmedCachedTransactions
|
||||||
.removeWhere((key, value) => value.confirmedStatus);
|
.removeWhere((key, value) => value.confirmedStatus);
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
print("CACHED_TRANSACTIONS_IS $cachedTransactions");
|
print("CACHED_TRANSACTIONS_IS $cachedTransactions");
|
||||||
|
}
|
||||||
if (cachedTransactions != null) {
|
if (cachedTransactions != null) {
|
||||||
for (final tx in allTxHashes.toList(growable: false)) {
|
for (final tx in allTxHashes.toList(growable: false)) {
|
||||||
final txHeight = tx["height"] as int;
|
final txHeight = tx["height"] as int;
|
||||||
if (txHeight > 0 &&
|
if (txHeight > 0 &&
|
||||||
txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
|
txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
|
||||||
if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
|
if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
|
||||||
print(cachedTransactions.findTransaction(tx["tx_hash"] as String));
|
if (kDebugMode) {
|
||||||
|
print(
|
||||||
|
cachedTransactions.findTransaction(tx["tx_hash"] as String));
|
||||||
print(unconfirmedCachedTransactions[tx["tx_hash"] as String]);
|
print(unconfirmedCachedTransactions[tx["tx_hash"] as String]);
|
||||||
|
}
|
||||||
final cachedTx =
|
final cachedTx =
|
||||||
cachedTransactions.findTransaction(tx["tx_hash"] as String);
|
cachedTransactions.findTransaction(tx["tx_hash"] as String);
|
||||||
if (!(cachedTx != null &&
|
if (!(cachedTx != null &&
|
||||||
addressType(address: cachedTx.address) ==
|
addressType(address: cachedTx.address) ==
|
||||||
DerivePathType.bip44 &&
|
DerivePathType.bip44 &&
|
||||||
Bitbox.Address.detectFormat(cachedTx.address) == 1)) {
|
bitbox.Address.detectFormat(cachedTx.address) ==
|
||||||
|
bitbox.Address.formatLegacy)) {
|
||||||
allTxHashes.remove(tx);
|
allTxHashes.remove(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2401,6 +2511,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2782,8 +2893,14 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
final n = output["n"];
|
final n = output["n"];
|
||||||
if (n != null && n == utxosToUse[i].vout) {
|
if (n != null && n == utxosToUse[i].vout) {
|
||||||
String address = output["scriptPubKey"]["addresses"][0] as String;
|
String address = output["scriptPubKey"]["addresses"][0] as String;
|
||||||
if (Bitbox.Address.detectFormat(address) == 0) {
|
if (bitbox.Address.detectFormat(address) ==
|
||||||
address = Bitbox.Address.toLegacyAddress(address);
|
bitbox.Address.formatCashAddr) {
|
||||||
|
if (validateCashAddr(address)) {
|
||||||
|
address = bitbox.Address.toLegacyAddress(address);
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
"Unsupported address found during fetchBuildTxData(): $address");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!addressTxid.containsKey(address)) {
|
if (!addressTxid.containsKey(address)) {
|
||||||
addressTxid[address] = <String>[];
|
addressTxid[address] = <String>[];
|
||||||
|
@ -2814,9 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
);
|
);
|
||||||
for (int i = 0; i < p2pkhLength; i++) {
|
for (int i = 0; i < p2pkhLength; i++) {
|
||||||
String address = addressesP2PKH[i];
|
String address = addressesP2PKH[i];
|
||||||
if (Bitbox.Address.detectFormat(address) == 0) {
|
|
||||||
address = Bitbox.Address.toLegacyAddress(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
// receives
|
// receives
|
||||||
final receiveDerivation = receiveDerivations[address];
|
final receiveDerivation = receiveDerivations[address];
|
||||||
|
@ -2950,36 +3064,36 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
required List<String> recipients,
|
required List<String> recipients,
|
||||||
required List<int> satoshiAmounts,
|
required List<int> satoshiAmounts,
|
||||||
}) async {
|
}) async {
|
||||||
final builder = Bitbox.Bitbox.transactionBuilder();
|
final builder = bitbox.Bitbox.transactionBuilder();
|
||||||
|
|
||||||
// retrieve address' utxos from the rest api
|
// retrieve address' utxos from the rest api
|
||||||
List<Bitbox.Utxo> _utxos =
|
List<bitbox.Utxo> _utxos =
|
||||||
[]; // await Bitbox.Address.utxo(address) as List<Bitbox.Utxo>;
|
[]; // await Bitbox.Address.utxo(address) as List<Bitbox.Utxo>;
|
||||||
utxosToUse.forEach((element) {
|
for (var element in utxosToUse) {
|
||||||
_utxos.add(Bitbox.Utxo(
|
_utxos.add(bitbox.Utxo(
|
||||||
element.txid,
|
element.txid,
|
||||||
element.vout,
|
element.vout,
|
||||||
Bitbox.BitcoinCash.fromSatoshi(element.value),
|
bitbox.BitcoinCash.fromSatoshi(element.value),
|
||||||
element.value,
|
element.value,
|
||||||
0,
|
0,
|
||||||
MINIMUM_CONFIRMATIONS + 1));
|
MINIMUM_CONFIRMATIONS + 1));
|
||||||
});
|
}
|
||||||
Logger.print("bch utxos: ${_utxos}");
|
Logger.print("bch utxos: $_utxos");
|
||||||
|
|
||||||
// placeholder for input signatures
|
// placeholder for input signatures
|
||||||
final signatures = <Map>[];
|
final List<Map<dynamic, dynamic>> signatures = [];
|
||||||
|
|
||||||
// placeholder for total input balance
|
// placeholder for total input balance
|
||||||
int totalBalance = 0;
|
// int totalBalance = 0;
|
||||||
|
|
||||||
// iterate through the list of address _utxos and use them as inputs for the
|
// iterate through the list of address _utxos and use them as inputs for the
|
||||||
// withdrawal transaction
|
// withdrawal transaction
|
||||||
_utxos.forEach((Bitbox.Utxo utxo) {
|
for (var utxo in _utxos) {
|
||||||
// add the utxo as an input for the transaction
|
// add the utxo as an input for the transaction
|
||||||
builder.addInput(utxo.txid, utxo.vout);
|
builder.addInput(utxo.txid, utxo.vout);
|
||||||
final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair;
|
final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair;
|
||||||
|
|
||||||
final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF());
|
final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF());
|
||||||
|
|
||||||
// add a signature to the list to be used later
|
// add a signature to the list to be used later
|
||||||
signatures.add({
|
signatures.add({
|
||||||
|
@ -2988,15 +3102,15 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
"original_amount": utxo.satoshis
|
"original_amount": utxo.satoshis
|
||||||
});
|
});
|
||||||
|
|
||||||
totalBalance += utxo.satoshis;
|
// totalBalance += utxo.satoshis;
|
||||||
});
|
}
|
||||||
|
|
||||||
// calculate the fee based on number of inputs and one expected output
|
// calculate the fee based on number of inputs and one expected output
|
||||||
final fee =
|
// final fee =
|
||||||
Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length);
|
// bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length);
|
||||||
|
|
||||||
// calculate how much balance will be left over to spend after the fee
|
// calculate how much balance will be left over to spend after the fee
|
||||||
final sendAmount = totalBalance - fee;
|
// final sendAmount = totalBalance - fee;
|
||||||
|
|
||||||
// add the output based on the address provided in the testing data
|
// add the output based on the address provided in the testing data
|
||||||
for (int i = 0; i < recipients.length; i++) {
|
for (int i = 0; i < recipients.length; i++) {
|
||||||
|
@ -3006,12 +3120,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign all inputs
|
// sign all inputs
|
||||||
signatures.forEach((signature) {
|
for (var signature in signatures) {
|
||||||
builder.sign(
|
builder.sign(
|
||||||
signature["vin"] as int,
|
signature["vin"] as int,
|
||||||
signature["key_pair"] as Bitbox.ECPair,
|
signature["key_pair"] as bitbox.ECPair,
|
||||||
signature["original_amount"] as int);
|
signature["original_amount"] as int);
|
||||||
});
|
}
|
||||||
|
|
||||||
// build the transaction
|
// build the transaction
|
||||||
final tx = builder.build();
|
final tx = builder.build();
|
||||||
|
@ -3038,7 +3152,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
);
|
);
|
||||||
|
|
||||||
// clear cache
|
// clear cache
|
||||||
_cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
|
await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin);
|
||||||
|
|
||||||
// back up data
|
// back up data
|
||||||
await _rescanBackup();
|
await _rescanBackup();
|
||||||
|
@ -3326,9 +3440,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
|
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
|
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
|
||||||
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
|
|
||||||
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
|
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
|
||||||
|
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
|
||||||
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/prefs.dart';
|
import 'package:stackwallet/utilities/prefs.dart';
|
||||||
|
@ -277,4 +277,7 @@ abstract class CoinServiceAPI {
|
||||||
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
|
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
|
||||||
|
|
||||||
Future<bool> generateNewAddress();
|
Future<bool> generateNewAddress();
|
||||||
|
|
||||||
|
// used for electrumx coins
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final priceData =
|
||||||
|
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||||
|
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network);
|
return Address.validateAddress(address, _network);
|
||||||
|
@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2983,9 +3033,10 @@ class DogecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -558,9 +558,10 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
final txLogEntryFirst = txLogEntry[0];
|
final txLogEntryFirst = txLogEntry[0];
|
||||||
Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst");
|
Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst");
|
||||||
final wallet = await Hive.openBox<dynamic>(_walletId);
|
final wallet = await Hive.openBox<dynamic>(_walletId);
|
||||||
final slateToAddresses = (await wallet.get("slate_to_address")) as Map?;
|
final slateToAddresses =
|
||||||
slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss'];
|
(await wallet.get("slate_to_address")) as Map? ?? {};
|
||||||
|
final slateId = txLogEntryFirst['tx_slate_id'] as String;
|
||||||
|
slateToAddresses[slateId] = txData['addresss'];
|
||||||
await wallet.put('slate_to_address', slateToAddresses);
|
await wallet.put('slate_to_address', slateToAddresses);
|
||||||
return txLogEntryFirst['tx_slate_id'] as String;
|
final slatesToCommits = await getSlatesToCommits();
|
||||||
|
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
||||||
|
Logging.instance.log("sent commitId: $commitId", level: LogLevel.Info);
|
||||||
|
return commitId!;
|
||||||
|
// return txLogEntryFirst['tx_slate_id'] as String;
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
|
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
|
||||||
|
@ -2154,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
as String? ??
|
as String? ??
|
||||||
"";
|
"";
|
||||||
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("commitId: $commitId $slateId", level: LogLevel.Info);
|
"commitId: $commitId, slateId: $slateId, id: ${tx["id"]}",
|
||||||
|
level: LogLevel.Info);
|
||||||
|
|
||||||
bool isCancelled = tx["tx_type"] == "TxSentCancelled" ||
|
bool isCancelled = tx["tx_type"] == "TxSentCancelled" ||
|
||||||
tx["tx_type"] == "TxReceivedCancelled";
|
tx["tx_type"] == "TxReceivedCancelled";
|
||||||
|
@ -2258,6 +2266,14 @@ class EpicCashWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
// not used in epic
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
// not used in epic
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
||||||
|
|
||||||
|
|
|
@ -821,9 +821,10 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -907,6 +908,52 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
Future<models.TransactionData> get _txnData =>
|
Future<models.TransactionData> get _txnData =>
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
|
|
||||||
|
models.TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final currentPrice = await firoPrice;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
/// Holds wallet lelantus transaction data
|
/// Holds wallet lelantus transaction data
|
||||||
Future<models.TransactionData>? _lelantusTransactionData;
|
Future<models.TransactionData>? _lelantusTransactionData;
|
||||||
Future<models.TransactionData> get lelantusTransactionData =>
|
Future<models.TransactionData> get lelantusTransactionData =>
|
||||||
|
@ -1109,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
final txHash = await _electrumXClient.broadcastTransaction(
|
final txHash = await _electrumXClient.broadcastTransaction(
|
||||||
rawTx: txData["hex"] as String);
|
rawTx: txData["hex"] as String);
|
||||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||||
|
txData["txid"] = txHash;
|
||||||
|
// dirty ui update hack
|
||||||
|
await updateSentCachedTxData(txData as Map<String, dynamic>);
|
||||||
return txHash;
|
return txHash;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||||
|
@ -3464,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -174,9 +175,10 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1284,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final priceData =
|
||||||
|
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||||
|
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network, _network.bech32!);
|
return Address.validateAddress(address, _network, _network.bech32!);
|
||||||
|
@ -2672,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,9 @@ class Manager with ChangeNotifier {
|
||||||
try {
|
try {
|
||||||
final txid = await _currentWallet.confirmSend(txData: txData);
|
final txid = await _currentWallet.confirmSend(txData: txData);
|
||||||
|
|
||||||
|
txData["txid"] = txid;
|
||||||
|
await _currentWallet.updateSentCachedTxData(txData);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return txid;
|
return txid;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -699,7 +699,7 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
name: name,
|
name: name,
|
||||||
type: WalletType.monero,
|
type: WalletType.monero,
|
||||||
isRecovery: false,
|
isRecovery: false,
|
||||||
restoreHeight: credentials.height ?? 0,
|
restoreHeight: bufferedCreateHeight,
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
path: path,
|
path: path,
|
||||||
dirPath: dirPath,
|
dirPath: dirPath,
|
||||||
|
@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
// not used in monero
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
// not used in monero
|
||||||
|
}
|
||||||
|
|
||||||
Future<TransactionData> _fetchTransactionData() async {
|
Future<TransactionData> _fetchTransactionData() async {
|
||||||
final transactions = walletBase?.transactionHistory!.transactions;
|
final transactions = walletBase?.transactionHistory!.transactions;
|
||||||
|
|
||||||
|
@ -1345,10 +1353,8 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement validateAddress
|
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
|
bool valid = walletBase!.validateAddress(address);
|
||||||
RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1376,9 +1382,10 @@ class MoneroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart';
|
||||||
import 'package:bs58check/bs58check.dart' as bs58check;
|
import 'package:bs58check/bs58check.dart' as bs58check;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
|
import 'package:devicelocale/devicelocale.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -170,9 +171,10 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1275,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
// hack to add tx to txData before refresh completes
|
||||||
|
// required based on current app architecture where we don't properly store
|
||||||
|
// transactions locally in a good way
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
final priceData =
|
||||||
|
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||||
|
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||||
|
final locale = await Devicelocale.currentLocale;
|
||||||
|
final String worthNow = Format.localizedStringAsFixed(
|
||||||
|
value:
|
||||||
|
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
|
||||||
|
Decimal.fromInt(Constants.satsPerCoin))
|
||||||
|
.toDecimal(scaleOnInfinitePrecision: 2),
|
||||||
|
decimalPlaces: 2,
|
||||||
|
locale: locale!);
|
||||||
|
|
||||||
|
final tx = models.Transaction(
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
confirmedStatus: false,
|
||||||
|
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
txType: "Sent",
|
||||||
|
amount: txData["recipientAmt"] as int,
|
||||||
|
worthNow: worthNow,
|
||||||
|
worthAtBlockTimestamp: worthNow,
|
||||||
|
fees: txData["fee"] as int,
|
||||||
|
inputSize: 0,
|
||||||
|
outputSize: 0,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
address: txData["address"] as String,
|
||||||
|
height: -1,
|
||||||
|
confirmations: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedTxData == null) {
|
||||||
|
final data = await _fetchTransactionData();
|
||||||
|
_transactionData = Future(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactions = cachedTxData!.getAllTransactions();
|
||||||
|
transactions[tx.txid] = tx;
|
||||||
|
cachedTxData = models.TransactionData.fromMap(transactions);
|
||||||
|
_transactionData = Future(() => cachedTxData!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
return Address.validateAddress(address, _network, namecoin.bech32!);
|
return Address.validateAddress(address, _network, namecoin.bech32!);
|
||||||
|
@ -2672,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
||||||
|
|
||||||
|
cachedTxData = txModel;
|
||||||
return txModel;
|
return txModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cw_core/get_height_by_date.dart';
|
||||||
import 'package:cw_core/monero_transaction_priority.dart';
|
import 'package:cw_core/monero_transaction_priority.dart';
|
||||||
import 'package:cw_core/node.dart';
|
import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
|
@ -647,7 +648,7 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: take in the default language when creating wallet.
|
//TODO: take in the default language when creating wallet.
|
||||||
Future<void> _generateNewWallet() async {
|
Future<void> _generateNewWallet({int seedWordsLength = 14}) async {
|
||||||
Logging.instance
|
Logging.instance
|
||||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||||
// TODO: ping wownero server and make sure the genesis hash matches
|
// TODO: ping wownero server and make sure the genesis hash matches
|
||||||
|
@ -685,9 +686,7 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
await pathForWalletDir(name: name, type: WalletType.wownero);
|
await pathForWalletDir(name: name, type: WalletType.wownero);
|
||||||
final path = await pathForWallet(name: name, type: WalletType.wownero);
|
final path = await pathForWallet(name: name, type: WalletType.wownero);
|
||||||
credentials = wownero.createWowneroNewWalletCredentials(
|
credentials = wownero.createWowneroNewWalletCredentials(
|
||||||
name: name,
|
name: name, language: "English", seedWordsLength: seedWordsLength);
|
||||||
language: "English",
|
|
||||||
);
|
|
||||||
|
|
||||||
walletInfo = WalletInfo.external(
|
walletInfo = WalletInfo.external(
|
||||||
id: WalletBase.idFor(name, WalletType.wownero),
|
id: WalletBase.idFor(name, WalletType.wownero),
|
||||||
|
@ -712,9 +711,12 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
// To restore from a seed
|
// To restore from a seed
|
||||||
final wallet = await _walletCreationService?.create(credentials);
|
final wallet = await _walletCreationService?.create(credentials);
|
||||||
|
|
||||||
// subtract a couple days to ensure we have a buffer for SWB
|
final bufferedCreateHeight = (seedWordsLength == 14)
|
||||||
final bufferedCreateHeight =
|
? getSeedHeightSync(wallet?.seed.trim() as String)
|
||||||
getSeedHeightSync(wallet?.seed.trim() as String);
|
: wownero.getHeightByDate(
|
||||||
|
date: DateTime.now().subtract(const Duration(
|
||||||
|
days:
|
||||||
|
2))); // subtract a couple days to ensure we have a buffer for SWB
|
||||||
|
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight);
|
boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight);
|
||||||
|
@ -722,6 +724,7 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
|
|
||||||
await _secureStore.write(
|
await _secureStore.write(
|
||||||
key: '${_walletId}_mnemonic', value: wallet?.seed.trim());
|
key: '${_walletId}_mnemonic', value: wallet?.seed.trim());
|
||||||
|
|
||||||
walletInfo.address = wallet?.walletAddresses.address;
|
walletInfo.address = wallet?.walletAddresses.address;
|
||||||
await DB.instance
|
await DB.instance
|
||||||
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
|
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
|
||||||
|
@ -778,7 +781,7 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement initializeWallet
|
// TODO: implement initializeWallet
|
||||||
Future<bool> initializeNew() async {
|
Future<bool> initializeNew({int seedWordsLength = 14}) async {
|
||||||
await _prefs.init();
|
await _prefs.init();
|
||||||
// TODO: ping actual wownero network
|
// TODO: ping actual wownero network
|
||||||
// try {
|
// try {
|
||||||
|
@ -796,7 +799,7 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
prefs = await SharedPreferences.getInstance();
|
prefs = await SharedPreferences.getInstance();
|
||||||
keysStorage = KeyService(storage!);
|
keysStorage = KeyService(storage!);
|
||||||
|
|
||||||
await _generateNewWallet();
|
await _generateNewWallet(seedWordsLength: seedWordsLength);
|
||||||
// var password;
|
// var password;
|
||||||
// try {
|
// try {
|
||||||
// password =
|
// password =
|
||||||
|
@ -942,6 +945,11 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
required int maxNumberOfIndexesToCheck,
|
required int maxNumberOfIndexesToCheck,
|
||||||
required int height,
|
required int height,
|
||||||
}) async {
|
}) async {
|
||||||
|
final int seedLength = mnemonic.trim().split(" ").length;
|
||||||
|
if (!(seedLength == 14 || seedLength == 25)) {
|
||||||
|
throw Exception("Invalid wownero mnemonic length found: $seedLength");
|
||||||
|
}
|
||||||
|
|
||||||
await _prefs.init();
|
await _prefs.init();
|
||||||
longMutex = true;
|
longMutex = true;
|
||||||
final start = DateTime.now();
|
final start = DateTime.now();
|
||||||
|
@ -969,7 +977,18 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
await _secureStore.write(
|
await _secureStore.write(
|
||||||
key: '${_walletId}_mnemonic', value: mnemonic.trim());
|
key: '${_walletId}_mnemonic', value: mnemonic.trim());
|
||||||
|
|
||||||
|
// extract seed height from 14 word seed
|
||||||
|
if (seedLength == 14) {
|
||||||
height = getSeedHeightSync(mnemonic.trim());
|
height = getSeedHeightSync(mnemonic.trim());
|
||||||
|
} else {
|
||||||
|
// 25 word seed. TODO validate
|
||||||
|
if (height == 0) {
|
||||||
|
height = wownero.getHeightByDate(
|
||||||
|
date: DateTime.now().subtract(const Duration(
|
||||||
|
days:
|
||||||
|
2))); // subtract a couple days to ensure we have a buffer for SWB\
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await DB.instance
|
await DB.instance
|
||||||
.put<dynamic>(boxName: walletId, key: "restoreHeight", value: height);
|
.put<dynamic>(boxName: walletId, key: "restoreHeight", value: height);
|
||||||
|
@ -1195,6 +1214,14 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
_transactionData ??= _fetchTransactionData();
|
_transactionData ??= _fetchTransactionData();
|
||||||
Future<TransactionData>? _transactionData;
|
Future<TransactionData>? _transactionData;
|
||||||
|
|
||||||
|
// not used in wownero
|
||||||
|
TransactionData? cachedTxData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
||||||
|
// not used in wownero
|
||||||
|
}
|
||||||
|
|
||||||
Future<TransactionData> _fetchTransactionData() async {
|
Future<TransactionData> _fetchTransactionData() async {
|
||||||
final transactions = walletBase?.transactionHistory!.transactions;
|
final transactions = walletBase?.transactionHistory!.transactions;
|
||||||
|
|
||||||
|
@ -1351,10 +1378,8 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement validateAddress
|
|
||||||
bool validateAddress(String address) {
|
bool validateAddress(String address) {
|
||||||
bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
|
bool valid = walletBase!.validateAddress(address);
|
||||||
RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1382,9 +1407,10 @@ class WowneroWallet extends CoinServiceAPI {
|
||||||
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
||||||
as bool;
|
as bool;
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance
|
Logging.instance.log(
|
||||||
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
"isFavorite fetch failed (returning false by default): $e\n$s",
|
||||||
rethrow;
|
level: LogLevel.Error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,8 @@ class _SVG {
|
||||||
String txExchangeFailed(BuildContext context) =>
|
String txExchangeFailed(BuildContext context) =>
|
||||||
"assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg";
|
"assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg";
|
||||||
|
|
||||||
|
String get framedGear => "assets/svg/framed-gear.svg";
|
||||||
|
String get framedAddressBook => "assets/svg/framed-address-book.svg";
|
||||||
String get themeLight => "assets/svg/light/light-mode.svg";
|
String get themeLight => "assets/svg/light/light-mode.svg";
|
||||||
String get themeDark => "assets/svg/dark/dark-theme.svg";
|
String get themeDark => "assets/svg/dark/dark-theme.svg";
|
||||||
String get circleNode => "assets/svg/node-circle.svg";
|
String get circleNode => "assets/svg/node-circle.svg";
|
||||||
|
@ -67,6 +69,7 @@ class _SVG {
|
||||||
String get circleLanguage => "assets/svg/language-circle.svg";
|
String get circleLanguage => "assets/svg/language-circle.svg";
|
||||||
String get circleDollarSign => "assets/svg/dollar-sign-circle.svg";
|
String get circleDollarSign => "assets/svg/dollar-sign-circle.svg";
|
||||||
String get circleLock => "assets/svg/lock-circle.svg";
|
String get circleLock => "assets/svg/lock-circle.svg";
|
||||||
|
String get enableButton => "assets/svg/enabled-button.svg";
|
||||||
String get disableButton => "assets/svg/Button.svg";
|
String get disableButton => "assets/svg/Button.svg";
|
||||||
String get polygon => "assets/svg/Polygon.svg";
|
String get polygon => "assets/svg/Polygon.svg";
|
||||||
String get personaIncognito => "assets/svg/persona-incognito-1.svg";
|
String get personaIncognito => "assets/svg/persona-incognito-1.svg";
|
||||||
|
|
|
@ -35,10 +35,6 @@ abstract class Constants {
|
||||||
|
|
||||||
static const int pinLength = 4;
|
static const int pinLength = 4;
|
||||||
|
|
||||||
// enable testnet
|
|
||||||
// TODO: currently unused
|
|
||||||
static const bool allowTestnets = true;
|
|
||||||
|
|
||||||
// Enable Logger.print statements
|
// Enable Logger.print statements
|
||||||
static const bool disableLogger = false;
|
static const bool disableLogger = false;
|
||||||
|
|
||||||
|
@ -66,7 +62,7 @@ abstract class Constants {
|
||||||
values.addAll([25]);
|
values.addAll([25]);
|
||||||
break;
|
break;
|
||||||
case Coin.wownero:
|
case Coin.wownero:
|
||||||
values.addAll([14]);
|
values.addAll([14, 25]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
|
|
89
lib/utilities/desktop_password_service.dart
Normal file
89
lib/utilities/desktop_password_service.dart
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:stack_wallet_backup/secure_storage.dart';
|
||||||
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
|
||||||
|
const String _kKeyBlobKey = "swbKeyBlobKeyStringID";
|
||||||
|
|
||||||
|
String _getMessageFromException(Object exception) {
|
||||||
|
if (exception is IncorrectPassphrase) {
|
||||||
|
return exception.errMsg();
|
||||||
|
}
|
||||||
|
if (exception is BadDecryption) {
|
||||||
|
return exception.errMsg();
|
||||||
|
}
|
||||||
|
if (exception is InvalidLength) {
|
||||||
|
return exception.errMsg();
|
||||||
|
}
|
||||||
|
if (exception is EncodingError) {
|
||||||
|
return exception.errMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class DPS {
|
||||||
|
StorageCryptoHandler? _handler;
|
||||||
|
final SecureStorageWrapper secureStorageWrapper;
|
||||||
|
|
||||||
|
StorageCryptoHandler get handler {
|
||||||
|
if (_handler == null) {
|
||||||
|
throw Exception(
|
||||||
|
"DPS: attempted to access handler without proper authentication");
|
||||||
|
}
|
||||||
|
return _handler!;
|
||||||
|
}
|
||||||
|
|
||||||
|
DPS({
|
||||||
|
this.secureStorageWrapper = const SecureStorageWrapper(
|
||||||
|
FlutterSecureStorage(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> initFromNew(String passphrase) async {
|
||||||
|
if (_handler != null) {
|
||||||
|
throw Exception("DPS: attempted to re initialize with new passphrase");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_handler = await StorageCryptoHandler.fromNewPassphrase(passphrase);
|
||||||
|
await secureStorageWrapper.write(
|
||||||
|
key: _kKeyBlobKey,
|
||||||
|
value: await _handler!.getKeyBlob(),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"${_getMessageFromException(e)}\n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initFromExisting(String passphrase) async {
|
||||||
|
if (_handler != null) {
|
||||||
|
throw Exception(
|
||||||
|
"DPS: attempted to re initialize with existing passphrase");
|
||||||
|
}
|
||||||
|
final keyBlob = await secureStorageWrapper.read(key: _kKeyBlobKey);
|
||||||
|
|
||||||
|
if (keyBlob == null) {
|
||||||
|
throw Exception(
|
||||||
|
"DPS: failed to find keyBlob while attempting to initialize with existing passphrase");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"${_getMessageFromException(e)}\n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasPassword() async {
|
||||||
|
return (await secureStorageWrapper.read(key: _kKeyBlobKey)) != null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,7 +132,7 @@ extension CoinExt on Coin {
|
||||||
case Coin.litecoinTestNet:
|
case Coin.litecoinTestNet:
|
||||||
return "litecoin";
|
return "litecoin";
|
||||||
case Coin.bitcoincashTestnet:
|
case Coin.bitcoincashTestnet:
|
||||||
return "bitcoincash";
|
return "bchtest";
|
||||||
case Coin.firoTestNet:
|
case Coin.firoTestNet:
|
||||||
return "firo";
|
return "firo";
|
||||||
case Coin.dogecoinTestNet:
|
case Coin.dogecoinTestNet:
|
||||||
|
|
|
@ -1,26 +1,121 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
Future<bool> testMoneroNodeConnection(Uri uri) async {
|
class MoneroNodeConnectionResponse {
|
||||||
|
final X509Certificate? cert;
|
||||||
|
final String? url;
|
||||||
|
final int? port;
|
||||||
|
final bool success;
|
||||||
|
|
||||||
|
MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MoneroNodeConnectionResponse> testMoneroNodeConnection(
|
||||||
|
Uri uri,
|
||||||
|
bool allowBadX509Certificate,
|
||||||
|
) async {
|
||||||
|
final client = HttpClient();
|
||||||
|
MoneroNodeConnectionResponse? badCertResponse;
|
||||||
try {
|
try {
|
||||||
final client = http.Client();
|
client.badCertificateCallback = (cert, url, port) {
|
||||||
final response = await client
|
if (allowBadX509Certificate) {
|
||||||
.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}),
|
|
||||||
)
|
|
||||||
.timeout(const Duration(milliseconds: 1200),
|
|
||||||
onTimeout: () async => http.Response('Error', 408));
|
|
||||||
|
|
||||||
final result = jsonDecode(response.body);
|
|
||||||
// TODO: json decoded without error so assume connection exists?
|
|
||||||
// or we can check for certain values in the response to decide
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e, s) {
|
}
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
|
||||||
|
if (badCertResponse == null) {
|
||||||
|
badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false);
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
final request = await client.postUrl(uri);
|
||||||
|
|
||||||
|
final body = utf8.encode(
|
||||||
|
jsonEncode({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "0",
|
||||||
|
"method": "get_info",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
request.headers.add(
|
||||||
|
'Content-Length',
|
||||||
|
body.length.toString(),
|
||||||
|
preserveHeaderCase: true,
|
||||||
|
);
|
||||||
|
request.headers.set(
|
||||||
|
'Content-Type',
|
||||||
|
'application/json',
|
||||||
|
preserveHeaderCase: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
request.add(body);
|
||||||
|
|
||||||
|
final response = await request.close();
|
||||||
|
final result = await response.transform(utf8.decoder).join();
|
||||||
|
// TODO: json decoded without error so assume connection exists?
|
||||||
|
// or we can check for certain values in the response to decide
|
||||||
|
return MoneroNodeConnectionResponse(null, null, null, true);
|
||||||
|
} catch (e, s) {
|
||||||
|
if (badCertResponse != null) {
|
||||||
|
return badCertResponse!;
|
||||||
|
} else {
|
||||||
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
return MoneroNodeConnectionResponse(null, null, null, false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close(force: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showBadX509CertificateDialog(
|
||||||
|
X509Certificate cert,
|
||||||
|
String url,
|
||||||
|
int port,
|
||||||
|
BuildContext context,
|
||||||
|
) async {
|
||||||
|
final chars = Format.uint8listToString(cert.sha1)
|
||||||
|
.toUpperCase()
|
||||||
|
.characters
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
String sha1 = chars.sublist(0, 2).join();
|
||||||
|
for (int i = 2; i < chars.length; i += 2) {
|
||||||
|
sha1 += ":${chars.sublist(i, i + 2).join()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) {
|
||||||
|
return StackDialog(
|
||||||
|
title: "Untrusted X509Certificate",
|
||||||
|
message: "SHA1:\n$sha1",
|
||||||
|
leftButton: SecondaryButton(
|
||||||
|
label: "Cancel",
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
rightButton: PrimaryButton(
|
||||||
|
label: "Trust",
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -508,6 +508,25 @@ class STextStyles {
|
||||||
|
|
||||||
// Desktop
|
// Desktop
|
||||||
|
|
||||||
|
static TextStyle desktopH1(BuildContext context) {
|
||||||
|
switch (_theme(context).themeType) {
|
||||||
|
case ThemeType.light:
|
||||||
|
return GoogleFonts.inter(
|
||||||
|
color: _theme(context).textDark,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 40,
|
||||||
|
height: 40 / 40,
|
||||||
|
);
|
||||||
|
case ThemeType.dark:
|
||||||
|
return GoogleFonts.inter(
|
||||||
|
color: _theme(context).textDark,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 40,
|
||||||
|
height: 40 / 40,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static TextStyle desktopH2(BuildContext context) {
|
static TextStyle desktopH2(BuildContext context) {
|
||||||
switch (_theme(context).themeType) {
|
switch (_theme(context).themeType) {
|
||||||
case ThemeType.light:
|
case ThemeType.light:
|
||||||
|
|
|
@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget {
|
||||||
required this.text,
|
required this.text,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.textSize,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
final double? textSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
|
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
|
||||||
|
@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: widget.text,
|
text: widget.text,
|
||||||
style: STextStyles.link2(context).copyWith(color: color),
|
style: widget.textSize == null
|
||||||
|
? STextStyles.link2(context).copyWith(
|
||||||
|
color: color,
|
||||||
|
)
|
||||||
|
: STextStyles.link2(context).copyWith(
|
||||||
|
color: color,
|
||||||
|
fontSize: widget.textSize,
|
||||||
|
),
|
||||||
recognizer: widget.enabled
|
recognizer: widget.enabled
|
||||||
? (TapGestureRecognizer()
|
? (TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
|
|
|
@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState<NodeCard> {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
||||||
|
|
||||||
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.cert != null) {
|
||||||
|
if (mounted) {
|
||||||
|
final shouldAllowBadCert = await showBadX509CertificateDialog(
|
||||||
|
response.cert!,
|
||||||
|
response.url!,
|
||||||
|
response.port!,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldAllowBadCert) {
|
||||||
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString), true);
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget {
|
||||||
|
|
||||||
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
String uriString = "${uri.scheme}://${uri.host}:${node.port}$path";
|
||||||
|
|
||||||
testPassed = await testMoneroNodeConnection(Uri.parse(uriString));
|
final response = await testMoneroNodeConnection(
|
||||||
|
Uri.parse(uriString),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.cert != null) {
|
||||||
|
// if (mounted) {
|
||||||
|
final shouldAllowBadCert = await showBadX509CertificateDialog(
|
||||||
|
response.cert!,
|
||||||
|
response.url!,
|
||||||
|
response.port!,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldAllowBadCert) {
|
||||||
|
final response =
|
||||||
|
await testMoneroNodeConnection(Uri.parse(uriString), true);
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
testPassed = response.success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stackwallet/utilities/text_styles.dart';
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
|
||||||
class StackDialogBase extends StatelessWidget {
|
class StackDialogBase extends StatelessWidget {
|
||||||
const StackDialogBase({
|
const StackDialogBase({
|
||||||
|
@ -17,7 +18,8 @@ class StackDialogBase extends StatelessWidget {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment:
|
||||||
|
!Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Material(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
@ -179,9 +181,15 @@ class StackOkDialog extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: !Util.isDesktop
|
||||||
|
? () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
onOkPressed?.call("OK");
|
onOkPressed?.call("OK");
|
||||||
|
}
|
||||||
|
: () {
|
||||||
|
int count = 0;
|
||||||
|
Navigator.of(context).popUntil((_) => count++ >= 2);
|
||||||
|
// onOkPressed?.call("OK");
|
||||||
},
|
},
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
|
|
|
@ -1378,8 +1378,8 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
|
ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d"
|
||||||
resolved-ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
|
resolved-ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d"
|
||||||
url: "https://github.com/cypherstack/stack_wallet_backup.git"
|
url: "https://github.com/cypherstack/stack_wallet_backup.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|
|
@ -11,7 +11,7 @@ description: Stack Wallet
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.5.14+86
|
version: 1.5.17+89
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
@ -54,7 +54,7 @@ dependencies:
|
||||||
stack_wallet_backup:
|
stack_wallet_backup:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/cypherstack/stack_wallet_backup.git
|
url: https://github.com/cypherstack/stack_wallet_backup.git
|
||||||
ref: b7b184ec36466f2a24104a7056de88881cb0c1e9
|
ref: 011dc9ce3d29f5fdeeaf711d58b5122f055c146d
|
||||||
|
|
||||||
# Utility plugins
|
# Utility plugins
|
||||||
# provider: ^6.0.1
|
# provider: ^6.0.1
|
||||||
|
@ -298,6 +298,7 @@ flutter:
|
||||||
- assets/svg/persona-easy-1.svg
|
- assets/svg/persona-easy-1.svg
|
||||||
- assets/svg/persona-incognito-1.svg
|
- assets/svg/persona-incognito-1.svg
|
||||||
- assets/svg/Button.svg
|
- assets/svg/Button.svg
|
||||||
|
- assets/svg/enabled-button.svg
|
||||||
- assets/svg/lock-circle.svg
|
- assets/svg/lock-circle.svg
|
||||||
- assets/svg/dollar-sign-circle.svg
|
- assets/svg/dollar-sign-circle.svg
|
||||||
- assets/svg/language-circle.svg
|
- assets/svg/language-circle.svg
|
||||||
|
@ -338,6 +339,8 @@ flutter:
|
||||||
- assets/svg/message-question-1.svg
|
- assets/svg/message-question-1.svg
|
||||||
- assets/svg/drd-icon.svg
|
- assets/svg/drd-icon.svg
|
||||||
- assets/svg/box-auto.svg
|
- assets/svg/box-auto.svg
|
||||||
|
- assets/svg/framed-address-book.svg
|
||||||
|
- assets/svg/framed-gear.svg
|
||||||
# exchange icons
|
# exchange icons
|
||||||
- assets/svg/exchange_icons/change_now_logo_1.svg
|
- assets/svg/exchange_icons/change_now_logo_1.svg
|
||||||
- assets/svg/exchange_icons/simpleswap-icon.svg
|
- assets/svg/exchange_icons/simpleswap-icon.svg
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
LINUX_DIRECTORY=$(pwd)
|
LINUX_DIRECTORY=$(pwd)
|
||||||
mkdir build
|
mkdir -p build
|
||||||
|
|
||||||
# Build JsonCPP
|
# Build JsonCPP
|
||||||
cd build
|
cd build || exit
|
||||||
git clone https://github.com/open-source-parsers/jsoncpp.git
|
if ! [ -x "$(command -v git)" ]; then
|
||||||
cd jsoncpp
|
echo 'Error: git is not installed.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git -C jsoncpp pull || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp
|
||||||
|
cd jsoncpp || exit
|
||||||
git checkout 1.7.4
|
git checkout 1.7.4
|
||||||
mkdir build
|
mkdir -p build
|
||||||
cd build
|
cd build || exit
|
||||||
cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" ..
|
cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" ..
|
||||||
make -j$(nproc)
|
make -j"$(nproc)"
|
||||||
|
|
||||||
cd $LINUX_DIRECTORY
|
cd "$LINUX_DIRECTORY" || exit
|
||||||
# Build libSecret
|
# Build libSecret
|
||||||
# sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl
|
# sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl
|
||||||
# sudo apt install python3-pip
|
# sudo apt install python3-pip
|
||||||
#pip3 install --user meson --upgrade
|
#pip3 install --user meson --upgrade
|
||||||
# pip3 install --user gi-docgen
|
# pip3 install --user gi-docgen
|
||||||
cd build
|
cd build || exit
|
||||||
git clone https://gitlab.gnome.org/GNOME/libsecret.git
|
git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret
|
||||||
cd libsecret
|
cd libsecret || exit
|
||||||
|
if ! [ -x "$(command -v meson)" ]; then
|
||||||
|
echo 'Error: meson is not installed.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
meson _build
|
meson _build
|
||||||
|
if ! [ -x "$(command -v ninja)" ]; then
|
||||||
|
echo 'Error: ninja is not installed.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
ninja -C _build
|
ninja -C _build
|
||||||
|
|
|
@ -94,19 +94,19 @@ void main() {
|
||||||
test("get contacts", () {
|
test("get contacts", () {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("get addressBookEntries", () async {
|
test("get addressBookEntries", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
expect((await service.addressBookEntries).toString(),
|
expect((await service.addressBookEntries).toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("search contacts", () async {
|
test("search contacts", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
final results = await service.search("j");
|
final results = await service.search("j");
|
||||||
expect(results.toString(), [contactA, contactB].toString());
|
expect(results.toString(), [contactB, contactA].toString());
|
||||||
|
|
||||||
final results2 = await service.search("ja");
|
final results2 = await service.search("ja");
|
||||||
expect(results2.toString(), [contactB].toString());
|
expect(results2.toString(), [contactB].toString());
|
||||||
|
@ -118,7 +118,7 @@ void main() {
|
||||||
expect(results4.toString(), <Contact>[].toString());
|
expect(results4.toString(), <Contact>[].toString());
|
||||||
|
|
||||||
final results5 = await service.search("");
|
final results5 = await service.search("");
|
||||||
expect(results5.toString(), [contactA, contactB, contactC].toString());
|
expect(results5.toString(), [contactC, contactB, contactA].toString());
|
||||||
|
|
||||||
final results6 = await service.search("epic address");
|
final results6 = await service.search("epic address");
|
||||||
expect(results6.toString(), [contactC].toString());
|
expect(results6.toString(), [contactC].toString());
|
||||||
|
@ -140,7 +140,7 @@ void main() {
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("edit contact", () async {
|
test("edit contact", () async {
|
||||||
|
@ -149,14 +149,14 @@ void main() {
|
||||||
expect(await service.editContact(editedContact), true);
|
expect(await service.editContact(editedContact), true);
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, editedContact, contactC].toString());
|
[contactC, contactA, editedContact].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("remove existing contact", () async {
|
test("remove existing contact", () async {
|
||||||
final service = AddressBookService();
|
final service = AddressBookService();
|
||||||
await service.removeContact(contactB.id);
|
await service.removeContact(contactB.id);
|
||||||
expect(service.contacts.length, 2);
|
expect(service.contacts.length, 2);
|
||||||
expect(service.contacts.toString(), [contactA, contactC].toString());
|
expect(service.contacts.toString(), [contactC, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("remove non existing contact", () async {
|
test("remove non existing contact", () async {
|
||||||
|
@ -164,7 +164,7 @@ void main() {
|
||||||
await service.removeContact("some id");
|
await service.removeContact("some id");
|
||||||
expect(service.contacts.length, 3);
|
expect(service.contacts.length, 3);
|
||||||
expect(service.contacts.toString(),
|
expect(service.contacts.toString(),
|
||||||
[contactA, contactB, contactC].toString());
|
[contactC, contactB, contactA].toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:bitcoindart/bitcoindart.dart';
|
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
@ -61,7 +60,7 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group("validate mainnet bitcoincash addresses", () {
|
group("mainnet bitcoincash addressType", () {
|
||||||
MockElectrumX? client;
|
MockElectrumX? client;
|
||||||
MockCachedElectrumX? cachedClient;
|
MockCachedElectrumX? cachedClient;
|
||||||
MockPriceAPI? priceAPI;
|
MockPriceAPI? priceAPI;
|
||||||
|
@ -137,10 +136,172 @@ void main() {
|
||||||
verifyNoMoreInteractions(priceAPI);
|
verifyNoMoreInteractions(priceAPI);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("P2PKH cashaddr with prefix", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.addressType(
|
||||||
|
address:
|
||||||
|
"bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
|
||||||
|
DerivePathType.bip44);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("P2PKH cashaddr without prefix", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.addressType(
|
||||||
|
address: "qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
|
||||||
|
DerivePathType.bip44);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multisig cashaddr with prefix", () {
|
||||||
|
expect(
|
||||||
|
() => mainnetWallet?.addressType(
|
||||||
|
address:
|
||||||
|
"bitcoincash:pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
|
||||||
|
throwsArgumentError);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multisig cashaddr without prefix", () {
|
||||||
|
expect(
|
||||||
|
() => mainnetWallet?.addressType(
|
||||||
|
address: "pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
|
||||||
|
throwsArgumentError);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multisig/P2SH address", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.addressType(
|
||||||
|
address: "3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"),
|
||||||
|
DerivePathType.bip49);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("validate mainnet bitcoincash addresses", () {
|
||||||
|
MockElectrumX? client;
|
||||||
|
MockCachedElectrumX? cachedClient;
|
||||||
|
MockPriceAPI? priceAPI;
|
||||||
|
FakeSecureStorage? secureStore;
|
||||||
|
MockTransactionNotificationTracker? tracker;
|
||||||
|
|
||||||
|
BitcoinCashWallet? mainnetWallet;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
client = MockElectrumX();
|
||||||
|
cachedClient = MockCachedElectrumX();
|
||||||
|
priceAPI = MockPriceAPI();
|
||||||
|
secureStore = FakeSecureStorage();
|
||||||
|
tracker = MockTransactionNotificationTracker();
|
||||||
|
|
||||||
|
mainnetWallet = BitcoinCashWallet(
|
||||||
|
walletId: "validateAddressMainNet",
|
||||||
|
walletName: "validateAddressMainNet",
|
||||||
|
coin: Coin.bitcoincash,
|
||||||
|
client: client!,
|
||||||
|
cachedClient: cachedClient!,
|
||||||
|
tracker: tracker!,
|
||||||
|
priceAPI: priceAPI,
|
||||||
|
secureStore: secureStore,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid mainnet legacy/p2pkh address type", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.validateAddress("1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"),
|
||||||
|
true);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid mainnet legacy/p2pkh cashaddr with prefix address type", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.validateAddress(
|
||||||
|
"bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
|
||||||
|
true);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid mainnet legacy/p2pkh cashaddr without prefix address type", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet
|
||||||
|
?.validateAddress("qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"),
|
||||||
|
true);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid legacy/p2pkh address type", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
|
||||||
|
false);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"invalid cashaddr (is valid multisig but bitbox is broken for multisig)",
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
mainnetWallet
|
||||||
|
?.validateAddress("pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"),
|
||||||
|
false);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multisig address should fail for bitbox", () {
|
||||||
|
expect(
|
||||||
|
mainnetWallet?.validateAddress("3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"),
|
||||||
|
false);
|
||||||
|
expect(secureStore?.interactions, 0);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
verifyNoMoreInteractions(tracker);
|
||||||
|
verifyNoMoreInteractions(priceAPI);
|
||||||
|
});
|
||||||
|
|
||||||
test("invalid mainnet bitcoincash legacy/p2pkh address", () {
|
test("invalid mainnet bitcoincash legacy/p2pkh address", () {
|
||||||
expect(
|
expect(
|
||||||
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
|
mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"),
|
||||||
true);
|
false);
|
||||||
expect(secureStore?.interactions, 0);
|
expect(secureStore?.interactions, 0);
|
||||||
verifyNoMoreInteractions(client);
|
verifyNoMoreInteractions(client);
|
||||||
verifyNoMoreInteractions(cachedClient);
|
verifyNoMoreInteractions(cachedClient);
|
||||||
|
|
|
@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI {
|
||||||
// TODO: implement generateNewAddress
|
// TODO: implement generateNewAddress
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) {
|
||||||
|
// TODO: implement updateSentCachedTxData
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
234
test/services/coins/monero/monero_wallet_test.dart
Normal file
234
test/services/coins/monero/monero_wallet_test.dart
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
import 'dart:core';
|
||||||
|
import 'dart:core' as core;
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cw_core/node.dart';
|
||||||
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
import 'package:cw_core/wallet_service.dart';
|
||||||
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
import 'package:cw_monero/monero_wallet.dart';
|
||||||
|
import 'package:flutter_libmonero/core/key_service.dart';
|
||||||
|
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
|
||||||
|
import 'package:flutter_libmonero/monero/monero.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:hive_test/hive_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:stackwallet/services/wallets.dart';
|
||||||
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
|
||||||
|
// TODO trim down to the minimum imports above
|
||||||
|
|
||||||
|
import 'monero_wallet_test_data.dart';
|
||||||
|
|
||||||
|
//FlutterSecureStorage? storage;
|
||||||
|
FakeSecureStorage? storage;
|
||||||
|
WalletService? walletService;
|
||||||
|
SharedPreferences? prefs;
|
||||||
|
KeyService? keysStorage;
|
||||||
|
MoneroWalletBase? walletBase;
|
||||||
|
late WalletCreationService _walletCreationService;
|
||||||
|
dynamic _walletInfoSource;
|
||||||
|
Wallets? walletsService;
|
||||||
|
|
||||||
|
String path = '';
|
||||||
|
|
||||||
|
String name = 'namee${Random().nextInt(10000000)}';
|
||||||
|
int nettype = 0;
|
||||||
|
WalletType type = WalletType.monero;
|
||||||
|
|
||||||
|
@GenerateMocks([])
|
||||||
|
void main() async {
|
||||||
|
storage = FakeSecureStorage();
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
keysStorage = KeyService(storage!);
|
||||||
|
WalletInfo walletInfo = WalletInfo.external(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: '',
|
||||||
|
address: '',
|
||||||
|
dirPath: '');
|
||||||
|
late WalletCredentials credentials;
|
||||||
|
|
||||||
|
monero.onStartup();
|
||||||
|
|
||||||
|
bool hiveAdaptersRegistered = false;
|
||||||
|
|
||||||
|
group("Mainnet tests", () {
|
||||||
|
setUp(() async {
|
||||||
|
await setUpTestHive();
|
||||||
|
if (!hiveAdaptersRegistered) {
|
||||||
|
hiveAdaptersRegistered = true;
|
||||||
|
|
||||||
|
Hive.registerAdapter(NodeAdapter());
|
||||||
|
Hive.registerAdapter(WalletInfoAdapter());
|
||||||
|
Hive.registerAdapter(WalletTypeAdapter());
|
||||||
|
Hive.registerAdapter(UnspentCoinsInfoAdapter());
|
||||||
|
|
||||||
|
final wallets = await Hive.openBox('wallets');
|
||||||
|
await wallets.put('currentWalletName', name);
|
||||||
|
|
||||||
|
_walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
|
||||||
|
walletService = monero
|
||||||
|
.createMoneroWalletService(_walletInfoSource as Box<WalletInfo>);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// if (name?.isEmpty ?? true) {
|
||||||
|
// name = await generateName();
|
||||||
|
// }
|
||||||
|
final dirPath = await pathForWalletDir(name: name, type: type);
|
||||||
|
path = await pathForWallet(name: name, type: type);
|
||||||
|
credentials =
|
||||||
|
// // creating a new wallet
|
||||||
|
// monero.createMoneroNewWalletCredentials(
|
||||||
|
// name: name, language: "English");
|
||||||
|
// restoring a previous wallet
|
||||||
|
monero.createMoneroRestoreWalletFromSeedCredentials(
|
||||||
|
name: name, height: 2580000, mnemonic: testMnemonic);
|
||||||
|
|
||||||
|
walletInfo = WalletInfo.external(
|
||||||
|
id: WalletBase.idFor(name, type),
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: credentials.height ?? 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: path,
|
||||||
|
address: "",
|
||||||
|
dirPath: dirPath);
|
||||||
|
credentials.walletInfo = walletInfo;
|
||||||
|
|
||||||
|
_walletCreationService = WalletCreationService(
|
||||||
|
secureStorage: storage,
|
||||||
|
sharedPreferences: prefs,
|
||||||
|
walletService: walletService,
|
||||||
|
keyService: keysStorage,
|
||||||
|
);
|
||||||
|
_walletCreationService.changeWalletType();
|
||||||
|
} catch (e, s) {
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test mainnet address generation from seed", () async {
|
||||||
|
final wallet = await
|
||||||
|
// _walletCreationService.create(credentials);
|
||||||
|
_walletCreationService.restoreFromSeed(credentials);
|
||||||
|
walletInfo.address = wallet.walletAddresses.address;
|
||||||
|
//print(walletInfo.address);
|
||||||
|
|
||||||
|
await _walletInfoSource.add(walletInfo);
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as MoneroWalletBase;
|
||||||
|
//print("${walletBase?.seed}");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress(walletInfo.address ?? ''), true);
|
||||||
|
|
||||||
|
// print(walletBase);
|
||||||
|
// loggerPrint(walletBase.toString());
|
||||||
|
// loggerPrint("name: ${walletBase!.name} seed: ${walletBase!.seed} id: "
|
||||||
|
// "${walletBase!.id} walletinfo: ${toStringForinfo(walletBase!.walletInfo)} type: ${walletBase!.type} balance: "
|
||||||
|
// "${walletBase!.balance.entries.first.value.available} currency: ${walletBase!.currency}");
|
||||||
|
|
||||||
|
expect(walletInfo.address, mainnetTestData[0][0]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]);
|
||||||
|
expect(
|
||||||
|
await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress(''), false);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
// Not needed; only folder created, wallet files not saved yet. TODO test saving and deleting wallet files and make sure to clean up leftover folder afterwards
|
||||||
|
group("Mainnet wallet deletion test", () {
|
||||||
|
test("Test mainnet wallet existence", () {
|
||||||
|
expect(monero_wallet_manager.isWalletExistSync(path: path), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test mainnet wallet deletion", () {
|
||||||
|
// Remove wallet from wallet service
|
||||||
|
walletService?.remove(name);
|
||||||
|
walletsService?.removeWallet(walletId: name);
|
||||||
|
expect(monero_wallet_manager.isWalletExistSync(path: path), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Mainnet node tests", () {
|
||||||
|
test("Test mainnet node connection", () async {
|
||||||
|
await walletBase?.connectToNode(
|
||||||
|
node: Node(
|
||||||
|
uri: "monero-stagenet.stackwallet.com:38081",
|
||||||
|
type: WalletType.moneroStageNet));
|
||||||
|
await walletBase!.rescan(
|
||||||
|
height:
|
||||||
|
credentials.height); // Probably shouldn't be rescanning from 0...
|
||||||
|
await walletBase!.getNodeHeight();
|
||||||
|
int height = await walletBase!.getNodeHeight();
|
||||||
|
print('height: $height');
|
||||||
|
bool connected = await walletBase!.isConnected();
|
||||||
|
print('connected: $connected');
|
||||||
|
|
||||||
|
//expect...
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO test deletion of wallets ... and delete them
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> pathForWalletDir(
|
||||||
|
{required String name, required WalletType type}) async {
|
||||||
|
Directory root = (await getApplicationDocumentsDirectory());
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
root = (await getLibraryDirectory());
|
||||||
|
}
|
||||||
|
final prefix = walletTypeToString(type).toLowerCase();
|
||||||
|
final walletsDir = Directory('${root.path}/wallets');
|
||||||
|
final walletDire = Directory('${walletsDir.path}/$prefix/$name');
|
||||||
|
|
||||||
|
if (!walletDire.existsSync()) {
|
||||||
|
walletDire.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletDire.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> pathForWallet(
|
||||||
|
{required String name, required WalletType type}) async =>
|
||||||
|
await pathForWalletDir(name: name, type: type)
|
||||||
|
.then((path) => path + '/$name');
|
14
test/services/coins/monero/monero_wallet_test_data.dart
Normal file
14
test/services/coins/monero/monero_wallet_test_data.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
String testMnemonic =
|
||||||
|
'agreed aquarium wallets uptight karate wonders afoot guys itself nucleus reduce lamb fully fewest bimonthly dazed skulls magically mocked fugitive imbalance saga calamity dialect itself';
|
||||||
|
var mainnetTestData = [
|
||||||
|
[
|
||||||
|
'4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn',
|
||||||
|
'82WsoLmbZt3BPwJMF5PfT8GitThJzUq3FFoSQyr4fKfJdxZebgY3mHPcnAqTBA3FFwZRGxC4ZDwkfE1VVULPa55x3xXgCbj',
|
||||||
|
'84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'86SF44CsTBYU3vk1X7nGBbQnrUSknGbd6Uw8a9hUUgy3KBeXTDvk3pm8upMzZKw17m3mLPEzbcPp5WLpYVoHR5PKNVtFrHH',
|
||||||
|
'8Aa9LNGdBHwYUMsy6M9ZVXMEkTBZyEDT7aQmY32trCxbU6dwkZJSCSbcpyL7UiTB9QXXosomZtJYvUJ296vTNX5yQ81KaA2',
|
||||||
|
'85C5zZRcaD89PKmXEwjcYMVAUqoH5rrAXe3GokvSupXnDmccYvZagz5Qem7bQLteEw4iFEJ9oRk9BNfjTi4K2cyTJbTMMPT'
|
||||||
|
]
|
||||||
|
];
|
372
test/services/coins/wownero/wownero_wallet_test.dart
Normal file
372
test/services/coins/wownero/wownero_wallet_test.dart
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
import 'dart:core';
|
||||||
|
import 'dart:core' as core;
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cw_core/node.dart';
|
||||||
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
import 'package:cw_core/wallet_service.dart';
|
||||||
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
import 'package:cw_wownero/wownero_wallet.dart';
|
||||||
|
import 'package:flutter_libmonero/core/key_service.dart';
|
||||||
|
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
|
||||||
|
import 'package:flutter_libmonero/wownero/wownero.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:hive_test/hive_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
|
||||||
|
import 'wownero_wallet_test_data.dart';
|
||||||
|
|
||||||
|
FakeSecureStorage? storage;
|
||||||
|
WalletService? walletService;
|
||||||
|
SharedPreferences? prefs;
|
||||||
|
KeyService? keysStorage;
|
||||||
|
WowneroWalletBase? walletBase;
|
||||||
|
late WalletCreationService _walletCreationService;
|
||||||
|
dynamic _walletInfoSource;
|
||||||
|
|
||||||
|
String path = '';
|
||||||
|
|
||||||
|
String name = '';
|
||||||
|
int nettype = 0;
|
||||||
|
WalletType type = WalletType.wownero;
|
||||||
|
|
||||||
|
@GenerateMocks([])
|
||||||
|
void main() async {
|
||||||
|
storage = FakeSecureStorage();
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
keysStorage = KeyService(storage!);
|
||||||
|
WalletInfo walletInfo = WalletInfo.external(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: '',
|
||||||
|
address: '',
|
||||||
|
dirPath: '');
|
||||||
|
late WalletCredentials credentials;
|
||||||
|
|
||||||
|
wownero.onStartup();
|
||||||
|
|
||||||
|
bool hiveAdaptersRegistered = false;
|
||||||
|
|
||||||
|
group("Wownero 14 word seed generation", () {
|
||||||
|
setUp(() async {
|
||||||
|
await setUpTestHive();
|
||||||
|
if (!hiveAdaptersRegistered) {
|
||||||
|
hiveAdaptersRegistered = true;
|
||||||
|
|
||||||
|
Hive.registerAdapter(NodeAdapter());
|
||||||
|
Hive.registerAdapter(WalletInfoAdapter());
|
||||||
|
Hive.registerAdapter(WalletTypeAdapter());
|
||||||
|
Hive.registerAdapter(UnspentCoinsInfoAdapter());
|
||||||
|
|
||||||
|
final wallets = await Hive.openBox('wallets');
|
||||||
|
await wallets.put('currentWalletName', name);
|
||||||
|
|
||||||
|
_walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
|
||||||
|
walletService = wownero
|
||||||
|
.createWowneroWalletService(_walletInfoSource as Box<WalletInfo>);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
name = 'namee${Random().nextInt(10000000)}';
|
||||||
|
final dirPath = await pathForWalletDir(name: name, type: type);
|
||||||
|
path = await pathForWallet(name: name, type: type);
|
||||||
|
credentials = wownero.createWowneroNewWalletCredentials(
|
||||||
|
name: name,
|
||||||
|
language: "English",
|
||||||
|
seedWordsLength: 14); // TODO catch failure
|
||||||
|
|
||||||
|
walletInfo = WalletInfo.external(
|
||||||
|
id: WalletBase.idFor(name, type),
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: credentials.height ?? 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: path,
|
||||||
|
address: "",
|
||||||
|
dirPath: dirPath);
|
||||||
|
credentials.walletInfo = walletInfo;
|
||||||
|
|
||||||
|
_walletCreationService = WalletCreationService(
|
||||||
|
secureStorage: storage,
|
||||||
|
sharedPreferences: prefs,
|
||||||
|
walletService: walletService,
|
||||||
|
keyService: keysStorage,
|
||||||
|
);
|
||||||
|
_walletCreationService.changeWalletType();
|
||||||
|
} catch (e, s) {
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wownero 14 word seed address generation", () async {
|
||||||
|
final wallet = await _walletCreationService.create(credentials);
|
||||||
|
// TODO validate mnemonic
|
||||||
|
walletInfo.address = wallet.walletAddresses.address;
|
||||||
|
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
await _walletInfoSource.add(walletInfo);
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress(wallet.walletAddresses.address ?? ''), true);
|
||||||
|
} catch (_) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
|
||||||
|
// Address validation
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress(''), false);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true);
|
||||||
|
expect(
|
||||||
|
await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false);
|
||||||
|
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO delete left over wallet file with name: name
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Wownero 14 word seed restoration", () {
|
||||||
|
setUp(() async {
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
name = 'namee${Random().nextInt(10000000)}';
|
||||||
|
final dirPath = await pathForWalletDir(name: name, type: type);
|
||||||
|
path = await pathForWallet(name: name, type: type);
|
||||||
|
credentials = wownero.createWowneroRestoreWalletFromSeedCredentials(
|
||||||
|
name: name,
|
||||||
|
height: 465760,
|
||||||
|
mnemonic: testMnemonic14); // TODO catch failure
|
||||||
|
|
||||||
|
walletInfo = WalletInfo.external(
|
||||||
|
id: WalletBase.idFor(name, type),
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: credentials.height ?? 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: path,
|
||||||
|
address: "",
|
||||||
|
dirPath: dirPath);
|
||||||
|
credentials.walletInfo = walletInfo;
|
||||||
|
|
||||||
|
_walletCreationService = WalletCreationService(
|
||||||
|
secureStorage: storage,
|
||||||
|
sharedPreferences: prefs,
|
||||||
|
walletService: walletService,
|
||||||
|
keyService: keysStorage,
|
||||||
|
);
|
||||||
|
_walletCreationService.changeWalletType();
|
||||||
|
} catch (e, s) {
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wownero 14 word seed address generation", () async {
|
||||||
|
final wallet = await _walletCreationService.restoreFromSeed(credentials);
|
||||||
|
walletInfo.address = wallet.walletAddresses.address;
|
||||||
|
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
await _walletInfoSource.add(walletInfo);
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
|
||||||
|
expect(walletInfo.address, mainnetTestData14[0][0]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(0, 0),
|
||||||
|
mainnetTestData14[0][0]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(0, 1),
|
||||||
|
mainnetTestData14[0][1]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(0, 2),
|
||||||
|
mainnetTestData14[0][2]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(1, 0),
|
||||||
|
mainnetTestData14[1][0]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(1, 1),
|
||||||
|
mainnetTestData14[1][1]);
|
||||||
|
expect(await walletBase!.getTransactionAddress(1, 2),
|
||||||
|
mainnetTestData14[1][2]);
|
||||||
|
} catch (_) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO delete left over wallet file with name: name
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Wownero 25 word seed generation", () {
|
||||||
|
setUp(() async {
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
name = 'namee${Random().nextInt(10000000)}';
|
||||||
|
final dirPath = await pathForWalletDir(name: name, type: type);
|
||||||
|
path = await pathForWallet(name: name, type: type);
|
||||||
|
credentials = wownero.createWowneroNewWalletCredentials(
|
||||||
|
name: name,
|
||||||
|
language: "English",
|
||||||
|
seedWordsLength: 25); // TODO catch failure
|
||||||
|
|
||||||
|
walletInfo = WalletInfo.external(
|
||||||
|
id: WalletBase.idFor(name, type),
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: credentials.height ?? 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: path,
|
||||||
|
address: "",
|
||||||
|
dirPath: dirPath);
|
||||||
|
credentials.walletInfo = walletInfo;
|
||||||
|
|
||||||
|
_walletCreationService = WalletCreationService(
|
||||||
|
secureStorage: storage,
|
||||||
|
sharedPreferences: prefs,
|
||||||
|
walletService: walletService,
|
||||||
|
keyService: keysStorage,
|
||||||
|
);
|
||||||
|
_walletCreationService.changeWalletType();
|
||||||
|
} catch (e, s) {
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wownero 25 word seed address generation", () async {
|
||||||
|
final wallet = await _walletCreationService.create(credentials);
|
||||||
|
// TODO validate mnemonic
|
||||||
|
walletInfo.address = wallet.walletAddresses.address;
|
||||||
|
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
await _walletInfoSource.add(walletInfo);
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
|
||||||
|
// TODO validate
|
||||||
|
//expect(walletInfo.address, mainnetTestData14[0][0]);
|
||||||
|
} catch (_) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO delete left over wallet file with name: name
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Wownero 25 word seed restoration", () {
|
||||||
|
setUp(() async {
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
name = 'namee${Random().nextInt(10000000)}';
|
||||||
|
final dirPath = await pathForWalletDir(name: name, type: type);
|
||||||
|
path = await pathForWallet(name: name, type: type);
|
||||||
|
credentials = wownero.createWowneroRestoreWalletFromSeedCredentials(
|
||||||
|
name: name,
|
||||||
|
height: 465760,
|
||||||
|
mnemonic: testMnemonic25); // TODO catch failure
|
||||||
|
|
||||||
|
walletInfo = WalletInfo.external(
|
||||||
|
id: WalletBase.idFor(name, type),
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
isRecovery: false,
|
||||||
|
restoreHeight: credentials.height ?? 0,
|
||||||
|
date: DateTime.now(),
|
||||||
|
path: path,
|
||||||
|
address: "",
|
||||||
|
dirPath: dirPath);
|
||||||
|
credentials.walletInfo = walletInfo;
|
||||||
|
|
||||||
|
_walletCreationService = WalletCreationService(
|
||||||
|
secureStorage: storage,
|
||||||
|
sharedPreferences: prefs,
|
||||||
|
walletService: walletService,
|
||||||
|
keyService: keysStorage,
|
||||||
|
);
|
||||||
|
_walletCreationService.changeWalletType();
|
||||||
|
} catch (e, s) {
|
||||||
|
print(e);
|
||||||
|
print(s);
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wownero 25 word seed address generation", () async {
|
||||||
|
final wallet = await _walletCreationService.restoreFromSeed(credentials);
|
||||||
|
walletInfo.address = wallet.walletAddresses.address;
|
||||||
|
|
||||||
|
bool hasThrown = false;
|
||||||
|
try {
|
||||||
|
await _walletInfoSource.add(walletInfo);
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
|
||||||
|
expect(walletInfo.address, mainnetTestData25[0][0]);
|
||||||
|
} catch (_) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
expect(hasThrown, false);
|
||||||
|
|
||||||
|
walletBase?.close();
|
||||||
|
walletBase = wallet as WowneroWalletBase;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO delete left over wallet file with name: name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> pathForWalletDir(
|
||||||
|
{required String name, required WalletType type}) async {
|
||||||
|
Directory root = (await getApplicationDocumentsDirectory());
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
root = (await getLibraryDirectory());
|
||||||
|
}
|
||||||
|
final prefix = walletTypeToString(type).toLowerCase();
|
||||||
|
final walletsDir = Directory('${root.path}/wallets');
|
||||||
|
final walletDire = Directory('${walletsDir.path}/$prefix/$name');
|
||||||
|
|
||||||
|
if (!walletDire.existsSync()) {
|
||||||
|
walletDire.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletDire.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> pathForWallet(
|
||||||
|
{required String name, required WalletType type}) async =>
|
||||||
|
await pathForWalletDir(name: name, type: type)
|
||||||
|
.then((path) => path + '/$name');
|
22
test/services/coins/wownero/wownero_wallet_test_data.dart
Normal file
22
test/services/coins/wownero/wownero_wallet_test_data.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
String testMnemonic14 =
|
||||||
|
'weather cruise school such silly profit clerk wage reduce obtain ill sand episode shadow';
|
||||||
|
var mainnetTestData14 = [
|
||||||
|
[
|
||||||
|
'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi',
|
||||||
|
'WW3K54QzmMFB1uTZh3LVvgQYqANLmX1FkJHLJ4sU1E7BQmp8nGizyBnjNXSgsjCa4BQ3Rw3GG5jw1ByUkaUjSywm2KmHAbFvK',
|
||||||
|
'WW3e3F51KAojcSW2G5WimmE1WVFsbBHc6HppZFBa6dNiEn21cThXzdGGDbpv89aTKXSRSPSFaetK6HgCozYawaYz2knUi9Hmn'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'WW2nx7MFruyN2CcXnGnMbDdvqsyZUGQthLWKYPkQ4iM9XCE54RyWVjNjgopryUbyi9WKzYhHDai2wENbh1Jh1UHa28CL72TYt',
|
||||||
|
'WW34p57QBMoD6MEZVTu5u9R7G3KeYqvN4eYbvHLYsgbWXpLe992fBvVB7ANJNvaGmPg2uwY5oKjwKbpo4fDU6cGS231PmvXrZ',
|
||||||
|
'WW2KQLLt6gjC9gRsC4NGehbAZX6UPU7sK89UQFwSg3NKj3MXPwnjh5BiJVqYYNQb6JNsfa7oP7eDjLagtLa2H6YP11RhUNQqw'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
String testMnemonic25 =
|
||||||
|
'myth byline benches sadness nylon tamper guide giving match angled lurk rally makeup alarms river soapy dolphin woven ticket maul examine public luggage mammal alarms';
|
||||||
|
var mainnetTestData25 = [
|
||||||
|
[
|
||||||
|
'Wo3piMnt1ztjLktFJNsfs9ce6N1tyHk7DB93cNqTGPJ7To3RS7W2q5DdxgQAG5E6RQXQhchQD7ip8WWL3fD8Ww5K2XmAXYxta'
|
||||||
|
]
|
||||||
|
];
|
Loading…
Reference in a new issue