Merge pull request #219 from cypherstack/staging

Staging
This commit is contained in:
julian-CStack 2022-11-14 13:07:52 -06:00 committed by GitHub
commit e641238657
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 6448 additions and 2214 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View 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

View 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

View file

@ -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(

View file

@ -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,
), ),

View file

@ -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) {

View file

@ -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

View file

@ -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,6 +166,130 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
late final String address; late final String address;
late final Trade trade; late final Trade trade;
Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async {
final _amount = Format.decimalAmountToSatoshis(amount);
try {
bool wasCancelled = false;
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
return BuildingTransactionDialog(
onCancel: () {
wasCancelled = true;
Navigator.of(context).pop();
},
);
},
),
);
late Map<String, dynamic> txData;
// if not firo then do normal send
if (shouldSendPublicFiroFunds == null) {
txData = await manager.prepareSend(
address: address,
satoshiAmount: _amount,
args: {
"feeRate": FeeRateType.average,
// 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) {
// pop building dialog
if (mounted) {
Navigator.of(context).pop();
}
txData["note"] =
"${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange";
txData["address"] = address;
if (mounted) {
await Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmChangeNowSendView(
transactionInfo: txData,
walletId: walletId,
routeOnSuccessName: HomeView.routeName,
trade: trade,
shouldSendPublicFiroFunds: shouldSendPublicFiroFunds,
),
settings: const RouteSettings(
name: ConfirmChangeNowSendView.routeName,
),
),
);
}
}
} catch (e) {
// if (mounted) {
// pop building dialog
Navigator.of(context).pop();
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
);
// }
}
}
@override @override
void initState() { void initState() {
walletId = widget.walletId; walletId = widget.walletId;
@ -182,181 +310,278 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
final coin = manager.coin; final coin = manager.coin;
final isFiro = coin == Coin.firoTestNet || coin == Coin.firo;
return RoundedWhiteContainer( return RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: MaterialButton( child: ConditionalParent(
splashColor: Theme.of(context).extension<StackColors>()!.highlight, condition: isFiro,
key: Key("walletsSheetItemButtonKey_$walletId"), builder: (child) => Expandable(
padding: const EdgeInsets.all(8), header: Container(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, color: Colors.transparent,
shape: RoundedRectangleBorder( child: Padding(
borderRadius: BorderRadius.circular( padding: const EdgeInsets.all(12),
Constants.size.circularBorderRadius, 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,
),
],
), ),
), ),
onPressed: () async { child: ConditionalParent(
final _amount = Format.decimalAmountToSatoshis(amount); condition: !isFiro,
builder: (child) => MaterialButton(
try { splashColor: Theme.of(context).extension<StackColors>()!.highlight,
bool wasCancelled = false; key: Key("walletsSheetItemButtonKey_$walletId"),
padding: const EdgeInsets.all(8),
unawaited(showDialog<dynamic>( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
context: context, shape: RoundedRectangleBorder(
useSafeArea: false, borderRadius: BorderRadius.circular(
barrierDismissible: false, Constants.size.circularBorderRadius,
builder: (context) { ),
return BuildingTransactionDialog( ),
onCancel: () { onPressed: () => _send(manager),
wasCancelled = true; child: child,
),
Navigator.of(context).pop(); child: Row(
}, children: [
); Container(
}, decoration: BoxDecoration(
)); color: Theme.of(context)
.extension<StackColors>()!
final txData = await manager.prepareSend( .colorForCoin(manager.coin)
address: address, .withOpacity(0.5),
satoshiAmount: _amount, borderRadius: BorderRadius.circular(
args: { Constants.size.circularBorderRadius,
"feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider)
},
);
if (!wasCancelled) {
// pop building dialog
if (mounted) {
Navigator.of(context).pop();
}
txData["note"] =
"${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange";
txData["address"] = address;
if (mounted) {
await Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmChangeNowSendView(
transactionInfo: txData,
walletId: walletId,
routeOnSuccessName: HomeView.routeName,
trade: trade,
),
settings: const RouteSettings(
name: ConfirmChangeNowSendView.routeName,
),
), ),
); ),
} child: Padding(
} padding: const EdgeInsets.all(6),
} catch (e) { child: SvgPicture.asset(
// if (mounted) { Assets.svg.iconFor(coin: coin),
// pop building dialog width: 24,
Navigator.of(context).pop(); height: 24,
),
await showDialog<dynamic>( ),
context: context, ),
useSafeArea: false, const SizedBox(
barrierDismissible: true, width: 12,
builder: (context) { ),
return StackDialog( Expanded(
title: "Transaction failed", child: Column(
message: e.toString(), mainAxisAlignment: MainAxisAlignment.spaceBetween,
rightButton: TextButton( crossAxisAlignment: CrossAxisAlignment.start,
style: Theme.of(context) children: [
.extension<StackColors>()! Text(
.getSecondaryEnabledButtonColor(context), manager.walletName,
child: Text( style: STextStyles.titleBold12(context),
"Ok", ),
style: STextStyles.button(context).copyWith( if (!isFiro)
color: Theme.of(context) const SizedBox(
.extension<StackColors>()! height: 2,
.buttonTextSecondary,
), ),
), if (!isFiro)
onPressed: () { FutureBuilder(
Navigator.of(context).pop(); future: manager.totalBalance,
}, builder:
), (builderContext, AsyncSnapshot<Decimal> snapshot) {
); if (snapshot.connectionState ==
}, ConnectionState.done &&
); snapshot.hasData) {
// } return Text(
} "${Format.localizedStringAsFixed(
}, value: snapshot.data!,
child: Row( locale: locale,
children: [ decimalPlaces: coin == Coin.monero
Container( ? Constants.decimalPlacesMonero
decoration: BoxDecoration( : coin == Coin.wownero
color: Theme.of(context) ? Constants.decimalPlacesWownero
.extension<StackColors>()! : Constants.decimalPlaces,
.colorForCoin(manager.coin) )} ${coin.ticker}",
.withOpacity(0.5), style: STextStyles.itemSubtitle(context),
borderRadius: BorderRadius.circular( );
Constants.size.circularBorderRadius, } else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.itemSubtitle(context),
);
}
},
),
],
), ),
), ),
child: Padding( ],
padding: const EdgeInsets.all(6), ),
child: SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 24,
height: 24,
),
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
manager.walletName,
style: STextStyles.titleBold12(context),
),
const SizedBox(
height: 2,
),
FutureBuilder(
future: manager.totalBalance,
builder: (builderContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
return Text(
"${Format.localizedStringAsFixed(
value: snapshot.data!,
locale: locale,
decimalPlaces: coin == Coin.monero
? Constants.decimalPlacesMonero
: coin == Coin.wownero
? Constants.decimalPlacesWownero
: 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),
);
}
},
),
],
),
),
],
), ),
), ),
); );

View file

@ -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,25 +102,28 @@ 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 = uri.toString().replaceFirst("://", ":"); final uriString = AddressUtils.buildUriString(
widget.coin,
receivingAddress,
queryParams,
);
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();

View file

@ -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));

View file

@ -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);

View file

@ -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);

View file

