diff --git a/assets/svg/enabled-button.svg b/assets/svg/enabled-button.svg new file mode 100644 index 000000000..a26359e81 --- /dev/null +++ b/assets/svg/enabled-button.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/framed-address-book.svg b/assets/svg/framed-address-book.svg new file mode 100644 index 000000000..157117097 --- /dev/null +++ b/assets/svg/framed-address-book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/svg/framed-gear.svg b/assets/svg/framed-gear.svg new file mode 100644 index 000000000..749d9803d --- /dev/null +++ b/assets/svg/framed-gear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 51f74f05d..2da774385 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42 +Subproject commit 2da77438527732dfaa5398aa391eab5253dabe19 diff --git a/lib/main.dart b/lib/main.dart index 58a287b31..77a8b1441 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/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_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/base_currencies_provider.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; @@ -207,6 +208,7 @@ class _MaterialAppWithThemeState extends ConsumerState late final Completer loadingCompleter; bool didLoad = false; + bool _desktopHasPassword = false; Future load() async { try { @@ -218,6 +220,11 @@ class _MaterialAppWithThemeState extends ConsumerState await DB.instance.init(); await _prefs.init(); + if (Util.isDesktop) { + _desktopHasPassword = + await ref.read(storageCryptoHandlerProvider).hasPassword(); + } + _notificationsService = ref.read(notificationsProvider); _nodeService = ref.read(nodeServiceChangeNotifierProvider); _tradesService = ref.read(tradesServiceProvider); @@ -545,21 +552,23 @@ class _MaterialAppWithThemeState extends ConsumerState builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { // FlutterNativeSplash.remove(); - if (_wallets.hasWallets || _prefs.hasPin) { - // return HomeView(); - + if (Util.isDesktop && + (_wallets.hasWallets || _desktopHasPassword)) { String? startupWalletId; if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { startupWalletId = ref.read(prefsChangeNotifierProvider).startupWalletId; } - // TODO proper desktop auth view - if (Util.isDesktop) { - Future.delayed(Duration.zero).then((value) => - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, (route) => false)); - return Container(); + return DesktopLoginView(startupWalletId: startupWalletId); + } else if (!Util.isDesktop && + (_wallets.hasWallets || _prefs.hasPin)) { + // return HomeView(); + + String? startupWalletId; + if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; } return LockscreenView( diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 76e74fa14..1ce5d713a 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState { SizedBox( 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( "Choose start date", style: isDesktop @@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState { : STextStyles.smallMed12(context), 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( 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) RestoreFromDatePicker( @@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState { // if (isDesktop) // // 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( 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( child: Center( child: Text( @@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState { ), ), ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 24 : 16, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index def0724b5..6bac38371 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:bip39/src/wordlists/english.dart' as bip39wordlist; import 'package:flutter/material.dart'; import 'package:flutter/services.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_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -149,12 +150,18 @@ class _RestoreWalletViewState extends ConsumerState { super.dispose(); } + // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { var moneroWordList = monero.getMoneroWordList("English"); 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); } @@ -180,7 +187,13 @@ class _RestoreWalletViewState extends ConsumerState { if (widget.coin == Coin.monero) { 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 if (widget.coin == Coin.epicCash) { diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index d77ad6b8c..e99cf2df4 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -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/providers.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/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { required this.walletId, this.routeOnSuccessName = WalletView.routeName, required this.trade, + this.shouldSendPublicFiroFunds, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String walletId; final String routeOnSuccessName; final Trade trade; + final bool? shouldSendPublicFiroFunds; @override ConsumerState createState() => @@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState ref.read(walletsChangeNotifierProvider).getManager(walletId); 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()); // save note diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 20fc81903..c87175955 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -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/providers/providers.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/constants.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/theme/stack_colors.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/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -162,6 +166,130 @@ class _SendFromCardState extends ConsumerState { late final String address; late final Trade trade; + Future _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { + final _amount = Format.decimalAmountToSatoshis(amount); + + try { + bool wasCancelled = false; + + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + + late Map 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( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + // } + } + } + @override void initState() { walletId = widget.walletId; @@ -182,181 +310,278 @@ class _SendFromCardState extends ConsumerState { final coin = manager.coin; + final isFiro = coin == Coin.firoTestNet || coin == Coin.firo; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: MaterialButton( - splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: ConditionalParent( + condition: isFiro, + builder: (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: child, + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MaterialButton( + splashColor: + Theme.of(context).extension()!.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 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()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension()!.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 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()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + ], ), ), - onPressed: () async { - final _amount = Format.decimalAmountToSatoshis(amount); - - try { - bool wasCancelled = false; - - unawaited(showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = await manager.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, - ), - settings: const RouteSettings( - name: ConfirmChangeNowSendView.routeName, - ), + child: ConditionalParent( + condition: !isFiro, + builder: (child) => MaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send(manager), + child: child, + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + ), + 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), + ), + if (!isFiro) + const SizedBox( + height: 2, ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } - } - }, - child: Row( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (!isFiro) + FutureBuilder( + future: manager.totalBalance, + builder: + (builderContext, AsyncSnapshot 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), + ); + } + }, + ), + ], ), ), - 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 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), - ); - } - }, - ), - ], - ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 4c3c4c968..ae615bd96 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.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/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -101,25 +102,28 @@ class _GenerateUriQrCodeViewState extends State { return null; } - String query = ""; + Map queryParams = {}; if (amountString.isNotEmpty) { - query += "amount=$amountString"; + queryParams["amount"] = amountString; } if (noteString.isNotEmpty) { - if (query.isNotEmpty) { - query += "&"; - } - query += "message=$noteString"; + queryParams["message"] = noteString; } - final uri = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - query: query.isNotEmpty ? query : null, - ); + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + 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", level: LogLevel.Info); @@ -229,10 +233,21 @@ class _GenerateUriQrCodeViewState extends State { @override void initState() { isDesktop = Util.isDesktop; - _uriString = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - ).toString().replaceFirst("://", ":"); + + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + receivingAddress.contains(":")) { + // remove cash addr prefix + receivingAddress = receivingAddress.split(":").sublist(1).join(); + } + + _uriString = AddressUtils.buildUriString( + widget.coin, + receivingAddress, + {}, + ); + amountController = TextEditingController(); noteController = TextEditingController(); super.initState(); diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 81d5a3da2..26d1231f0 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -87,13 +87,13 @@ class _ConfirmTransactionViewState txid = await manager.confirmSend(txData: transactionInfo); } - unawaited(manager.refresh()); - // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); + unawaited(manager.refresh()); + // pop back to wallet if (mounted) { Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 87aee413e..382e3f09e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState { 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) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5e666ce2..f9b64c460 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState { 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) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 9242c0482..eacdda66a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -15,7 +15,10 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.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/desktop/primary_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'; @@ -93,316 +96,469 @@ class _RestoreFromFileViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Create backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Create backup", + style: STextStyles.navBarTitle(context), + ), + ), + 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), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark3), ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!Platform.isAndroid) - Consumer(builder: (context, ref, __) { - return Container( - color: Colors.transparent, - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); + ), + child, + ], + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( + color: Colors.transparent, + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem - .pickDir(context); - } + if (mounted) { + await stackFileSystem.pickDir(context); + } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.log("$e\n$s", - level: LogLevel.Error); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Save to...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension()! - .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 (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, ), - ); - }), - if (!Platform.isAndroid) + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension()! + .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()! + .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( - height: 8, + width: 16, ), - 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( - 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()! - .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); - } - + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { setState(() { - passwordFeedback = feedback; + hidePassword = !hidePassword; }); }, - ), - ), - if (passwordFocusNode.hasFocus || - 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()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) .extension()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + .textDark3, + width: 16, + height: 16, ), ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + width: 12, ), - 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( - 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()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - // TODO: ? check if passwords match? + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passwordFocusNode.hasFocus || + 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()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension()! + .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()! + .textDark3, + width: 16, + height: 16, + ), ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonColor(context), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, + ), + ), + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + !isDesktop + ? TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension()! + .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( + 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.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( + 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( + 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 ? null : () async { @@ -502,17 +658,19 @@ class _RestoreFromFileViewState extends State { } } }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + desktopMed: true, + label: "Cancel", + onPressed: () {}, ), ], ), - ), - ), - ); - }, + ], ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 232be9028..ee1fcf666 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; 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/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_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.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/text_styles.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/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/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - class RestoreFromFileView extends ConsumerStatefulWidget { const RestoreFromFileView({Key? key}) : super(key: key); @@ -42,6 +48,17 @@ class _RestoreFromFileViewState extends ConsumerState { bool hidePassword = true; + Future restoreBackupPopup(BuildContext context) async { + // await showDialog( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return const RestoreBackupDialog(); + // }, + // ); + } + @override void initState() { stackFileSystem = StackFileSystem(); @@ -65,190 +82,243 @@ class _RestoreFromFileViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Restore from file", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Restore from file", + style: STextStyles.navBarTitle(context), + ), + ), + 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()! + .textDark3), + textAlign: TextAlign.left, + ), ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: () async { - try { - await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem.openFile(context); - } + child, + ], + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: () async { + 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()! + .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()! + .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(() { - fileLocationController.text = - stackFileSystem.filePath ?? ""; + hidePassword = !hidePassword; }); - } - } 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()! - .textDark3, - width: 16, - height: 16, - ), - const SizedBox( - width: 12, - ), - ], + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, ), ), - ), - key: const Key("restoreFromFileLocationTextFieldKey"), - 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()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), + const SizedBox( + width: 12, ), - onChanged: (newValue) { - setState(() {}); - }, - ), + ], ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) - .extension()! - .getPrimaryDisabledButtonColor(context) - : Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { - final String fileToRestore = - fileLocationController.text; - final String passphrase = - passwordController.text; + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + !isDesktop + ? TextButton( + style: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) + .extension()! + .getPrimaryDisabledButtonColor(context) + : Theme.of(context) + .extension()! + .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) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } - if (!(await File(fileToRestore).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Backup file does not exist", - context: context, - ); - return; - } + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - bool shouldPop = false; + bool shouldPop = false; + unawaited( showDialog( barrierDismissible: false, context: context, @@ -288,52 +358,233 @@ class _RestoreFromFileViewState extends ConsumerState { ], ), ), + ), + ); + + 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( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), - debugLabel: "stack wallet decryption compute", - ); + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); - - passwordController.text = ""; - - if (jsonString == null) { - showFloatingFlushBar( + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( type: FlushBarType.warning, - message: "Failed to decrypt backup file", + message: "Backup file does not exist", context: context, ); return; } - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, + bool shouldPop = false; + unawaited( + showDialog( + 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, + ), + ), + ], + ), ), ), ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), + + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: + "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } + + await showDialog( + 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: () {}, + ), + ], + ), + ], + ), + )); } } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart index 5e5142425..7dec4e740 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/helpers/restore_create_backup.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/text_styles.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/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -39,6 +40,8 @@ class StackRestoreProgressView extends ConsumerStatefulWidget { class _StackRestoreProgressViewState extends ConsumerState { + bool isDesktop = Util.isDesktop; + Future _cancel() async { bool shouldPop = false; unawaited(showDialog( @@ -79,10 +82,15 @@ class _StackRestoreProgressViewState await SWB.cancelRestore(); shouldPop = true; + + int count = 0; + if (mounted) { - Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile - ? RestoreFromEncryptedStringView.routeName - : StackBackupView.routeName)); + !isDesktop + ? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile + ? RestoreFromEncryptedStringView.routeName + : StackBackupView.routeName)) + : Navigator.of(context).popUntil((_) => count++ >= 2); } } @@ -179,281 +187,289 @@ class _StackRestoreProgressViewState @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (_success) { - _addWalletsToHomeView(); - if (mounted) { - Navigator.of(context).pop(); - } - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - ), - title: Text( - "Restoring Stack wallet", - style: STextStyles.navBarTitle(context), - ), - ), - 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, + bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return WillPopScope( + onWillPop: _onWillPop, + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + if (_success) { + _addWalletsToHomeView(); + if (mounted) { + Navigator.of(context).pop(); + } + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, ), - 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), + title: Text( + "Restoring Stack wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, + ), + ), + ); + }, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + 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()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, color: Theme.of(context) .extension()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.gear, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .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()! - .buttonBackSecondary, - child: Center( - child: AddressBookIcon( - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .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()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .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()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .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, + .accentColorDark, ), ), ), - 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()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension()! + .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()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension()! + .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()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension()! + .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, ), - ), - ), - ), - floatingActionButton: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - onPressed: () async { - if (_success) { - _addWalletsToHomeView(); - Navigator.of(context) - .popUntil(ModalRoute.withName(HomeView.routeName)); - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - child: Text( - _success ? "OK" : "Cancel restore process", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + onPressed: () async { + if (_success) { + Navigator.of(context).pop(); + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor(context), + child: Text( + _success ? "OK" : "Cancel restore process", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), ), - ), + ], ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index fdfa6f404..20aeedf61 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -4,7 +4,10 @@ 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/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/blue_text_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -18,269 +21,363 @@ class SupportView extends StatelessWidget { @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Support", - 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), - ), + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - const SizedBox( - height: 12, + title: Text( + "Support", + style: STextStyles.navBarTitle(context), ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ); + }, + 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()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://t.me/stackwallet"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.telegram, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Telegram", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.telegram, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + 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, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://discord.gg/RZMG3yUm"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.discord, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Discord", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.discord, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + 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, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://www.reddit.com/r/stackwallet/"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.reddit, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Reddit", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.reddit, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + 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, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://twitter.com/stack_wallet"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.twitter, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Twitter", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.twitter, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + 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, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("mailto://support@stackwallet.com"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.envelope, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Email", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.envelope, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + 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, + ); + }, + ), + ], ), ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 6f23f2e01..1c2fb8e5d 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -471,75 +471,80 @@ class _TransactionDetailsViewState MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + 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() == - "sent" - ? "Sent to" - : "Receiving address", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - _transaction.txType.toLowerCase() == - "received" - ? FutureBuilder( - future: fetchContactNameFor( - _transaction.address), - builder: (builderContext, - AsyncSnapshot - snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot + snapshot) { + String addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ), + ], + ), ), if (isDesktop) IconCopyButton( diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 2391a22f6..0a8429058 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -1,10 +1,12 @@ import 'dart:async'; 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:stackwallet/notifications/show_flush_bar.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/constants.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:zxcvbn/zxcvbn.dart'; -class CreatePasswordView extends StatefulWidget { +class CreatePasswordView extends ConsumerStatefulWidget { const CreatePasswordView({ Key? key, this.secureStore = const SecureStorageWrapper( @@ -31,10 +33,10 @@ class CreatePasswordView extends StatefulWidget { final FlutterSecureStorageInterface secureStore; @override - State createState() => _CreatePasswordViewState(); + ConsumerState createState() => _CreatePasswordViewState(); } -class _CreatePasswordViewState extends State { +class _CreatePasswordViewState extends ConsumerState { late final TextEditingController passwordController; late final TextEditingController passwordRepeatController; @@ -76,8 +78,16 @@ class _CreatePasswordViewState extends State { return; } - await widget.secureStore - .write(key: "stackDesktopPassword", value: passphrase); + try { + await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); + } catch (e) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Error: $e", + context: context, + )); + return; + } if (mounted) { unawaited(Navigator.of(context) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart new file mode 100644 index 000000000..fe05d719f --- /dev/null +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -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 createState() => _DesktopLoginViewState(); +} + +class _DesktopLoginViewState extends State { + 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()! + .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, + ); + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/forgot_password_desktop_view.dart b/lib/pages_desktop_specific/forgot_password_desktop_view.dart new file mode 100644 index 000000000..d501cbd38 --- /dev/null +++ b/lib/pages_desktop_specific/forgot_password_desktop_view.dart @@ -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 createState() => + _ForgotPasswordDesktopViewState(); +} + +class _ForgotPasswordDesktopViewState extends State { + @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()! + .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, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart new file mode 100644 index 000000000..3622fcf1e --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -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 createState() => _DesktopAddressBook(); +} + +class _DesktopAddressBook extends ConsumerState { + 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(), + // ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 6aa104081..cb8aba255 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.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_settings_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/utilities/theme/stack_colors.dart'; @@ -29,19 +32,25 @@ class _DesktopHomeViewState extends ConsumerState { Container( color: Colors.red, ), - Container( - color: Colors.orange, + const Navigator( + key: Key("desktopAddressBookHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAddressBook.routeName, ), const Navigator( key: Key("desktopSettingHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSettingsView.routeName, ), - Container( - color: Colors.blue, + const Navigator( + key: Key("desktopSupportHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopSupportView.routeName, ), - Container( - color: Colors.pink, + const Navigator( + key: Key("desktopAboutHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAboutView.routeName, ), ]; diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart index 0406a059f..cf687e3e7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.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/utilities/assets.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/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -105,7 +109,7 @@ class _DebugInfoDialog extends ConsumerState { ], ), Expanded( - flex: 24, + // flex: 24, child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { @@ -314,7 +318,7 @@ class _DebugInfoDialog extends ConsumerState { ), ), ), - const Spacer(), + // const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( @@ -322,7 +326,18 @@ class _DebugInfoDialog extends ConsumerState { Expanded( child: SecondaryButton( 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( diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index d524453e2..bf6fc81e2 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -1,16 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/constants.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/widgets/custom_buttons/draggable_switch_button.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 { const AppearanceOptionSettings({Key? key}) : super(key: key); @@ -23,6 +26,19 @@ class AppearanceOptionSettings extends ConsumerStatefulWidget { class _AppearanceOptionSettings extends ConsumerState { + // late bool isLight; + + // @override + // void initState() { + // + // super.initState(); + // } + // + // @override + // void dispose() { + // super.dispose(); + // } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -128,10 +144,7 @@ class _AppearanceOptionSettings ), ), const Padding( - padding: EdgeInsets.only( - left: 10, - right: 10, - ), + padding: EdgeInsets.all(10), child: ThemeToggle(), ), ], @@ -143,7 +156,7 @@ class _AppearanceOptionSettings } } -class ThemeToggle extends StatefulWidget { +class ThemeToggle extends ConsumerStatefulWidget { const ThemeToggle({ Key? key, }) : super(key: key); @@ -152,160 +165,226 @@ class ThemeToggle extends StatefulWidget { // final void Function(bool)? onChanged; @override - State createState() => _ThemeToggle(); + ConsumerState createState() => _ThemeToggle(); } -class _ThemeToggle extends State { +class _ThemeToggle extends ConsumerState { // late bool externalCallsEnabled; + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + @override Widget build(BuildContext context) { return Row( - // mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () {}, //onPressed - child: Stack( + ), + onPressed: () { + DB.instance.put( + 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: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 24, - ), - 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) + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "light" + ? Theme.of(context) .extension()! - .textDark, - ), - ), - ) - ], - ), - // if (externalCallsEnabled) - Positioned( - bottom: 0, - left: 6, + .infoItemIcons + : Theme.of(context).extension()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), child: SvgPicture.asset( - Assets.svg.checkCircle, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .infoItemIcons, + Assets.svg.themeLight, ), ), - // if (!externalCallsEnabled) - // Positioned( - // top: 4, - // right: 4, - // child: Container( - // width: 20, - // height: 20, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(1000), - // color: Theme.of(context) - // .extension()! - // .textFieldDefaultBG, - // ), - // ), - // ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put( + 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()! + .textDark, + ), + ), + ], + ), ], ), ), ), const SizedBox( - width: 1, + width: 20, ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.themeDark, - ), - Padding( - padding: const EdgeInsets.only( - left: 50, - top: 12, - ), - child: Text( - "Dark", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ), - ], - ), - // if (externalCallsEnabled) - Positioned( - bottom: 0, - left: 0, - child: SvgPicture.asset( - Assets.svg.checkCircle, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .infoItemIcons, + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "dark" + ? Theme.of(context) + .extension()! + .infoItemIcons + : Theme.of(context).extension()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - // if (!externalCallsEnabled) - // Positioned( - // top: 4, - // right: 4, - // child: Container( - // width: 20, - // height: 20, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(1000), - // color: Theme.of(context) - // .extension()! - // .textFieldDefaultBG, - // ), - // ), - // ), - ], - ), + child: SvgPicture.asset( + Assets.svg.themeDark, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put( + 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()! + .textDark, + ), + ), + ], + ), + ], ), ), ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 8b74f8d40..3ea6cea6c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -2,14 +2,28 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/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/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.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/stack_dialog.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 { const BackupRestoreSettings({Key? key}) : super(key: key); @@ -21,290 +35,516 @@ class BackupRestoreSettings extends ConsumerStatefulWidget { } class _BackupRestoreSettings extends ConsumerState { + 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 enableAutoBackup(BuildContext context) async { + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const EnableBackupDialog(); + }, + ); + } + + Future createAutoBackup() async { + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return CreateAutoBackup(); + }, + ); + } + + Future attemptDisable() async { + final result = await showDialog( + 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()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context).extension()!.accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .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 Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - width: 48, - height: 48, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( + + bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + .select((value) => value.isAutoBackupEnabled)); + + ref.listen( + prefsChangeNotifierProvider + .select((value) => value.backupFrequencyType), + (previous, BackupFrequencyType next) { + frequencyController.text = Format.prettyFrequencyType(next); + }); + + return LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Auto Backup", - style: STextStyles.desktopTextSmall(context), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + ), + isEnabledAutoBackup + ? SvgPicture.asset( + Assets.svg.enableButton, + ) + : SvgPicture.asset( + Assets.svg.disableButton, + ), + ], + ), ), - 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), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ), + ), + ), + ), + ], + ), ), - TextSpan( - text: - "\n\nFor more information, please see our website ", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: "stackwallet.com", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com/"), - mode: LaunchMode.externalApplication, - ); - }, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: !isEnabledAutoBackup + ? PrimaryButton( + desktopMed: true, + width: 200, + label: "Enable auto backup", + onPressed: () { + enableAutoBackup(context); + }, + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: 403, + color: Theme.of(context) + .extension()! + .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(); + }, + ), + ], + ) + ], + ), + ), + ], ), ], ), ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: AutoBackupButton(), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, ), - ], - ), - ], - ), - ), - ), - 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( + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Manual Backup", - style: STextStyles.desktopTextSmall(context), + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), ), - 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), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: createBackup + ? const SizedBox( + width: 512, + child: CreateBackupView(), + ) + : PrimaryButton( + desktopMed: true, + width: 200, + label: "Create manual backup", + onPressed: () { + setState(() { + createBackup = true; + }); + }, + ), + ), + ], ), ], ), ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: ManualBackupButton(), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + bottom: 40, ), - ], - ), - ], - ), - ), - ), - 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( + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Restore Backup", - style: STextStyles.desktopTextSmall(context), + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), ), - TextSpan( - text: - "\n\nUse your Stack Wallet backup file to restore your wallets, address book " - "and wallet preferences.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + Center( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: restoreBackup + ? const SizedBox( + width: 512, + child: RestoreFromFileView(), + ) + : PrimaryButton( + desktopMed: true, + width: 200, + label: "Restore backup", + onPressed: () { + setState(() { + restoreBackup = true; + }); + }, + ), + ), + ], ), ], ), ), ), - ), - 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 enableAutoBackup() async { - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const EnableBackupDialog(); - }, - ); - } - - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension()! - .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()! - .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 restoreBackup() async { - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreBackupDialog(); - }, - ); - } - - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - restoreBackup(); - }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), - ); + )); + }); } } diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index f3e502bcb..acd0e689c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -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_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.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/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/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/desktop/desktop_dialog.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/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; -class CreateAutoBackup extends StatefulWidget { - const CreateAutoBackup({Key? key}) : super(key: key); +class CreateAutoBackup extends ConsumerStatefulWidget { + const CreateAutoBackup({ + Key? key, + this.secureStore = const SecureStorageWrapper( + FlutterSecureStorage(), + ), + }) : super(key: key); + + final FlutterSecureStorageInterface secureStore; @override - State createState() => _CreateAutoBackup(); + ConsumerState createState() => _CreateAutoBackup(); } -class _CreateAutoBackup extends State { +class _CreateAutoBackup extends ConsumerState { late final TextEditingController fileLocationController; late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; - late final FocusNode chooseFileLocation; + late final FlutterSecureStorageInterface secureStore; + + late final StackFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; late final FocusNode passphraseRepeatFocusNode; + final zxcvbn = Zxcvbn(); bool shouldShowPasswordHint = 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 => passphraseController.text == passphraseRepeatController.text; - String _currentDropDownValue = "Every 10 minutes"; + BackupFrequencyType _currentDropDownValue = + BackupFrequencyType.everyTenMinutes; - final List _dropDownItems = [ - "Every 10 minutes", - "Every 20 minutes", - "Every 30 minutes", + final List _dropDownItems = [ + BackupFrequencyType.everyTenMinutes, + BackupFrequencyType.everyAppStart, + BackupFrequencyType.afterClosingAWallet, ]; @override void initState() { + secureStore = widget.secureStore; + stackFileSystem = StackFileSystem(); + fileLocationController = TextEditingController(); passphraseController = TextEditingController(); passphraseRepeatController = TextEditingController(); - chooseFileLocation = FocusNode(); passphraseFocusNode = 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(); } @@ -59,7 +115,6 @@ class _CreateAutoBackup extends State { passphraseController.dispose(); passphraseRepeatController.dispose(); - chooseFileLocation.dispose(); passphraseFocusNode.dispose(); passphraseRepeatFocusNode.dispose(); @@ -70,10 +125,13 @@ class _CreateAutoBackup extends State { Widget build(BuildContext context) { 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( - maxHeight: 650, + maxHeight: 680, maxWidth: 600, child: Column( children: [ @@ -127,198 +185,287 @@ class _CreateAutoBackup extends State { height: 10, ), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - 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()!.textDark3, - ), - suffixIcon: Container( - decoration: BoxDecoration( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, - width: 20, - height: 17.5, - ), - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 24, - ), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 32), - child: Text( - "Create a passphrase", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark3, - ), - textAlign: TextAlign.left, - ), - ), - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - 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()!.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()! - .textDark3, - width: 20, - height: 17.5, + child: TextField( + autocorrect: false, + enableSuggestions: false, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension()! + .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) {}, ), + ); + }), + 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()! + .textDark3), + textAlign: TextAlign.left, ), ), - ), - ), - ), - ), - 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()!.textDark3, + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - suffixIcon: UnconstrainedBox( - child: GestureDetector( - key: const Key( - "createDesktopAutoBackupShowPassphraseButton2"), - onTap: () async { + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passphraseFocusNode, + controller: passphraseController, + 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()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { setState(() { - hidePassword = !hidePassword; + passwordFeedback = ""; }); - }, - 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()! - .textDark3, - width: 20, - height: 17.5, - ), + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: 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()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension()! + .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()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], ), ), ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, ), ), - ), + ], ), ), const SizedBox( @@ -343,39 +490,85 @@ class _CreateAutoBackup extends State { left: 32, right: 32, ), - child: DropdownButtonFormField( - isExpanded: true, - elevation: 0, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ), - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 10, - height: 5, - color: Theme.of(context).extension()!.textDark3, - ), - dropdownColor: - Theme.of(context).extension()!.textFieldActiveBG, - // focusColor: , - value: _currentDropDownValue, - items: _dropDownItems - .map( - (e) => DropdownMenuItem( - value: e, - child: Text(e), + child: isDesktop + ? DropdownButtonHideUnderline( + child: DropdownButton2( + offset: Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: _currentDropDownValue, + items: [ + ..._dropDownItems.map( + (e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = + "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + 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()! + .textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), ) - .toList(), - onChanged: (value) { - if (value is String) { - setState(() { - _currentDropDownValue = value; - }); - } - }, - ), + : null, ), + const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( @@ -385,7 +578,9 @@ class _CreateAutoBackup extends State { label: "Cancel", onPressed: () { 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 { Expanded( child: PrimaryButton( label: "Enable Auto Backup", - enabled: false, - onPressed: () {}, + enabled: shouldEnableCreate, + 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( + 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.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( + 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( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Failed to enable Auto Backup"), + ); + } + } + }, ), ) ], diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 046d136a8..963fb4441 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget { child: SecondaryButton( label: "Cancel", onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of(context).pop(); }, ), ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart deleted file mode 100644 index 07d49274a..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart +++ /dev/null @@ -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()!.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(); - }, - ), - ) - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart new file mode 100644 index 000000000..18988cb68 --- /dev/null +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -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 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 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 futureFiroList = [ + doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit), + isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit), + ]; + Future commitFiroFuture = Future.wait(futureFiroList); + List futureEpicList = [ + doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit), + isHeadCommit( + "cypherstack", "flutter_libepiccash", "main", epicCashCommit), + ]; + Future commitEpicFuture = Future.wait(futureEpicList); + List 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()!.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()! + .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()! + .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 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 + 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 + 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 + 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, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart new file mode 100644 index 000000000..ce3e3f3cc --- /dev/null +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart @@ -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 createState() => _DesktopSupportView(); +} + +class _DesktopSupportView extends ConsumerState { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension()!.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(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/providers/desktop/storage_crypto_handler_provider.dart b/lib/providers/desktop/storage_crypto_handler_provider.dart new file mode 100644 index 000000000..5b15ccaf3 --- /dev/null +++ b/lib/providers/desktop/storage_crypto_handler_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/desktop_password_service.dart'; + +final storageCryptoHandlerProvider = Provider((ref) => DPS()); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a6f23ffdc..f3e37e383 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -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/wallets_view/wallets_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_settings_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/settings_menu.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/event_bus/events/global/node_connection_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(), settings: RouteSettings(name: settings.name)); + case ForgotPasswordDesktopView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const ForgotPasswordDesktopView(), + settings: RouteSettings(name: settings.name)); + case DesktopHomeView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -1084,6 +1094,24 @@ class RouteGenerator { builder: (_) => const AdvancedSettings(), 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: if (args is List) { return FadePageRoute( diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 6f7d2b9bd..f51eefbba 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier { List get contacts { final keys = List.from( DB.instance.keys(boxName: DB.boxNameAddressBook)); - return keys + final _contacts = keys .map((id) => Contact.fromJson(Map.from(DB.instance .get(boxName: DB.boxNameAddressBook, key: id) as Map))) .toList(growable: false); + _contacts + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return _contacts; } Future>? _addressBookEntries; diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index da3bdfed0..d0920075d 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -174,9 +175,10 @@ class BitcoinWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -1282,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _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 updateSentCachedTxData(Map 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 bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2660,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 3a5cebdec..5b3b54663 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -6,11 +6,12 @@ import 'dart:typed_data'; import 'package:bech32/bech32.dart'; import 'package:bip32/bip32.dart' as bip32; 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:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -207,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip44); Future? _currentReceivingAddressP2PKH; - Future get currentReceivingAddressP2SH => - _currentReceivingAddressP2SH ??= - _getCurrentAddressForChain(0, DerivePathType.bip49); + // Future get currentReceivingAddressP2SH => + // _currentReceivingAddressP2SH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip49); Future? _currentReceivingAddressP2SH; @override @@ -258,7 +259,7 @@ class BitcoinCashWallet extends CoinServiceAPI { } Future updateStoredChainHeight({required int newHeight}) async { - DB.instance.put( + await DB.instance.put( boxName: walletId, key: "storedChainHeight", value: newHeight); } @@ -266,8 +267,13 @@ class BitcoinCashWallet extends CoinServiceAPI { Uint8List? decodeBase58; Segwit? decodeBech32; try { - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw ArgumentError('$address is not currently supported'); + } } } catch (e, s) {} try { @@ -292,11 +298,14 @@ class BitcoinCashWallet extends CoinServiceAPI { } catch (err) { // Bech32 decode fail } - if (_network.bech32 != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); + + if (decodeBech32 != null) { + if (_network.bech32 != decodeBech32.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } } } throw ArgumentError('$address has no matching Script'); @@ -609,7 +618,9 @@ class BitcoinCashWallet extends CoinServiceAPI { // get address tx counts final counts = await _getBatchTxCount(addresses: txCountCallArgs); - print("Counts $counts"); + if (kDebugMode) { + print("Counts $counts"); + } // check and add appropriate addresses for (int k = 0; k < txCountBatchSize; k++) { int count = counts["${_id}_$k"]!; @@ -745,31 +756,35 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on new incoming transaction for (final tx in unconfirmedTxnsToNotifyPending) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Sending transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } @@ -778,26 +793,30 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on confirmed for (final tx in unconfirmedTxnsToNotifyConfirmed) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Outgoing transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } @@ -862,7 +881,7 @@ class BitcoinCashWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height - updateStoredChainHeight(newHeight: currentHeight); + await updateStoredChainHeight(newHeight: currentHeight); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); @@ -1143,14 +1162,82 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _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 updateSentCachedTxData(Map 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 bool validateAddress(String address) { try { // 0 for bitcoincash: address scheme, 1 for legacy address - final format = Bitbox.Address.detectFormat(address); - print("format $format"); - return true; - } catch (e, s) { + final format = bitbox.Address.detectFormat(address); + if (kDebugMode) { + print("format $format"); + } + + if (_coin == Coin.bitcoincashTestnet) { + return true; + } + + if (format == bitbox.Address.formatCashAddr) { + return validateCashAddr(address); + } else { + return address.startsWith("1"); + } + } catch (e) { return false; } } @@ -1226,7 +1313,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); if (shouldRefresh) { - refresh(); + unawaited(refresh()); } } @@ -1522,12 +1609,15 @@ class BitcoinCashWallet extends CoinServiceAPI { break; } - print("Array key is ${jsonEncode(arrayKey)}"); + if (kDebugMode) { + print("Array key is ${jsonEncode(arrayKey)}"); + } final internalChainArray = DB.instance.get(boxName: walletId, key: arrayKey); if (derivePathType == DerivePathType.bip44) { - if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) { - return Bitbox.Address.toCashAddress(internalChainArray.last as String); + if (bitbox.Address.detectFormat(internalChainArray.last as String) == + bitbox.Address.formatLegacy) { + return bitbox.Address.toCashAddress(internalChainArray.last as String); } } return internalChainArray.last as String; @@ -1642,7 +1732,9 @@ class BitcoinCashWallet extends CoinServiceAPI { batches[batchNumber] = {}; } 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({ scripthash: [scripthash] }); @@ -1818,20 +1910,28 @@ class BitcoinCashWallet extends CoinServiceAPI { }) async { try { final Map> args = {}; - print("Address $addresses"); + if (kDebugMode) { + print("Address $addresses"); + } for (final entry in addresses.entries) { args[entry.key] = [_convertToScriptHash(entry.value, _network)]; } - print("Args ${jsonEncode(args)}"); + if (kDebugMode) { + print("Args ${jsonEncode(args)}"); + } final response = await electrumXClient.getBatchHistory(args: args); - print("Response ${jsonEncode(response)}"); + if (kDebugMode) { + print("Response ${jsonEncode(response)}"); + } final Map result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } - print("result ${jsonEncode(result)}"); + if (kDebugMode) { + print("result ${jsonEncode(result)}"); + } return result; } catch (e, s) { Logging.instance.log( @@ -1995,8 +2095,10 @@ class BitcoinCashWallet extends CoinServiceAPI { /// Returns the scripthash or throws an exception on invalid bch address String _convertToScriptHash(String bchAddress, NetworkType network) { try { - if (Bitbox.Address.detectFormat(bchAddress) == 0) { - bchAddress = Bitbox.Address.toLegacyAddress(bchAddress); + if (bitbox.Address.detectFormat(bchAddress) == + bitbox.Address.formatCashAddr && + validateCashAddr(bchAddress)) { + bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); final hash = sha256.convert(output.toList(growable: false)).toString(); @@ -2073,8 +2175,9 @@ class BitcoinCashWallet extends CoinServiceAPI { List allAddressesOld = await _fetchAllOwnAddresses(); List allAddresses = []; for (String address in allAddressesOld) { - if (Bitbox.Address.detectFormat(address) == 1) { - allAddresses.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy && + addressType(address: address) == DerivePathType.bip44) { + allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); } @@ -2085,8 +2188,9 @@ class BitcoinCashWallet extends CoinServiceAPI { as List; List changeAddressesP2PKH = []; for (var address in changeAddressesP2PKHOld) { - if (Bitbox.Address.detectFormat(address as String) == 1) { - changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address as String) == + bitbox.Address.formatLegacy) { + changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address)); } else { changeAddressesP2PKH.add(address); } @@ -2108,21 +2212,27 @@ class BitcoinCashWallet extends CoinServiceAPI { unconfirmedCachedTransactions .removeWhere((key, value) => value.confirmedStatus); - print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + if (kDebugMode) { + print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + } if (cachedTransactions != null) { for (final tx in allTxHashes.toList(growable: false)) { final txHeight = tx["height"] as int; if (txHeight > 0 && txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - print(cachedTransactions.findTransaction(tx["tx_hash"] as String)); - print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + if (kDebugMode) { + print( + cachedTransactions.findTransaction(tx["tx_hash"] as String)); + print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + } final cachedTx = cachedTransactions.findTransaction(tx["tx_hash"] as String); if (!(cachedTx != null && addressType(address: cachedTx.address) == DerivePathType.bip44 && - Bitbox.Address.detectFormat(cachedTx.address) == 1)) { + bitbox.Address.detectFormat(cachedTx.address) == + bitbox.Address.formatLegacy)) { allTxHashes.remove(tx); } } @@ -2401,6 +2511,7 @@ class BitcoinCashWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -2782,8 +2893,14 @@ class BitcoinCashWallet extends CoinServiceAPI { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { String address = output["scriptPubKey"]["addresses"][0] as String; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(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)) { addressTxid[address] = []; @@ -2814,9 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); - } // receives final receiveDerivation = receiveDerivations[address]; @@ -2950,36 +3064,36 @@ class BitcoinCashWallet extends CoinServiceAPI { required List recipients, required List satoshiAmounts, }) async { - final builder = Bitbox.Bitbox.transactionBuilder(); + final builder = bitbox.Bitbox.transactionBuilder(); // retrieve address' utxos from the rest api - List _utxos = + List _utxos = []; // await Bitbox.Address.utxo(address) as List; - utxosToUse.forEach((element) { - _utxos.add(Bitbox.Utxo( + for (var element in utxosToUse) { + _utxos.add(bitbox.Utxo( element.txid, element.vout, - Bitbox.BitcoinCash.fromSatoshi(element.value), + bitbox.BitcoinCash.fromSatoshi(element.value), element.value, 0, MINIMUM_CONFIRMATIONS + 1)); - }); - Logger.print("bch utxos: ${_utxos}"); + } + Logger.print("bch utxos: $_utxos"); // placeholder for input signatures - final signatures = []; + final List> signatures = []; // 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 // withdrawal transaction - _utxos.forEach((Bitbox.Utxo utxo) { + for (var utxo in _utxos) { // add the utxo as an input for the transaction builder.addInput(utxo.txid, utxo.vout); 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 signatures.add({ @@ -2988,15 +3102,15 @@ class BitcoinCashWallet extends CoinServiceAPI { "original_amount": utxo.satoshis }); - totalBalance += utxo.satoshis; - }); + // totalBalance += utxo.satoshis; + } // calculate the fee based on number of inputs and one expected output - final fee = - Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); + // final fee = + // bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); // 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 for (int i = 0; i < recipients.length; i++) { @@ -3006,12 +3120,12 @@ class BitcoinCashWallet extends CoinServiceAPI { } // sign all inputs - signatures.forEach((signature) { + for (var signature in signatures) { builder.sign( signature["vin"] as int, - signature["key_pair"] as Bitbox.ECPair, + signature["key_pair"] as bitbox.ECPair, signature["original_amount"] as int); - }); + } // build the transaction final tx = builder.build(); @@ -3038,7 +3152,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); // clear cache - _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); // back up data await _rescanBackup(); @@ -3326,9 +3440,10 @@ class BitcoinCashWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index c36fa9eee..655865494 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -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/firo/firo_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/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -277,4 +277,7 @@ abstract class CoinServiceAPI { Future estimateFeeFor(int satoshiAmount, int feeRate); Future generateNewAddress(); + + // used for electrumx coins + Future updateSentCachedTxData(Map txData); } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 0235a0c02..67be291a2 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _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 updateSentCachedTxData(Map 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 bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } @@ -2983,9 +3033,10 @@ class DogecoinWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 7ccb7feaf..6c71f39e4 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -558,9 +558,10 @@ class EpicCashWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -832,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI { final txLogEntryFirst = txLogEntry[0]; Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst"); final wallet = await Hive.openBox(_walletId); - final slateToAddresses = (await wallet.get("slate_to_address")) as Map?; - slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss']; + final slateToAddresses = + (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); - 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) { Logging.instance.log("Error sending $e - $s", level: LogLevel.Error); @@ -2154,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI { as String? ?? ""; String? commitId = slatesToCommits[slateId]?['commitId'] as String?; - Logging.instance - .log("commitId: $commitId $slateId", level: LogLevel.Info); + Logging.instance.log( + "commitId: $commitId, slateId: $slateId, id: ${tx["id"]}", + level: LogLevel.Info); bool isCancelled = tx["tx_type"] == "TxSentCancelled" || tx["tx_type"] == "TxReceivedCancelled"; @@ -2258,6 +2266,14 @@ class EpicCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _transactionData; + // not used in epic + TransactionData? cachedTxData; + + @override + Future updateSentCachedTxData(Map txData) async { + // not used in epic + } + @override Future> get unspentOutputs => throw UnimplementedError(); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 61ef2e9de..d19c4f1ab 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -821,9 +821,10 @@ class FiroWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -907,6 +908,52 @@ class FiroWallet extends CoinServiceAPI { Future get _txnData => _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 updateSentCachedTxData(Map 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 Future? _lelantusTransactionData; Future get lelantusTransactionData => @@ -1109,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI { final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + txData["txid"] = txHash; + // dirty ui update hack + await updateSentCachedTxData(txData as Map); return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -3464,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 0ab3a92a8..4551325f7 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -174,9 +175,10 @@ class LitecoinWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -1284,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _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 updateSentCachedTxData(Map 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 bool validateAddress(String address) { return Address.validateAddress(address, _network, _network.bech32!); @@ -2672,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index c8329ec28..8054fe168 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -108,6 +108,9 @@ class Manager with ChangeNotifier { try { final txid = await _currentWallet.confirmSend(txData: txData); + txData["txid"] = txid; + await _currentWallet.updateSentCachedTxData(txData); + notifyListeners(); return txid; } catch (e) { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index b0ebac4e6..a96ecac83 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -699,7 +699,7 @@ class MoneroWallet extends CoinServiceAPI { name: name, type: WalletType.monero, isRecovery: false, - restoreHeight: credentials.height ?? 0, + restoreHeight: bufferedCreateHeight, date: DateTime.now(), path: path, dirPath: dirPath, @@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _transactionData; + // not used in monero + TransactionData? cachedTxData; + + @override + Future updateSentCachedTxData(Map txData) async { + // not used in monero + } + Future _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; @@ -1345,10 +1353,8 @@ class MoneroWallet extends CoinServiceAPI { Future> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } @@ -1376,9 +1382,10 @@ class MoneroWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index e9cae1ab2..8a4b26012 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -170,9 +171,10 @@ class NamecoinWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } @@ -1275,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _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 updateSentCachedTxData(Map 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 bool validateAddress(String address) { return Address.validateAddress(address, _network, namecoin.bech32!); @@ -2672,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI { await DB.instance.put( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index d3aba5bbb..4c02ec037 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.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. - Future _generateNewWallet() async { + Future _generateNewWallet({int seedWordsLength = 14}) async { Logging.instance .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); // 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); final path = await pathForWallet(name: name, type: WalletType.wownero); credentials = wownero.createWowneroNewWalletCredentials( - name: name, - language: "English", - ); + name: name, language: "English", seedWordsLength: seedWordsLength); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.wownero), @@ -712,9 +711,12 @@ class WowneroWallet extends CoinServiceAPI { // To restore from a seed final wallet = await _walletCreationService?.create(credentials); - // subtract a couple days to ensure we have a buffer for SWB - final bufferedCreateHeight = - getSeedHeightSync(wallet?.seed.trim() as String); + final bufferedCreateHeight = (seedWordsLength == 14) + ? 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( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); @@ -722,6 +724,7 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); + walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); @@ -778,7 +781,7 @@ class WowneroWallet extends CoinServiceAPI { @override // TODO: implement initializeWallet - Future initializeNew() async { + Future initializeNew({int seedWordsLength = 14}) async { await _prefs.init(); // TODO: ping actual wownero network // try { @@ -796,7 +799,7 @@ class WowneroWallet extends CoinServiceAPI { prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(storage!); - await _generateNewWallet(); + await _generateNewWallet(seedWordsLength: seedWordsLength); // var password; // try { // password = @@ -942,6 +945,11 @@ class WowneroWallet extends CoinServiceAPI { required int maxNumberOfIndexesToCheck, required int height, }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + await _prefs.init(); longMutex = true; final start = DateTime.now(); @@ -969,7 +977,18 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( 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 .put(boxName: walletId, key: "restoreHeight", value: height); @@ -1195,6 +1214,14 @@ class WowneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _transactionData; + // not used in wownero + TransactionData? cachedTxData; + + @override + Future updateSentCachedTxData(Map txData) async { + // not used in wownero + } + Future _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; @@ -1351,10 +1378,8 @@ class WowneroWallet extends CoinServiceAPI { Future> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } @@ -1382,9 +1407,10 @@ class WowneroWallet extends CoinServiceAPI { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 386ea1cd8..f853a00d8 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,8 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension()!.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 themeDark => "assets/svg/dark/dark-theme.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 circleDollarSign => "assets/svg/dollar-sign-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 polygon => "assets/svg/Polygon.svg"; String get personaIncognito => "assets/svg/persona-incognito-1.svg"; diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 4fb3fb54b..e27fbaa3d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -35,10 +35,6 @@ abstract class Constants { static const int pinLength = 4; - // enable testnet - // TODO: currently unused - static const bool allowTestnets = true; - // Enable Logger.print statements static const bool disableLogger = false; @@ -66,7 +62,7 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14]); + values.addAll([14, 25]); break; } return values; diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart new file mode 100644 index 000000000..da537b3c2 --- /dev/null +++ b/lib/utilities/desktop_password_service.dart @@ -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 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 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 hasPassword() async { + return (await secureStorageWrapper.read(key: _kKeyBlobKey)) != null; + } +} diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 95294c8aa..48212bde8 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -132,7 +132,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: return "litecoin"; case Coin.bitcoincashTestnet: - return "bitcoincash"; + return "bchtest"; case Coin.firoTestNet: return "firo"; case Coin.dogecoinTestNet: diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 7cb01e8b1..5e35f9a03 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -1,26 +1,121 @@ 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/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -Future 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 testMoneroNodeConnection( + Uri uri, + bool allowBadX509Certificate, +) async { + final client = HttpClient(); + MoneroNodeConnectionResponse? badCertResponse; try { - final client = http.Client(); - final response = await client - .post( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}), - ) - .timeout(const Duration(milliseconds: 1200), - onTimeout: () async => http.Response('Error', 408)); + client.badCertificateCallback = (cert, url, port) { + if (allowBadX509Certificate) { + return true; + } - 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? // or we can check for certain values in the response to decide - return true; + return MoneroNodeConnectionResponse(null, null, null, true); } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - return false; + if (badCertResponse != null) { + return badCertResponse!; + } else { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return MoneroNodeConnectionResponse(null, null, null, false); + } + } finally { + client.close(force: true); } } + +Future 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( + 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; +} diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 299ba5bec..63aa19afb 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -508,6 +508,25 @@ class STextStyles { // 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) { switch (_theme(context).themeType) { case ThemeType.light: diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index aa7f75b1f..a87d1e6b2 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget { required this.text, this.onTap, this.enabled = true, + this.textSize, }) : super(key: key); final String text; final VoidCallback? onTap; final bool enabled; + final double? textSize; @override ConsumerState createState() => _BlueTextButtonState(); @@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState textAlign: TextAlign.center, text: TextSpan( 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 ? (TapGestureRecognizer() ..onTap = () { diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index bf9d2746e..1da7e9012 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState { 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) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index a5345161c..7ffd290f3 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget { 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) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index be1d51596..ea2638264 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; class StackDialogBase extends StatelessWidget { const StackDialogBase({ @@ -17,7 +18,8 @@ class StackDialogBase extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: Column( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + !Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center, children: [ Material( borderRadius: BorderRadius.circular( @@ -179,10 +181,16 @@ class StackOkDialog extends StatelessWidget { ), Expanded( child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - onOkPressed?.call("OK"); - }, + onPressed: !Util.isDesktop + ? () { + Navigator.of(context).pop(); + onOkPressed?.call("OK"); + } + : () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + // onOkPressed?.call("OK"); + }, style: Theme.of(context) .extension()! .getPrimaryEnabledButtonColor(context), diff --git a/pubspec.lock b/pubspec.lock index 20992d827..2f7770d3a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1378,8 +1378,8 @@ packages: dependency: "direct main" description: path: "." - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 - resolved-ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" + resolved-ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" url: "https://github.com/cypherstack/stack_wallet_backup.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 9ba6d4eb9..b7947a58d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.14+86 +version: 1.5.17+89 environment: sdk: ">=2.17.0 <3.0.0" @@ -54,7 +54,7 @@ dependencies: stack_wallet_backup: git: url: https://github.com/cypherstack/stack_wallet_backup.git - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: 011dc9ce3d29f5fdeeaf711d58b5122f055c146d # Utility plugins # provider: ^6.0.1 @@ -298,6 +298,7 @@ flutter: - assets/svg/persona-easy-1.svg - assets/svg/persona-incognito-1.svg - assets/svg/Button.svg + - assets/svg/enabled-button.svg - assets/svg/lock-circle.svg - assets/svg/dollar-sign-circle.svg - assets/svg/language-circle.svg @@ -338,6 +339,8 @@ flutter: - assets/svg/message-question-1.svg - assets/svg/drd-icon.svg - assets/svg/box-auto.svg + - assets/svg/framed-address-book.svg + - assets/svg/framed-gear.svg # exchange icons - assets/svg/exchange_icons/change_now_logo_1.svg - assets/svg/exchange_icons/simpleswap-icon.svg diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 5ed032b1a..378f7a604 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,25 +1,37 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) -mkdir build +mkdir -p build # Build JsonCPP -cd build -git clone https://github.com/open-source-parsers/jsoncpp.git -cd jsoncpp +cd build || exit +if ! [ -x "$(command -v git)" ]; then + 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 -mkdir build -cd build +mkdir -p build +cd build || exit 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 # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip #pip3 install --user meson --upgrade # pip3 install --user gi-docgen -cd build -git clone https://gitlab.gnome.org/GNOME/libsecret.git -cd libsecret +cd build || exit +git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret +cd libsecret || exit +if ! [ -x "$(command -v meson)" ]; then + echo 'Error: meson is not installed.' >&2 + exit 1 +fi meson _build +if ! [ -x "$(command -v ninja)" ]; then + echo 'Error: ninja is not installed.' >&2 + exit 1 +fi ninja -C _build diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index 1059f7fc3..c5effd223 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -94,19 +94,19 @@ void main() { test("get contacts", () { final service = AddressBookService(); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("get addressBookEntries", () async { final service = AddressBookService(); expect((await service.addressBookEntries).toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("search contacts", () async { final service = AddressBookService(); final results = await service.search("j"); - expect(results.toString(), [contactA, contactB].toString()); + expect(results.toString(), [contactB, contactA].toString()); final results2 = await service.search("ja"); expect(results2.toString(), [contactB].toString()); @@ -118,7 +118,7 @@ void main() { expect(results4.toString(), [].toString()); 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"); expect(results6.toString(), [contactC].toString()); @@ -140,7 +140,7 @@ void main() { expect(result, false); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("edit contact", () async { @@ -149,14 +149,14 @@ void main() { expect(await service.editContact(editedContact), true); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, editedContact, contactC].toString()); + [contactC, contactA, editedContact].toString()); }); test("remove existing contact", () async { final service = AddressBookService(); await service.removeContact(contactB.id); 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 { @@ -164,7 +164,7 @@ void main() { await service.removeContact("some id"); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); tearDown(() async { diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 4c392fe81..077163809 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -1,4 +1,3 @@ -import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; @@ -61,7 +60,7 @@ void main() { }); }); - group("validate mainnet bitcoincash addresses", () { + group("mainnet bitcoincash addressType", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; @@ -137,10 +136,172 @@ void main() { 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", () { expect( mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), - true); + false); expect(secureStore?.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index a3ae28a4b..c5f300c16 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI { // TODO: implement generateNewAddress throw UnimplementedError(); } + + @override + Future updateSentCachedTxData(Map txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } } diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart new file mode 100644 index 000000000..79bf98a41 --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -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.boxName); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box); + } + + 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 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 pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/monero/monero_wallet_test_data.dart b/test/services/coins/monero/monero_wallet_test_data.dart new file mode 100644 index 000000000..dc0a0f4cb --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test_data.dart @@ -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' + ] +]; diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart new file mode 100644 index 000000000..78a9c56c1 --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -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.boxName); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box); + } + + 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 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 pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/wownero/wownero_wallet_test_data.dart b/test/services/coins/wownero/wownero_wallet_test_data.dart new file mode 100644 index 000000000..7f27fc486 --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test_data.dart @@ -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' + ] +];