@ -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,316 +96,469 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final isDesktop = Util.isDesktop;
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar( return ConditionalParent(
leading: AppBarBackButton( condition: !isDesktop,
onPressed: () async { builder: (child) {
if (FocusScope.of(context).hasFocus) { return Scaffold(
FocusScope.of(context).unfocus(); backgroundColor:
await Future<void>.delayed(const Duration(milliseconds: 75)); Theme.of(context).extension<StackColors>()!.background,
} appBar: AppBar(
if (mounted) { leading: AppBarBackButton(
Navigator.of(context).pop(); onPressed: () async {
} if (FocusScope.of(context).hasFocus) {
}, FocusScope.of(context).unfocus();
), await Future<void>.delayed(const Duration(milliseconds: 75));
title: Text( }
"Create backup", if (mounted) {
style: STextStyles.navBarTitle(context), Navigator.of(context).pop();
), }
), },
body: Padding( ),
padding: const EdgeInsets.all(16), title: Text(
child: LayoutBuilder( "Create backup",
builder: (context, constraints) { style: STextStyles.navBarTitle(context),
return SingleChildScrollView( ),
child: ConstrainedBox( ),
constraints: BoxConstraints( body: Padding(
minHeight: constraints.maxHeight, padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
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: IntrinsicHeight( ),
child: Column( child,
crossAxisAlignment: CrossAxisAlignment.stretch, ],
children: [ );
if (!Platform.isAndroid) },
Consumer(builder: (context, ref, __) { child: Column(
return Container( crossAxisAlignment: CrossAxisAlignment.stretch,
color: Colors.transparent, children: [
child: TextField( if (!Platform.isAndroid)
autocorrect: Util.isDesktop ? false : true, Consumer(builder: (context, ref, __) {
enableSuggestions: Util.isDesktop ? false : true, return Container(
onTap: Platform.isAndroid color: Colors.transparent,
? null child: TextField(
: () async { autocorrect: Util.isDesktop ? false : true,
try { enableSuggestions: Util.isDesktop ? false : true,
await stackFileSystem.prepareStorage(); onTap: Platform.isAndroid
? null
: () async {
try {
await stackFileSystem.prepareStorage();
if (mounted) { if (mounted) {
await stackFileSystem await stackFileSystem.pickDir(context);
.pickDir(context); }
}
if (mounted) { if (mounted) {
setState(() { setState(() {
fileLocationController.text = fileLocationController.text =
stackFileSystem.dirPath ?? ""; stackFileSystem.dirPath ?? "";
}); });
} }
} 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,
style: STextStyles.field(context), style: STextStyles.field(context),
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Save to...", hintText: "Save to...",
hintStyle: STextStyles.fieldLabel(context), hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox( suffixIcon: UnconstrainedBox(
child: Row( child: Row(
children: [ children: [
const SizedBox( const SizedBox(
width: 16, width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
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) {
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
},
), ),
); SvgPicture.asset(
}), Assets.svg.folder,
if (!Platform.isAndroid) color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
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) {
// ref.read(addressEntryDataProvider(widget.id)).address = newValue;
},
),
);
}),
if (!Platform.isAndroid)
SizedBox(
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(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox( const SizedBox(
height: 8, width: 16,
), ),
ClipRRect( GestureDetector(
borderRadius: BorderRadius.circular( key: const Key(
Constants.size.circularBorderRadius, "createBackupPasswordFieldShowPasswordButtonKey"),
), onTap: () async {
child: TextField(
key: const Key("createBackupPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
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(() { setState(() {
passwordFeedback = feedback; hidePassword = !hidePassword;
}); });
}, },
), child: SvgPicture.asset(
), hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
if (passwordFocusNode.hasFocus || color: Theme.of(context)
passwordRepeatFocusNode.hasFocus ||
passwordController.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 (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: MediaQuery.of(context).size.width - 32 - 24,
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>()! .extension<StackColors>()!
.buttonBackSecondary, .textDark3,
percent: passwordStrength < 0.25 width: 16,
? 0.03 height: 16,
: passwordStrength,
), ),
), ),
const SizedBox( const SizedBox(
height: 10, width: 12,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
), ),
child: TextField( ],
key: const Key("createBackupPasswordFieldKey2"), ),
focusNode: passwordRepeatFocusNode, ),
controller: passwordRepeatController, ),
style: STextStyles.field(context), onChanged: (newValue) {
obscureText: hidePassword, if (newValue.isEmpty) {
enableSuggestions: false, setState(() {
autocorrect: false, passwordFeedback = "";
decoration: standardInputDecoration( });
"Confirm passphrase", return;
passwordRepeatFocusNode, }
context, final result = zxcvbn.evaluate(newValue);
).copyWith( String suggestionsAndTips = "";
suffixIcon: UnconstrainedBox( for (var sug in result.feedback.suggestions!.toSet()) {
child: Row( suggestionsAndTips += "$sug\n";
children: [ }
const SizedBox( suggestionsAndTips += result.feedback.warning!;
width: 16, String feedback =
), // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
GestureDetector( suggestionsAndTips;
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"), passwordStrength = result.score! / 4;
onTap: () async {
setState(() { // hack fix to format back string returned from zxcvbn
hidePassword = !hidePassword; if (feedback.contains("phrasesNo need")) {
}); feedback = feedback.replaceFirst(
}, "phrasesNo need", "phrases\nNo need");
child: SvgPicture.asset( }
hidePassword
? Assets.svg.eye if (feedback.endsWith("\n")) {
: Assets.svg.eyeSlash, feedback = feedback.substring(0, feedback.length - 2);
color: Theme.of(context) }
.extension<StackColors>()!
.textDark3, setState(() {
width: 16, passwordFeedback = feedback;
height: 16, });
), },
), ),
const SizedBox( ),
width: 12, if (passwordFocusNode.hasFocus ||
), passwordRepeatFocusNode.hasFocus ||
], passwordController.text.isNotEmpty)
), Padding(
), padding: EdgeInsets.only(
), left: 12,
onChanged: (newValue) { right: 12,
setState(() {}); top: passwordFeedback.isNotEmpty ? 4 : 0,
// TODO: ? check if passwords match? ),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(context),
)
: null,
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
key: const Key("createStackBackUpProgressBar"),
width: MediaQuery.of(context).size.width - 32 - 24,
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(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passwordRepeatFocusNode,
controller: passwordRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm passphrase",
passwordRepeatFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
}, },
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
), ),
), const SizedBox(
const SizedBox( width: 12,
height: 16, ),
), ],
const Spacer(), ),
TextButton( ),
style: shouldEnableCreate ),
? Theme.of(context) onChanged: (newValue) {
.extension<StackColors>()! setState(() {});
.getPrimaryEnabledButtonColor(context) // TODO: ? check if passwords match?
: Theme.of(context) },
.extension<StackColors>()! ),
.getPrimaryDisabledButtonColor(context), ),
const SizedBox(
height: 16,
),
if (!isDesktop) const Spacer(),
!isDesktop
? TextButton(
style: shouldEnableCreate
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context)
: Theme.of(context)
.extension<StackColors>()!
.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", const SizedBox(
style: STextStyles.button(context), width: 16,
), ),
SecondaryButton(
width: 183,
desktopMed: true,
label: "Cancel",
onPressed: () {},
), ),
], ],
), ),
), ],
),
);
},
), ),
), ),
); );

View file

@ -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,190 +82,243 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final isDesktop = Util.isDesktop;
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar( return ConditionalParent(
leading: AppBarBackButton( condition: !isDesktop,
onPressed: () async { builder: (child) {
if (FocusScope.of(context).hasFocus) { return Scaffold(
FocusScope.of(context).unfocus(); backgroundColor:
await Future<void>.delayed(const Duration(milliseconds: 75)); Theme.of(context).extension<StackColors>()!.background,
} appBar: AppBar(
if (mounted) { leading: AppBarBackButton(
Navigator.of(context).pop(); onPressed: () async {
} if (FocusScope.of(context).hasFocus) {
}, FocusScope.of(context).unfocus();
), await Future<void>.delayed(
title: Text( const Duration(milliseconds: 75));
"Restore from file", }
style: STextStyles.navBarTitle(context), if (mounted) {
), Navigator.of(context).pop();
), }
body: Padding( },
padding: const EdgeInsets.all(16), ),
child: LayoutBuilder( title: Text(
builder: (context, constraints) { "Restore from file",
return SingleChildScrollView( style: STextStyles.navBarTitle(context),
child: ConstrainedBox( ),
constraints: BoxConstraints( ),
minHeight: constraints.maxHeight, body: Padding(
padding: const EdgeInsets.all(16),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
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: IntrinsicHeight( child,
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.stretch, );
children: [ },
TextField( child: Column(
autocorrect: Util.isDesktop ? false : true, crossAxisAlignment: CrossAxisAlignment.stretch,
enableSuggestions: Util.isDesktop ? false : true, children: [
onTap: () async { TextField(
try { autocorrect: Util.isDesktop ? false : true,
await stackFileSystem.prepareStorage(); enableSuggestions: Util.isDesktop ? false : true,
if (mounted) { onTap: () async {
await stackFileSystem.openFile(context); try {
} await stackFileSystem.prepareStorage();
if (mounted) {
await stackFileSystem.openFile(context);
}
if (mounted) { if (mounted) {
setState(() {
fileLocationController.text =
stackFileSystem.filePath ?? "";
});
}
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
}
},
controller: fileLocationController,
style: STextStyles.field(context),
decoration: InputDecoration(
hintText: "Choose file...",
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
),
),
),
key: const Key("restoreFromFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
),
SizedBox(
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(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("restoreFromFilePasswordFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"restoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() { setState(() {
fileLocationController.text = hidePassword = !hidePassword;
stackFileSystem.filePath ?? "";
}); });
} },
} catch (e, s) { child: SvgPicture.asset(
Logging.instance hidePassword
.log("$e\n$s", level: LogLevel.Error); ? Assets.svg.eye
} : Assets.svg.eyeSlash,
}, color: Theme.of(context)
controller: fileLocationController, .extension<StackColors>()!
style: STextStyles.field(context), .textDark3,
decoration: InputDecoration( width: 16,
hintText: "Choose file...", height: 16,
hintStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
const SizedBox(
width: 12,
),
],
), ),
), ),
), const SizedBox(
key: const Key("restoreFromFileLocationTextFieldKey"), width: 12,
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("restoreFromFilePasswordFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
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: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
), ),
onChanged: (newValue) { ],
setState(() {});
},
),
), ),
const SizedBox( ),
height: 16, ),
), onChanged: (newValue) {
const Spacer(), setState(() {});
TextButton( },
style: passwordController.text.isEmpty || ),
fileLocationController.text.isEmpty ),
? Theme.of(context) const SizedBox(
.extension<StackColors>()! height: 16,
.getPrimaryDisabledButtonColor(context) ),
: Theme.of(context) if (!isDesktop) const Spacer(),
.extension<StackColors>()! !isDesktop
.getPrimaryEnabledButtonColor(context), ? TextButton(
onPressed: passwordController.text.isEmpty || style: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty fileLocationController.text.isEmpty
? null ? Theme.of(context)
: () async { .extension<StackColors>()!
final String fileToRestore = .getPrimaryDisabledButtonColor(context)
fileLocationController.text; : Theme.of(context)
final String passphrase = .extension<StackColors>()!
passwordController.text; .getPrimaryEnabledButtonColor(context),
onPressed: passwordController.text.isEmpty ||
fileLocationController.text.isEmpty
? null
: () async {
final String fileToRestore =
fileLocationController.text;
final String passphrase = passwordController.text;
if (FocusScope.of(context).hasFocus) { if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
await Future<void>.delayed( await Future<void>.delayed(
const Duration(milliseconds: 75)); const Duration(milliseconds: 75));
} }
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,
); );
return; return;
} }
bool shouldPop = false; bool shouldPop = false;
unawaited(
showDialog<dynamic>( showDialog<dynamic>(
barrierDismissible: false, barrierDismissible: false,
context: context, context: context,
@ -288,52 +358,233 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
], ],
), ),
), ),
),
);
final String? jsonString = await compute(
SWB.decryptStackWalletWithPassphrase,
Tuple2(fileToRestore, passphrase),
debugLabel: "stack wallet decryption compute",
);
if (mounted) {
// pop LoadingIndicator
shouldPop = true;
Navigator.of(context).pop();
passwordController.text = "";
if (jsonString == null) {
await showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to decrypt backup file",
context: context,
);
return;
}
await Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (_) => StackRestoreProgressView(
jsonString: jsonString,
),
),
); );
}
},
child: Text(
"Restore",
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;
final String? jsonString = await compute( if (FocusScope.of(context).hasFocus) {
SWB.decryptStackWalletWithPassphrase, FocusScope.of(context).unfocus();
Tuple2(fileToRestore, passphrase), await Future<void>.delayed(
debugLabel: "stack wallet decryption compute", const Duration(milliseconds: 75));
); }
if (mounted) { if (!(await File(fileToRestore).exists())) {
// pop LoadingIndicator await showFloatingFlushBar(
shouldPop = true;
Navigator.of(context).pop();
passwordController.text = "";
if (jsonString == null) {
showFloatingFlushBar(
type: FlushBarType.warning, type: FlushBarType.warning,
message: "Failed to decrypt backup file", message: "Backup file does not exist",
context: context, context: context,
); );
return; return;
} }
Navigator.of(context).push( bool shouldPop = false;
RouteGenerator.getRoute( unawaited(
builder: (_) => StackRestoreProgressView( showDialog<dynamic>(
jsonString: jsonString, 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(
child: Text( SWB.decryptStackWalletWithPassphrase,
"Restore", Tuple2(fileToRestore, passphrase),
style: STextStyles.button(context), 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,
),
),
],
),
),
),
);
},
),
);
});
}
},
), ),
), const SizedBox(
], width: 16,
), ),
), SecondaryButton(
), width: 183,
); desktopMed: true,
}, label: "Cancel",
), onPressed: () {},
), ),
); ],
),
],
),
));
} }
} }

View file

@ -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
? RestoreFromEncryptedStringView.routeName ? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile
: StackBackupView.routeName)); ? RestoreFromEncryptedStringView.routeName
: StackBackupView.routeName))
: Navigator.of(context).popUntil((_) => count++ >= 2);
} }
} }
@ -179,281 +187,289 @@ class _StackRestoreProgressViewState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( bool isDesktop = Util.isDesktop;
onWillPop: _onWillPop,
child: Scaffold( return ConditionalParent(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, condition: !isDesktop,
appBar: AppBar( builder: (child) {
leading: AppBarBackButton( return WillPopScope(
onPressed: () async { onWillPop: _onWillPop,
if (FocusScope.of(context).hasFocus) { child: Scaffold(
FocusScope.of(context).unfocus(); backgroundColor:
await Future<void>.delayed(const Duration(milliseconds: 75)); Theme.of(context).extension<StackColors>()!.background,
} appBar: AppBar(
if (_success) { leading: AppBarBackButton(
_addWalletsToHomeView(); onPressed: () async {
if (mounted) { if (FocusScope.of(context).hasFocus) {
Navigator.of(context).pop(); FocusScope.of(context).unfocus();
} await Future<void>.delayed(
} else { const Duration(milliseconds: 75));
if (await _requestCancel()) { }
await _cancel(); if (_success) {
} _addWalletsToHomeView();
} if (mounted) {
}, Navigator.of(context).pop();
), }
title: Text( } else {
"Restoring Stack wallet", if (await _requestCancel()) {
style: STextStyles.navBarTitle(context), await _cancel();
), }
), }
body: Padding( },
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: 4,
top: 4,
right: 4,
bottom: 0,
), ),
child: Column( title: Text(
crossAxisAlignment: CrossAxisAlignment.start, "Restoring Stack wallet",
children: [ style: STextStyles.navBarTitle(context),
Text( ),
"Settings", ),
style: STextStyles.itemSubtitle(context), body: Padding(
), padding: const EdgeInsets.only(
const SizedBox( left: 12,
height: 12, top: 12,
), right: 12,
Consumer( ),
builder: (_, ref, __) { child: child,
final state = ref.watch(stackRestoringUIStateProvider ),
.select((value) => value.preferences)); ),
return RestoringItemCard( );
left: SizedBox( },
width: 32, child: SingleChildScrollView(
height: 32, child: Padding(
child: RoundedContainer( padding: const EdgeInsets.only(
padding: const EdgeInsets.all(0), left: 4,
top: 4,
right: 4,
bottom: 0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Settings",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.preferences));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: SvgPicture.asset(
Assets.svg.gear,
width: 16,
height: 16,
color: Theme.of(context) color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.buttonBackSecondary, .accentColorDark,
child: Center(
child: SvgPicture.asset(
Assets.svg.gear,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Preferences",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.addressBook));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: AddressBookIcon(
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Address book",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.nodes));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: SvgPicture.asset(
Assets.svg.node,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Nodes",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.trades));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: SvgPicture.asset(
Assets.svg.arrowRotate2,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Exchange history",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 16,
),
Text(
"Wallets",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
height: 8,
),
...ref
.watch(stackRestoringUIStateProvider
.select((value) => value.walletStateProviders))
.values
.map(
(provider) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RestoringWalletCard(
provider: provider,
), ),
), ),
), ),
const SizedBox( ),
height: 80, right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Preferences",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.addressBook));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: AddressBookIcon(
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Address book",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.nodes));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: SvgPicture.asset(
Assets.svg.node,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Nodes",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
final state = ref.watch(stackRestoringUIStateProvider
.select((value) => value.trades));
return RestoringItemCard(
left: SizedBox(
width: 32,
height: 32,
child: RoundedContainer(
padding: const EdgeInsets.all(0),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
child: Center(
child: SvgPicture.asset(
Assets.svg.arrowRotate2,
width: 16,
height: 16,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
right: SizedBox(
width: 20,
height: 20,
child: _getIconForState(state),
),
title: "Exchange history",
subTitle: state == StackRestoringStatus.failed
? Text(
"Something went wrong",
style: STextStyles.errorSmall(context),
)
: null,
);
},
),
const SizedBox(
height: 16,
),
Text(
"Wallets",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
height: 8,
),
...ref
.watch(stackRestoringUIStateProvider
.select((value) => value.walletStateProviders))
.values
.map(
(provider) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RestoringWalletCard(
provider: provider,
),
),
), ),
], const SizedBox(
height: 30,
), ),
), SizedBox(
), width: MediaQuery.of(context).size.width - 32,
), child: TextButton(
floatingActionButton: SizedBox( onPressed: () async {
width: MediaQuery.of(context).size.width - 32, if (_success) {
child: TextButton( Navigator.of(context).pop();
onPressed: () async { } else {
if (_success) { if (await _requestCancel()) {
_addWalletsToHomeView(); await _cancel();
Navigator.of(context) }
.popUntil(ModalRoute.withName(HomeView.routeName)); }
} else { },
if (await _requestCancel()) { style: Theme.of(context)
await _cancel(); .extension<StackColors>()!
} .getPrimaryEnabledButtonColor(context),
} child: Text(
}, _success ? "OK" : "Cancel restore process",
style: Theme.of(context) style: STextStyles.button(context).copyWith(
.extension<StackColors>()! color: Theme.of(context)
.getPrimaryEnabledButtonColor(context), .extension<StackColors>()!
child: Text( .buttonTextPrimary,
_success ? "OK" : "Cancel restore process", ),
style: STextStyles.button(context).copyWith( ),
color: Theme.of(context) ),
.extension<StackColors>()!
.buttonTextPrimary,
), ),
), ],
), ),
), ),
), ),

View file

@ -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,269 +21,363 @@ 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 Scaffold( return ConditionalParent(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, condition: !isDesktop,
appBar: AppBar( builder: (child) {
leading: AppBarBackButton( return Scaffold(
onPressed: () { backgroundColor:
Navigator.of(context).pop(); Theme.of(context).extension<StackColors>()!.background,
}, appBar: AppBar(
), leading: AppBarBackButton(
title: Text( onPressed: () {
"Support", Navigator.of(context).pop();
style: STextStyles.navBarTitle(context), },
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"If you need support or want to report a bug, reach out to us on any of our socials!",
style: STextStyles.smallMed12(context),
),
), ),
const SizedBox( title: Text(
height: 12, "Support",
style: STextStyles.navBarTitle(context),
), ),
RoundedWhiteContainer( ),
padding: const EdgeInsets.all(0), body: Padding(
child: RawMaterialButton( padding: const EdgeInsets.all(16),
// splashColor: Theme.of(context).extension<StackColors>()!.highlight, child: child,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ),
shape: RoundedRectangleBorder( );
borderRadius: BorderRadius.circular( },
Constants.size.circularBorderRadius, child: Column(
), crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"If you need support or want to report a bug, reach out to us on any of our socials!",
style: STextStyles.smallMed12(context),
),
),
isDesktop
? const SizedBox(
height: 24,
)
: const SizedBox(
height: 12,
), ),
onPressed: () { RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
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( },
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 12,
), vertical: 20,
child: Row( ),
children: [ child: Row(
SvgPicture.asset( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Assets.socials.telegram, children: [
width: iconSize, Row(
height: iconSize, children: [
color: Theme.of(context) SvgPicture.asset(
.extension<StackColors>()! Assets.socials.telegram,
.accentColorDark, width: iconSize,
), height: iconSize,
const SizedBox( color: Theme.of(context)
width: 12, .extension<StackColors>()!
), .accentColorDark,
Text( ),
"Telegram", const SizedBox(
style: STextStyles.titleBold12(context), width: 12,
textAlign: TextAlign.left, ),
), Text(
], "Telegram",
), style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
],
),
BlueTextButton(
text: isDesktop ? "@stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://t.me/stackwallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
), ),
), ),
), ),
const SizedBox( ),
height: 8, const SizedBox(
), height: 8,
RoundedWhiteContainer( ),
padding: const EdgeInsets.all(0), RoundedWhiteContainer(
child: RawMaterialButton( padding: const EdgeInsets.all(0),
// splashColor: Theme.of(context).extension<StackColors>()!.highlight, child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular( shape: RoundedRectangleBorder(
Constants.size.circularBorderRadius, borderRadius: BorderRadius.circular(
), Constants.size.circularBorderRadius,
), ),
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( },
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 12,
), vertical: 20,
child: Row( ),
children: [ child: Row(
SvgPicture.asset( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Assets.socials.discord, children: [
width: iconSize, Row(
height: iconSize, children: [
color: Theme.of(context) SvgPicture.asset(
.extension<StackColors>()! Assets.socials.discord,
.accentColorDark, width: iconSize,
), height: iconSize,
const SizedBox( color: Theme.of(context)
width: 12, .extension<StackColors>()!
), .accentColorDark,
Text( ),
"Discord", const SizedBox(
style: STextStyles.titleBold12(context), width: 12,
textAlign: TextAlign.left, ),
), Text(
], "Discord",
), style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
],
),
BlueTextButton(
text: isDesktop ? "Stack Wallet" : "",
onTap: () {
launchUrl(
Uri.parse(
"https://discord.gg/RZMG3yUm"), //expired link?
mode: LaunchMode.externalApplication,
);
},
),
],
), ),
), ),
), ),
const SizedBox( ),
height: 8, const SizedBox(
), height: 8,
RoundedWhiteContainer( ),
padding: const EdgeInsets.all(0), RoundedWhiteContainer(
child: RawMaterialButton( padding: const EdgeInsets.all(0),
// splashColor: Theme.of(context).extension<StackColors>()!.highlight, child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular( shape: RoundedRectangleBorder(
Constants.size.circularBorderRadius, borderRadius: BorderRadius.circular(
), Constants.size.circularBorderRadius,
), ),
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( },
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 12,
), vertical: 20,
child: Row( ),
children: [ child: Row(
SvgPicture.asset( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Assets.socials.reddit, children: [
width: iconSize, Row(
height: iconSize, children: [
color: Theme.of(context) SvgPicture.asset(
.extension<StackColors>()! Assets.socials.reddit,
.accentColorDark, width: iconSize,
), height: iconSize,
const SizedBox( color: Theme.of(context)
width: 12, .extension<StackColors>()!
), .accentColorDark,
Text( ),
"Reddit", const SizedBox(
style: STextStyles.titleBold12(context), width: 12,
textAlign: TextAlign.left, ),
), Text(
], "Reddit",
), style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
],
),
BlueTextButton(
text: isDesktop ? "r/stackwallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://www.reddit.com/r/stackwallet/"),
mode: LaunchMode.externalApplication,
);
},
),
],
), ),
), ),
), ),
const SizedBox( ),
height: 8, const SizedBox(
), height: 8,
RoundedWhiteContainer( ),
padding: const EdgeInsets.all(0), RoundedWhiteContainer(
child: RawMaterialButton( padding: const EdgeInsets.all(0),
// splashColor: Theme.of(context).extension<StackColors>()!.highlight, child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular( shape: RoundedRectangleBorder(
Constants.size.circularBorderRadius, borderRadius: BorderRadius.circular(
), Constants.size.circularBorderRadius,
), ),
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( },
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 12,
), vertical: 20,
child: Row( ),
children: [ child: Row(
SvgPicture.asset( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Assets.socials.twitter, children: [
width: iconSize, Row(
height: iconSize, children: [
color: Theme.of(context) SvgPicture.asset(
.extension<StackColors>()! Assets.socials.twitter,
.accentColorDark, width: iconSize,
), height: iconSize,
const SizedBox( color: Theme.of(context)
width: 12, .extension<StackColors>()!
), .accentColorDark,
Text( ),
"Twitter", const SizedBox(
style: STextStyles.titleBold12(context), width: 12,
textAlign: TextAlign.left, ),
), Text(
], "Twitter",
), style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
],
),
BlueTextButton(
text: isDesktop ? "@stack_wallet" : "",
onTap: () {
launchUrl(
Uri.parse("https://twitter.com/stack_wallet"),
mode: LaunchMode.externalApplication,
);
},
),
],
), ),
), ),
), ),
const SizedBox( ),
height: 8, const SizedBox(
), height: 8,
RoundedWhiteContainer( ),
padding: const EdgeInsets.all(0), RoundedWhiteContainer(
child: RawMaterialButton( padding: const EdgeInsets.all(0),
// splashColor: Theme.of(context).extension<StackColors>()!.highlight, child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // splashColor: Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
borderRadius: BorderRadius.circular( shape: RoundedRectangleBorder(
Constants.size.circularBorderRadius, borderRadius: BorderRadius.circular(
), Constants.size.circularBorderRadius,
), ),
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( },
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 12, padding: const EdgeInsets.symmetric(
vertical: 20, horizontal: 12,
), vertical: 20,
child: Row( ),
children: [ child: Row(
SvgPicture.asset( mainAxisAlignment: MainAxisAlignment.spaceBetween,
Assets.svg.envelope, children: [
width: iconSize, Row(
height: iconSize, children: [
color: Theme.of(context) SvgPicture.asset(
.extension<StackColors>()! Assets.svg.envelope,
.accentColorDark, width: iconSize,
), height: iconSize,
const SizedBox( color: Theme.of(context)
width: 12, .extension<StackColors>()!
), .accentColorDark,
Text( ),
"Email", const SizedBox(
style: STextStyles.titleBold12(context), width: 12,
textAlign: TextAlign.left, ),
), Text(
], "Email",
), style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
],
),
BlueTextButton(
text: isDesktop ? "support@stackwallet.com" : "",
onTap: () {
launchUrl(
Uri.parse("mailto://support@stackwallet.com"),
mode: LaunchMode.externalApplication,
);
},
),
],
), ),
), ),
), ),
], ),
), ],
), ),
); );
} }

View file

@ -471,75 +471,80 @@ class _TransactionDetailsViewState
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( Expanded(
crossAxisAlignment: child: Column(
CrossAxisAlignment.start, crossAxisAlignment:
children: [ CrossAxisAlignment.start,
Text( children: [
Text(
_transaction.txType.toLowerCase() ==
"sent"
? "Sent to"
: "Receiving address",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context)
: STextStyles.itemSubtitle(
context),
),
const SizedBox(
height: 8,
),
_transaction.txType.toLowerCase() == _transaction.txType.toLowerCase() ==
"sent" "received"
? "Sent to" ? FutureBuilder(
: "Receiving address", future: fetchContactNameFor(
style: isDesktop _transaction.address),
? STextStyles builder: (builderContext,
.desktopTextExtraExtraSmall( AsyncSnapshot<String>
context) snapshot) {
: STextStyles.itemSubtitle(context), String addressOrContactName =
), _transaction.address;
const SizedBox( if (snapshot.connectionState ==
height: 8, ConnectionState
), .done &&
_transaction.txType.toLowerCase() == snapshot.hasData) {
"received" addressOrContactName =
? FutureBuilder( snapshot.data!;
future: fetchContactNameFor( }
_transaction.address), return SelectableText(
builder: (builderContext, addressOrContactName,
AsyncSnapshot<String> style: isDesktop
snapshot) { ? STextStyles
String addressOrContactName = .desktopTextExtraExtraSmall(
_transaction.address; context)
if (snapshot.connectionState == .copyWith(
ConnectionState.done && color: Theme.of(
snapshot.hasData) { context)
addressOrContactName = .extension<
snapshot.data!; StackColors>()!
} .textDark,
return SelectableText( )
addressOrContactName, : STextStyles
style: isDesktop .itemSubtitle12(
? STextStyles context),
.desktopTextExtraExtraSmall( );
context) },
.copyWith( )
color: Theme.of( : SelectableText(
context) _transaction.address,
.extension< style: isDesktop
StackColors>()! ? STextStyles
.textDark, .desktopTextExtraExtraSmall(
) context)
: STextStyles .copyWith(
.itemSubtitle12( color: Theme.of(context)
context), .extension<
); StackColors>()!
}, .textDark,
) )
: SelectableText( : STextStyles
_transaction.address, .itemSubtitle12(
style: isDesktop context),
? STextStyles ),
.desktopTextExtraExtraSmall( ],
context) ),
.copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(
context),
),
],
), ),
if (isDesktop) if (isDesktop)
IconCopyButton( IconCopyButton(

View file

@ -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)

View 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,
);
},
),
],
),
),
],
),
);
}
}

View 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,
),
],
),
),
],
),
);
}
}

View file

@ -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(),
// ),
],
);
}
}

View file

@ -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,
), ),
]; ];

View file

@ -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(

View file

@ -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,160 +165,226 @@ 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( hoverColor: Colors.transparent,
elevation: 0, padding: const EdgeInsets.all(0),
hoverColor: Colors.transparent, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius * 2, Constants.size.circularBorderRadius,
),
), ),
onPressed: () {}, //onPressed ),
child: Stack( onPressed: () {
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)
),
child: SvgPicture.asset(
Assets.svg.themeLight,
),
),
Padding(
padding: const EdgeInsets.only(
left: 50,
top: 12,
),
child: Text(
"Light",
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.textDark, .infoItemIcons
), : Theme.of(context).extension<StackColors>()!.popupBG,
), ),
) borderRadius: BorderRadius.circular(
], Constants.size.circularBorderRadius,
), ),
// if (externalCallsEnabled) ),
Positioned(
bottom: 0,
left: 6,
child: SvgPicture.asset( child: SvgPicture.asset(
Assets.svg.checkCircle, Assets.svg.themeLight,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
), ),
), ),
// if (!externalCallsEnabled) const SizedBox(
// Positioned( height: 12,
// top: 4, ),
// right: 4, Row(
// child: Container( children: [
// width: 20, SizedBox(
// height: 20, width: 20,
// decoration: BoxDecoration( height: 20,
// borderRadius: BorderRadius.circular(1000), child: Radio(
// color: Theme.of(context) activeColor: Theme.of(context)
// .extension<StackColors>()! .extension<StackColors>()!
// .textFieldDefaultBG, .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",
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
],
),
], ],
), ),
), ),
), ),
const SizedBox( const SizedBox(
width: 1, width: 20,
), ),
Expanded( MaterialButton(
child: Padding( splashColor: Colors.transparent,
padding: const EdgeInsets.all(8.0), hoverColor: Colors.transparent,
child: RawMaterialButton( padding: const EdgeInsets.all(0),
elevation: 0, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
hoverColor: Colors.transparent, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius,
Constants.size.circularBorderRadius * 2, ),
), ),
), onPressed: () {
onPressed: () {}, //onPressed DB.instance.put<dynamic>(
child: Stack( boxName: DB.boxNameTheme,
children: [ key: "colorScheme",
Column( value: ThemeType.dark.name,
crossAxisAlignment: CrossAxisAlignment.start, );
children: [ ref.read(colorThemeProvider.state).state =
SvgPicture.asset( StackColors.fromStackColorTheme(
Assets.svg.themeDark, DarkColors(),
), );
Padding(
padding: const EdgeInsets.only( setState(() {
left: 50, _selectedTheme = "dark";
top: 12, });
), },
child: Text( child: SizedBox(
"Dark", width: 200,
style: STextStyles.desktopTextExtraSmall(context) child: Column(
.copyWith( mainAxisSize: MainAxisSize.min,
color: Theme.of(context) children: [
.extension<StackColors>()! Container(
.textDark, decoration: BoxDecoration(
), border: Border.all(
), width: 2.5,
), color: _selectedTheme == "dark"
], ? Theme.of(context)
), .extension<StackColors>()!
// if (externalCallsEnabled) .infoItemIcons
Positioned( : Theme.of(context).extension<StackColors>()!.popupBG,
bottom: 0, ),
left: 0, borderRadius: BorderRadius.circular(
child: SvgPicture.asset( Constants.size.circularBorderRadius,
Assets.svg.checkCircle,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
), ),
), ),
// if (!externalCallsEnabled) child: SvgPicture.asset(
// Positioned( Assets.svg.themeDark,
// top: 4, ),
// right: 4, ),
// child: Container( const SizedBox(
// width: 20, height: 12,
// height: 20, ),
// decoration: BoxDecoration( Row(
// borderRadius: BorderRadius.circular(1000), children: [
// color: Theme.of(context) SizedBox(
// .extension<StackColors>()! width: 20,
// .textFieldDefaultBG, 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",
style:
STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
],
),
],
), ),
), ),
), ),

View file

@ -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,290 +35,516 @@ class BackupRestoreSettings extends ConsumerStatefulWidget {
} }
class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
late bool createBackup = false;
late bool restoreBackup = false;
final toggleController = DSBController();
late final TextEditingController fileLocationController;
late final TextEditingController passwordController;
late final TextEditingController frequencyController;
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>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const EnableBackupDialog();
},
);
}
Future<void> createAutoBackup() async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return CreateAutoBackup();
},
);
}
Future<void> attemptDisable() async {
final result = await showDialog<bool?>(
context: context,
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)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
child: Text(
"Disable",
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
return ListView(
shrinkWrap: true, bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider
scrollDirection: Axis.vertical, .select((value) => value.isAutoBackupEnabled));
children: [
Padding( ref.listen(
padding: const EdgeInsets.only( prefsChangeNotifierProvider
right: 30, .select((value) => value.backupFrequencyType),
), (previous, BackupFrequencyType next) {
child: RoundedWhiteContainer( frequencyController.text = Format.prettyFrequencyType(next);
child: Column( });
crossAxisAlignment: CrossAxisAlignment.start,
children: [ return LayoutBuilder(builder: (context, constraints) {
SvgPicture.asset( return SingleChildScrollView(
Assets.svg.backupAuto, scrollDirection: Axis.vertical,
width: 48, child: ConstrainedBox(
height: 48, constraints: BoxConstraints(
), minHeight: constraints.maxHeight,
Center( ),
child: Padding( child: IntrinsicHeight(
padding: const EdgeInsets.all(10), child: Column(
child: RichText( children: [
textAlign: TextAlign.start, Padding(
text: TextSpan( padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
TextSpan( Padding(
text: "Auto Backup", padding: const EdgeInsets.all(8.0),
style: STextStyles.desktopTextSmall(context), 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,
),
],
),
), ),
TextSpan( Center(
text: child: Row(
"\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." children: [
"To ensure maximum security, we recommend using a unique password that you haven't used anywhere " Expanded(
"else on the internet before. Your password is not stored.", child: Padding(
style: padding: const EdgeInsets.all(10),
STextStyles.desktopTextExtraExtraSmall(context), 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,
);
},
),
],
),
),
),
),
],
),
), ),
TextSpan( Column(
text: crossAxisAlignment: CrossAxisAlignment.start,
"\n\nFor more information, please see our website ", children: [
style: Padding(
STextStyles.desktopTextExtraExtraSmall(context), padding: const EdgeInsets.all(10),
), child: !isEnabledAutoBackup
TextSpan( ? PrimaryButton(
text: "stackwallet.com", desktopMed: true,
style: STextStyles.richLink(context) width: 200,
.copyWith(fontSize: 14), label: "Enable auto backup",
recognizer: TapGestureRecognizer() onPressed: () {
..onTap = () { enableAutoBackup(context);
launchUrl( },
Uri.parse("https://stackwallet.com/"), )
mode: LaunchMode.externalApplication, : 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(
Column( height: 25,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: const [ Padding(
Padding( padding: const EdgeInsets.only(
padding: EdgeInsets.all( right: 30,
10,
),
child: AutoBackupButton(),
), ),
], child: RoundedWhiteContainer(
), child: Column(
], crossAxisAlignment: CrossAxisAlignment.start,
),
),
),
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: [ children: [
TextSpan( Padding(
text: "Manual Backup", padding: const EdgeInsets.all(8.0),
style: STextStyles.desktopTextSmall(context), child: SvgPicture.asset(
Assets.svg.backupAdd,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
), ),
TextSpan( Center(
text: child: Row(
"\n\nCreate manual backup to easily transfer your data between devices. " children: [
"You will create a backup file that can be later used in the Restore option. " Expanded(
"Use a strong password to encrypt your data.", child: Padding(
style: padding: const EdgeInsets.all(10),
STextStyles.desktopTextExtraExtraSmall(context), 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(
Column( height: 25,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: const [ Padding(
Padding( padding: const EdgeInsets.only(
padding: EdgeInsets.all( right: 30,
10, bottom: 40,
),
child: ManualBackupButton(),
), ),
], child: RoundedWhiteContainer(
), child: Column(
], crossAxisAlignment: CrossAxisAlignment.start,
),
),
),
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: [ children: [
TextSpan( Padding(
text: "Restore Backup", padding: const EdgeInsets.all(8.0),
style: STextStyles.desktopTextSmall(context), child: SvgPicture.asset(
Assets.svg.backupRestore,
width: 48,
height: 48,
alignment: Alignment.topLeft,
),
), ),
TextSpan( Center(
text: child: Row(
"\n\nUse your Stack Wallet backup file to restore your wallets, address book " children: [
"and wallet preferences.", Expanded(
style: child: Padding(
STextStyles.desktopTextExtraExtraSmall(context), 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;
});
},
),
),
],
), ),
], ],
), ),
), ),
), ),
), ],
Column( ),
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: EdgeInsets.all(
10,
),
child: RestoreBackupButton(),
),
],
),
],
), ),
), ));
), });
],
);
}
}
class AutoBackupButton extends ConsumerWidget {
const AutoBackupButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> enableAutoBackup() async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const EnableBackupDialog();
},
);
}
return SizedBox(
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>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const RestoreBackupDialog();
},
);
}
return SizedBox(
width: 200,
height: 48,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {
restoreBackup();
},
child: Text(
"Restore",
style: STextStyles.button(context),
),
),
);
} }
} }

View file

@ -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,198 +185,287 @@ 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,
height: 32, enableSuggestions: false,
width: 32, onTap: Platform.isAndroid
child: Center( ? null
child: SvgPicture.asset( : () async {
Assets.svg.folder, try {
color: Theme.of(context) await stackFileSystem.prepareStorage();
.extension<StackColors>()!
.textDark3, if (mounted) {
width: 20, await stackFileSystem.pickDir(context);
height: 17.5, }
),
), if (mounted) {
), setState(() {
), fileLocationController.text =
), stackFileSystem.dirPath ?? "";
), });
), }
const SizedBox( } catch (e, s) {
height: 24, Logging.instance
), .log("$e\n$s", level: LogLevel.Error);
Container( }
alignment: Alignment.centerLeft, },
padding: const EdgeInsets.only(left: 32), controller: fileLocationController,
child: Text( style: STextStyles.field(context),
"Create a passphrase", decoration: InputDecoration(
style: STextStyles.desktopTextExtraSmall(context).copyWith( hintText: "Save to...",
color: Theme.of(context).extension<StackColors>()!.textDark3, hintStyle: STextStyles.fieldLabel(context),
), suffixIcon: UnconstrainedBox(
textAlign: TextAlign.left, child: Row(
), children: [
), const SizedBox(
const SizedBox( width: 16,
height: 10, ),
), SvgPicture.asset(
Padding( Assets.svg.folder,
padding: const EdgeInsets.only( color: Theme.of(context)
left: 32, .extension<StackColors>()!
right: 32, .textDark3,
), width: 16,
child: ClipRRect( height: 16,
borderRadius: BorderRadius.circular( ),
Constants.size.circularBorderRadius, const SizedBox(
), width: 12,
child: TextField( ),
key: const Key("createBackupPassphrase"), ],
focusNode: passphraseFocusNode, ),
controller: passphraseController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passphraseFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
suffixIcon: UnconstrainedBox(
child: GestureDetector(
key: const Key(
"createDesktopAutoBackupShowPassphraseButton1"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 20,
height: 17.5,
), ),
), ),
key: const Key(
"createBackupSaveToFileLocationTextFieldKey"),
readOnly: true,
toolbarOptions: const ToolbarOptions(
copy: true,
cut: false,
paste: false,
selectAll: false,
),
onChanged: (newValue) {},
), ),
);
}),
if (!Platform.isAndroid)
const SizedBox(
height: 24,
),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
"Create a passphrase",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
), ),
), ),
), ClipRRect(
), borderRadius: BorderRadius.circular(
), Constants.size.circularBorderRadius,
),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPassphrase"),
focusNode: passphraseRepeatFocusNode,
controller: passphraseRepeatController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm passphrase",
passphraseRepeatFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
), ),
suffixIcon: UnconstrainedBox( child: TextField(
child: GestureDetector( key: const Key("createBackupPasswordFieldKey1"),
key: const Key( focusNode: passphraseFocusNode,
"createDesktopAutoBackupShowPassphraseButton2"), controller: passphraseController,
onTap: () async { style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Create passphrase",
passphraseFocusNode,
context,
).copyWith(
labelStyle:
isDesktop ? STextStyles.fieldLabel(context) : null,
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() { setState(() {
hidePassword = !hidePassword; passwordFeedback = "";
}); });
}, return;
child: Container( }
decoration: BoxDecoration( final result = zxcvbn.evaluate(newValue);
color: Colors.transparent, String suggestionsAndTips = "";
borderRadius: BorderRadius.circular(1000), for (var sug in result.feedback.suggestions!.toSet()) {
), suggestionsAndTips += "$sug\n";
height: 32, }
width: 32, suggestionsAndTips += result.feedback.warning!;
child: Center( String feedback =
child: SvgPicture.asset( // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, suggestionsAndTips;
color: Theme.of(context)
.extension<StackColors>()! passwordStrength = result.score! / 4;
.textDark3,
width: 20, // hack fix to format back string returned from zxcvbn
height: 17.5, 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: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
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(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("createBackupPasswordFieldKey2"),
focusNode: passphraseRepeatFocusNode,
controller: passphraseRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm passphrase",
passphraseRepeatFocusNode,
context,
).copyWith(
labelStyle: STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"createBackupPasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
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
isExpanded: true, ? DropdownButtonHideUnderline(
elevation: 0, child: DropdownButton2(
style: STextStyles.desktopTextExtraSmall(context).copyWith( offset: Offset(0, -10),
color: Theme.of(context).extension<StackColors>()!.textDark, isExpanded: true,
), dropdownElevation: 0,
icon: SvgPicture.asset( value: _currentDropDownValue,
Assets.svg.chevronDown, items: [
width: 10, ..._dropDownItems.map(
height: 5, (e) {
color: Theme.of(context).extension<StackColors>()!.textDark3, String message = "";
), switch (e) {
dropdownColor: case BackupFrequencyType.everyTenMinutes:
Theme.of(context).extension<StackColors>()!.textFieldActiveBG, message = "Every 10 minutes";
// focusColor: , break;
value: _currentDropDownValue, case BackupFrequencyType.everyAppStart:
items: _dropDownItems message = "Every app startup";
.map( break;
(e) => DropdownMenuItem( case BackupFrequencyType.afterClosingAWallet:
value: e, message =
child: Text(e), "After closing a cryptocurrency wallet";
break;
}
return DropdownMenuItem(
value: e,
child: Text(message),
);
},
),
],
onChanged: (value) {
if (value is BackupFrequencyType) {
if (ref
.read(prefsChangeNotifierProvider)
.backupFrequencyType !=
value) {
ref
.read(prefsChangeNotifierProvider)
.backupFrequencyType = value;
}
setState(() {
_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,
),
),
), ),
) )
.toList(), : null,
onChanged: (value) {
if (value is String) {
setState(() {
_currentDropDownValue = value;
});
}
},
),
), ),
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"),
);
}
}
},
), ),
) )
], ],

View file

@ -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);
}, },
), ),
), ),

View file

@ -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();
},
),
)
],
),
),
],
),
);
}
}

View file

@ -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,
);
},
),
],
)
],
)
],
);
},
)
],
),
)
],
),
),
),
),
],
),
),
],
),
);
}
}

View file

@ -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(),
),
],
),
),
],
),
);
}
}

View file

@ -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());

View file

@ -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(

View file

@ -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;

View file

@ -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;
} }

View file

@ -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,11 +298,14 @@ class BitcoinCashWallet extends CoinServiceAPI {
} catch (err) { } catch (err) {
// Bech32 decode fail // Bech32 decode fail
} }
if (_network.bech32 != decodeBech32!.hrp) {
throw ArgumentError('Invalid prefix or Network mismatch'); if (decodeBech32 != null) {
} if (_network.bech32 != decodeBech32.hrp) {
if (decodeBech32.version != 0) { throw ArgumentError('Invalid prefix or Network mismatch');
throw ArgumentError('Invalid address version'); }
if (decodeBech32.version != 0) {
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);
print("Counts $counts"); if (kDebugMode) {
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,31 +756,35 @@ 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") {
NotificationApi.showNotification( unawaited(
title: "Incoming transaction", NotificationApi.showNotification(
body: walletName, title: "Incoming transaction",
walletId: walletId, body: walletName,
iconAssetName: Assets.svg.iconFor(coin: coin), walletId: walletId,
date: DateTime.now(), iconAssetName: Assets.svg.iconFor(coin: coin),
shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, date: DateTime.now(),
coinName: coin.name, shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
txid: tx.txid, coinName: coin.name,
confirmations: tx.confirmations, txid: tx.txid,
requiredConfirmations: MINIMUM_CONFIRMATIONS, confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
),
); );
await txTracker.addNotifiedPending(tx.txid); await txTracker.addNotifiedPending(tx.txid);
} else if (tx.txType == "Sent") { } else if (tx.txType == "Sent") {
NotificationApi.showNotification( unawaited(
title: "Sending transaction", NotificationApi.showNotification(
body: walletName, title: "Sending transaction",
walletId: walletId, body: walletName,
iconAssetName: Assets.svg.iconFor(coin: coin), walletId: walletId,
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), iconAssetName: Assets.svg.iconFor(coin: coin),
shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
coinName: coin.name, shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
txid: tx.txid, coinName: coin.name,
confirmations: tx.confirmations, txid: tx.txid,
requiredConfirmations: MINIMUM_CONFIRMATIONS, confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
),
); );
await txTracker.addNotifiedPending(tx.txid); await txTracker.addNotifiedPending(tx.txid);
} }
@ -778,26 +793,30 @@ 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") {
NotificationApi.showNotification( unawaited(
title: "Incoming transaction confirmed", NotificationApi.showNotification(
body: walletName, title: "Incoming transaction confirmed",
walletId: walletId, body: walletName,
iconAssetName: Assets.svg.iconFor(coin: coin), walletId: walletId,
date: DateTime.now(), iconAssetName: Assets.svg.iconFor(coin: coin),
shouldWatchForUpdates: false, date: DateTime.now(),
coinName: coin.name, shouldWatchForUpdates: false,
coinName: coin.name,
),
); );
await txTracker.addNotifiedConfirmed(tx.txid); await txTracker.addNotifiedConfirmed(tx.txid);
} else if (tx.txType == "Sent") { } else if (tx.txType == "Sent") {
NotificationApi.showNotification( unawaited(
title: "Outgoing transaction confirmed", NotificationApi.showNotification(
body: walletName, title: "Outgoing transaction confirmed",
walletId: walletId, body: walletName,
iconAssetName: Assets.svg.iconFor(coin: coin), walletId: walletId,
date: DateTime.now(), iconAssetName: Assets.svg.iconFor(coin: coin),
shouldWatchForUpdates: false, date: DateTime.now(),
coinName: coin.name, shouldWatchForUpdates: false,
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);
print("format $format"); if (kDebugMode) {
return true; print("format $format");
} catch (e, s) { }
if (_coin == Coin.bitcoincashTestnet) {
return true;
}
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;
} }
print("Array key is ${jsonEncode(arrayKey)}"); if (kDebugMode) {
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);
print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); if (kDebugMode) {
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 = {};
print("Address $addresses"); if (kDebugMode) {
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)];
} }
print("Args ${jsonEncode(args)}"); if (kDebugMode) {
print("Args ${jsonEncode(args)}");
}
final response = await electrumXClient.getBatchHistory(args: args); final response = await electrumXClient.getBatchHistory(args: args);
print("Response ${jsonEncode(response)}"); if (kDebugMode) {
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;
} }
print("result ${jsonEncode(result)}"); if (kDebugMode) {
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);
print("CACHED_TRANSACTIONS_IS $cachedTransactions"); if (kDebugMode) {
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(unconfirmedCachedTransactions[tx["tx_hash"] as String]); print(
cachedTransactions.findTransaction(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;
} }
} }

View file

@ -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);
} }

View file

@ -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;
} }
} }

View file

@ -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();

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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) {

View file

@ -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;
} }
} }

View file

@ -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;
} }

View file

@ -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());
height = getSeedHeightSync(mnemonic.trim()); // extract seed height from 14 word seed
if (seedLength == 14) {
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;
} }
} }

View file

@ -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";

View file

@ -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;

View 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;
}
}

View file

@ -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:

View file

@ -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( return true;
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); if (badCertResponse == null) {
badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false);
} else {
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? // TODO: json decoded without error so assume connection exists?
// or we can check for certain values in the response to decide // or we can check for certain values in the response to decide
return true; return MoneroNodeConnectionResponse(null, null, null, true);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning); if (badCertResponse != null) {
return false; 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;
}

View file

@ -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:

View file

@ -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 = () {

View file

@ -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);

View file

@ -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);

View file

@ -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,10 +181,16 @@ class StackOkDialog extends StatelessWidget {
), ),
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () { onPressed: !Util.isDesktop
Navigator.of(context).pop(); ? () {
onOkPressed?.call("OK"); Navigator.of(context).pop();
}, 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>()!
.getPrimaryEnabledButtonColor(context), .getPrimaryEnabledButtonColor(context),

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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);

View file

@ -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();
}
} }

View 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');

View 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'
]
];

View 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');

View 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'
]
];