From c88dbbc5c860c5b0b0169235cd936fae493b1bc4 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 11:17:26 -0600 Subject: [PATCH 1/6] fix https://github.com/cypherstack/stack_wallet/issues/1121 --- .../send_view/confirm_transaction_view.dart | 788 +++++++++--------- 1 file changed, 376 insertions(+), 412 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 5090645e3..6d40734b0 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -118,11 +118,7 @@ class _ConfirmTransactionViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final List txids = []; Future txDataFuture; @@ -131,11 +127,13 @@ class _ConfirmTransactionViewState try { if (widget.isTokenTx) { - txDataFuture = - ref.read(pCurrentTokenWallet)!.confirmSend(txData: widget.txData); + txDataFuture = ref + .read(pCurrentTokenWallet)! + .confirmSend(txData: widget.txData); } else if (widget.isPaynymNotificationTransaction) { - txDataFuture = (wallet as PaynymInterface) - .broadcastNotificationTx(txData: widget.txData); + txDataFuture = (wallet as PaynymInterface).broadcastNotificationTx( + txData: widget.txData, + ); } else if (widget.isPaynymTransaction) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { @@ -145,8 +143,9 @@ class _ConfirmTransactionViewState if (widget.txData.sparkMints == null) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { - txDataFuture = - wallet.confirmSparkMintTransactions(txData: widget.txData); + txDataFuture = wallet.confirmSparkMintTransactions( + txData: widget.txData, + ); } break; @@ -171,10 +170,7 @@ class _ConfirmTransactionViewState } } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); sendProgressController.triggerSuccess?.call(); await Future.delayed(const Duration(seconds: 5)); @@ -189,12 +185,10 @@ class _ConfirmTransactionViewState // save note for (final txid in txids) { - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); } @@ -209,8 +203,9 @@ class _ConfirmTransactionViewState // pop back to wallet if (mounted) { if (widget.onSuccessInsteadOfRouteOnSuccess == null) { - Navigator.of(context) - .popUntil(ModalRoute.withName(routeOnSuccessName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(routeOnSuccessName)); } else { widget.onSuccessInsteadOfRouteOnSuccess!.call(); } @@ -253,9 +248,7 @@ class _ConfirmTransactionViewState "Broadcast transaction failed", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Flexible( child: SingleChildScrollView( child: SelectableText( @@ -264,9 +257,7 @@ class _ConfirmTransactionViewState ), ), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -294,9 +285,10 @@ class _ConfirmTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -377,7 +369,8 @@ class _ConfirmTransactionViewState case FiroType.spark: fee = widget.txData.fee; - amountWithoutChange = (widget.txData.amountWithoutChange ?? + amountWithoutChange = + (widget.txData.amountWithoutChange ?? Amount.zeroWith( fractionDigits: wallet.cryptoCurrency.fractionDigits, )) + @@ -394,82 +387,79 @@ class _ConfirmTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(), - ), - Text( - "Confirm $unit transaction", - style: STextStyles.desktopH3(context), + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: + () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm $unit transaction", + style: STextStyles.desktopH3(context), + ), + ], ), + Flexible(child: SingleChildScrollView(child: child)), ], ), - Flexible( - child: SingleChildScrollView( - child: child, - ), - ), - ], - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -478,13 +468,8 @@ class _ConfirmTransactionViewState Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Send $unit", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 12, - ), + Text("Send $unit", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -495,9 +480,7 @@ class _ConfirmTransactionViewState : "Recipient", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName @@ -508,25 +491,23 @@ class _ConfirmTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), SelectableText( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( amountWithoutChange, - ethContract: widget.isTokenTx - ? ref - .watch(pCurrentTokenWallet)! - .tokenContract - : null, + ethContract: + widget.isTokenTx + ? ref + .watch(pCurrentTokenWallet)! + .tokenContract + : null, ), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, @@ -534,10 +515,7 @@ class _ConfirmTransactionViewState ], ), ), - if (coin is! NanoCurrency) - const SizedBox( - height: 12, - ), + if (coin is! NanoCurrency) const SizedBox(height: 12), if (coin is! NanoCurrency) RoundedWhiteContainer( child: Row( @@ -556,9 +534,7 @@ class _ConfirmTransactionViewState ), ), if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.fee != null && widget.txData.vSize != null) RoundedWhiteContainer( child: Row( @@ -568,9 +544,7 @@ class _ConfirmTransactionViewState "sats/vByte", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), @@ -579,9 +553,7 @@ class _ConfirmTransactionViewState ), ), if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) RoundedWhiteContainer( child: Column( @@ -591,9 +563,7 @@ class _ConfirmTransactionViewState "On chain note", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( widget.txData.noteOnChain!, style: STextStyles.itemSubtitle12(context), @@ -602,9 +572,7 @@ class _ConfirmTransactionViewState ), ), if (widget.txData.note!.isNotEmpty) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.note!.isNotEmpty) RoundedWhiteContainer( child: Column( @@ -614,9 +582,7 @@ class _ConfirmTransactionViewState (coin is Epiccash) ? "Local Note" : "Note", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( widget.txData.note!, style: STextStyles.itemSubtitle12(context), @@ -644,9 +610,10 @@ class _ConfirmTransactionViewState children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -674,9 +641,7 @@ class _ConfirmTransactionViewState width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text( "Send $unit", style: STextStyles.desktopTextMedium(context), @@ -697,9 +662,7 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Builder( builder: (context) { final externalCalls = ref.watch( @@ -710,77 +673,81 @@ class _ConfirmTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = widget.isTokenTx - ? ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getTokenPrice( - ref - .read(pCurrentTokenWallet)! - .tokenContract - .address, - ) - .item1 - : ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getPrice(coin) - .item1; + final price = + widget.isTokenTx + ? ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getTokenPrice( + ref + .read(pCurrentTokenWallet)! + .tokenContract + .address, + ) + .item1 + : ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getPrice(coin) + .item1; if (price > Decimal.zero) { - fiatAmount = - (amountWithoutChange.decimal * price) - .toAmount(fractionDigits: 2) - .fiatString( - locale: ref + fiatAmount = (amountWithoutChange.decimal * + price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: + ref .read( localeServiceChangeNotifierProvider, ) .locale, - ); + ); } } return Row( children: [ SelectableText( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( amountWithoutChange, - ethContract: widget.isTokenTx - ? ref - .watch(pCurrentTokenWallet)! - .tokenContract - : null, + ethContract: + widget.isTokenTx + ? ref + .watch( + pCurrentTokenWallet, + )! + .tokenContract + : null, + ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, ), - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), ), if (externalCalls) Text( " | ", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), if (externalCalls) SelectableText( - "~$fiatAmount ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ); @@ -791,9 +758,10 @@ class _ConfirmTransactionViewState ), Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), Padding( padding: const EdgeInsets.all(12), @@ -809,22 +777,24 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( // TODO: [prio=med] spark transaction specifics - better handling widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName : widget.txData.recipients?.first.address ?? - widget.txData.sparkRecipients!.first + widget + .txData + .sparkRecipients! + .first .address, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -833,9 +803,10 @@ class _ConfirmTransactionViewState if (widget.isPaynymTransaction) Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), if (widget.isPaynymTransaction) Padding( @@ -850,17 +821,16 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -905,10 +875,7 @@ class _ConfirmTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -919,10 +886,7 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is Epiccash) - const SizedBox( - height: 8, - ), + if (coin is Epiccash) const SizedBox(height: 8), if (coin is Epiccash) ClipRRect( borderRadius: BorderRadius.circular( @@ -941,47 +905,46 @@ class _ConfirmTransactionViewState _onChainNoteFocusNode, context, ).copyWith( - suffixIcon: onChainNoteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - onChainNoteController.text = ""; - }); - }, - ), - ], + suffixIcon: + onChainNoteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + onChainNoteController.text = + ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - if (coin is Epiccash) - const SizedBox( - height: 12, - ), + if (coin is Epiccash) const SizedBox(height: 12), SelectableText( (coin is Epiccash) ? "Local Note (optional)" : "Note (optional)", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -993,11 +956,13 @@ class _ConfirmTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), onChanged: (_) => setState(() {}), @@ -1013,39 +978,36 @@ class _ConfirmTransactionViewState bottom: 12, right: 5, ), - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState( - () => noteController.text = "", - ); - }, - ), - ], + suffixIcon: + noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState( + () => noteController.text = "", + ); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), ), if (isDesktop && !widget.isPaynymTransaction) Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "Transaction fee", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -1053,19 +1015,16 @@ class _ConfirmTransactionViewState ), if (isDesktop && !widget.isPaynymTransaction) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -1077,9 +1036,7 @@ class _ConfirmTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "sats/vByte", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -1090,19 +1047,16 @@ class _ConfirmTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -1110,154 +1064,164 @@ class _ConfirmTransactionViewState ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 23 : 12, - ), + SizedBox(height: isDesktop ? 23 : 12), if (!widget.isTokenTx) Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ) - : const EdgeInsets.all(12), - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, + padding: + isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), ), SelectableText( ref .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), textAlign: TextAlign.right, ), ], ), ), ), - SizedBox( - height: isDesktop ? 28 : 16, - ), + SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { - final dynamic unlocked; - if (isDesktop) { - unlocked = await showDialog( + final unlocked = await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); + if (context.mounted && unlocked is bool) { + if (unlocked) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase", + context: context, + ), + ); + } + } } else { - unlocked = await Navigator.push( + final unlocked = await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: + "Confirm Transaction", + ), + settings: const RouteSettings( + name: "/confirmsendlockscreen", ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), ), ); - } - if (mounted) { - if (unlocked == true) { - unawaited(_attemptSend(context)); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", - context: context, - ), - ); + if (context.mounted) { + if (unlocked == true) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid PIN", + context: context, + ), + ); + } } } }, ), ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (isDesktop) const SizedBox(height: 32), ], ), ), From 04631ac3b8556d2f7d9766d6163202abd6cae254 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 12:28:03 -0600 Subject: [PATCH 2/6] fix https://github.com/cypherstack/stack_wallet/issues/1108 --- .../confirm_change_now_send.dart | 640 ++++++++---------- 1 file changed, 301 insertions(+), 339 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 999fca764..239b56db4 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -17,6 +17,7 @@ import 'package:uuid/uuid.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/trade_wallet_lookup.dart'; +import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; @@ -98,11 +99,7 @@ class _ConfirmChangeNowSendViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); late String txid; Future txidFuture; @@ -118,10 +115,7 @@ class _ConfirmChangeNowSendViewState unawaited(wallet.refresh()); - final results = await Future.wait([ - txidFuture, - time, - ]); + final results = await Future.wait([txidFuture, time]); sendProgressController.triggerSuccess?.call(); await Future.delayed(const Duration(seconds: 5)); @@ -129,15 +123,15 @@ class _ConfirmChangeNowSendViewState txid = (results.first as TxData).txid!; // save note - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); - await ref.read(tradeSentFromStackLookupProvider).save( + await ref + .read(tradeSentFromStackLookupProvider) + .save( tradeWalletLookup: TradeWalletLookup( uuid: const Uuid().v1(), txid: txid, @@ -162,7 +156,11 @@ class _ConfirmChangeNowSendViewState Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e, s) { - Logging.instance.e("Broadcast transaction failed: ", error: e, stackTrace: s); + Logging.instance.e( + "Broadcast transaction failed: ", + error: e, + stackTrace: s, + ); // pop sending dialog Navigator.of(context).pop(); @@ -182,9 +180,10 @@ class _ConfirmChangeNowSendViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -205,53 +204,61 @@ class _ConfirmChangeNowSendViewState if (Util.isDesktop) { unlocked = await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); } else { unlocked = await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), settings: const RouteSettings(name: "/confirmsendlockscreen"), ), ); } - if (unlocked is bool && unlocked && mounted) { - await _attemptSend(context); + if (unlocked is bool && mounted) { + if (unlocked) { + await _attemptSend(context); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase", + context: context, + ), + ); + } } } @@ -292,11 +299,7 @@ class _ConfirmChangeNowSendViewState body: LayoutBuilder( builder: (builderContext, constraints) { return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), child: SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( @@ -318,205 +321,195 @@ class _ConfirmChangeNowSendViewState }, child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( + builder: + (child) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( children: [ - const SizedBox( - width: 6, - ), - const AppBarBackButton( - isCompact: true, - iconSize: 23, - ), - const SizedBox( - width: 12, - ), - Text( - "Confirm ${ref.watch(pWalletCoin(walletId)).ticker} transaction", - style: STextStyles.desktopH3(context), + Row( + children: [ + const SizedBox(width: 6), + const AppBarBackButton(isCompact: true, iconSize: 23), + const SizedBox(width: 12), + Text( + "Confirm ${ref.watch(pWalletCoin(walletId)).ticker} transaction", + style: STextStyles.desktopH3(context), + ), + ], ), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - borderColor: Theme.of(context) - .extension()! - .background, - child: child, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), - const SizedBox( - height: 16, - ), - Row( + child: Column( children: [ - Text( - "Transaction fee", - style: - STextStyles.desktopTextExtraExtraSmall(context), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: child, ), - ], - ), - const SizedBox( - height: 10, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - ref - .watch( - pAmountFormatter( - ref.watch(pWalletCoin(walletId)), - ), - ) - .format(widget.txData.fee!), - style: - STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Total amount", - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + const SizedBox(height: 16), + Row( + children: [ + Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), - ), - Builder( - builder: (context) { - final coin = ref.read(pWalletCoin(walletId)); - final fee = widget.txData.fee!; - final amount = widget.txData.amountWithoutChange!; - final total = amount + fee; - - return Text( - ref.watch(pAmountFormatter(coin)).format(total), - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + ], + ), + const SizedBox(height: 10), + RoundedContainer( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + ref + .watch( + pAmountFormatter( + ref.watch(pWalletCoin(walletId)), + ), + ) + .format(widget.txData.fee!), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), - textAlign: TextAlign.right, - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, + ), + ], ), ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Send", - buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: _confirmSend, + const SizedBox(height: 16), + RoundedContainer( + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + Builder( + builder: (context) { + final coin = ref.read(pWalletCoin(walletId)); + final fee = widget.txData.fee!; + final amount = + widget.txData.amountWithoutChange!; + final total = amount + fee; + + return Text( + ref + .watch(pAmountFormatter(coin)) + .format(total), + style: STextStyles.itemSubtitle12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, + ), + ], ), ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ), + ], + ), ], ), - ], - ), + ), + ], ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ConditionalParent( condition: isDesktop, - builder: (child) => Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.background, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + builder: + (child) => Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [child]), ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - child, - ], - ), - ), - ), child: Text( "Send ${ref.watch(pWalletCoin(walletId)).ticker}", - style: isDesktop - ? STextStyles.desktopTextMedium(context) - : STextStyles.pageTitleH1(context), + style: + isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.pageTitleH1(context), ), ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Send from", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Send from", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( ref.watch(pWalletName(walletId)), style: STextStyles.itemSubtitle12(context), @@ -526,13 +519,10 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -541,9 +531,7 @@ class _ConfirmChangeNowSendViewState "${trade.exchangeName} address", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle12(context), @@ -553,68 +541,64 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), ConditionalParent( condition: isDesktop, - builder: (child) => Row( - children: [ - child, - Builder( - builder: (context) { - final coin = ref.watch(pWalletCoin(walletId)); - final price = ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ); - final amountWithoutChange = - widget.txData.amountWithoutChange!; - final value = - (price.item1 * amountWithoutChange.decimal) + builder: + (child) => Row( + children: [ + child, + Builder( + builder: (context) { + final coin = ref.watch(pWalletCoin(walletId)); + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); + final amountWithoutChange = + widget.txData.amountWithoutChange!; + final value = (price.item1 * + amountWithoutChange.decimal) .toAmount(fractionDigits: 2); - final currency = ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), - ); - final locale = ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ); - - return Text( - " | ${value.fiatString(locale: locale)} $currency", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ); - }, + final currency = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + ); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + + return Text( + " | ${value.fiatString(locale: locale)} $currency", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ], ), - ], - ), child: Text( ref .watch( - pAmountFormatter( - ref.watch(pWalletCoin(walletId)), - ), + pAmountFormatter(ref.watch(pWalletCoin(walletId))), ) .format((widget.txData.amountWithoutChange!)), style: STextStyles.itemSubtitle12(context), @@ -626,13 +610,10 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -646,9 +627,7 @@ class _ConfirmChangeNowSendViewState .watch( pAmountFormatter(ref.read(pWalletCoin(walletId))), ) - .format( - widget.txData.fee!, - ), + .format(widget.txData.fee!), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -657,24 +636,16 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.note ?? "", style: STextStyles.itemSubtitle12(context), @@ -684,21 +655,15 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Trade ID", - style: STextStyles.smallMed12(context), - ), + Text("Trade ID", style: STextStyles.smallMed12(context)), Text( trade.tradeId, style: STextStyles.itemSubtitle12(context), @@ -707,24 +672,23 @@ class _ConfirmChangeNowSendViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Total amount", style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + color: + Theme.of( + context, + ).extension()!.textConfirmTotalAmount, ), ), Builder( @@ -737,9 +701,10 @@ class _ConfirmChangeNowSendViewState return Text( ref.watch(pAmountFormatter(coin)).format(total), style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, ), textAlign: TextAlign.right, ); @@ -748,10 +713,7 @@ class _ConfirmChangeNowSendViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) const Spacer(), if (!isDesktop) PrimaryButton( From b40daff8425cd4402c2dc77c07eecfc1002e4614 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 14:29:00 -0600 Subject: [PATCH 3/6] use state provider and autoformatting --- .../restore_options_view.dart | 619 +++++++++--------- 1 file changed, 294 insertions(+), 325 deletions(-) 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 a7fe47d6e..6d424a2c8 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 @@ -8,6 +8,8 @@ * */ +import 'package:cs_monero/src/deprecated/get_height_by_date.dart' + as cs_monero_deprecated; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -50,7 +52,7 @@ import 'sub_widgets/restore_from_date_picker.dart'; import 'sub_widgets/restore_options_next_button.dart'; import 'sub_widgets/restore_options_platform_layout.dart'; -import 'package:cs_monero/src/deprecated/get_height_by_date.dart' as cs_monero_deprecated; +final _pIsUsingDate = StateProvider.autoDispose((_) => true); class RestoreOptionsView extends ConsumerStatefulWidget { const RestoreOptionsView({ @@ -76,12 +78,11 @@ class _RestoreOptionsViewState extends ConsumerState { late TextEditingController _dateController; late TextEditingController _blockHeightController; late FocusNode _blockHeightFocusNode; - final ValueNotifier _isUsingDateNotifier = ValueNotifier(true); late FocusNode textFieldFocusNode; late final FocusNode passwordFocusNode; late final TextEditingController passwordController; - final bool _nextEnabled = true; + bool _hasBlockHeight = false; DateTime? _restoreFromDate; bool hidePassword = true; @@ -92,6 +93,7 @@ class _RestoreOptionsViewState extends ConsumerState { @override void initState() { + super.initState(); walletName = widget.walletName; coin = widget.coin; isDesktop = Util.isDesktop; @@ -102,9 +104,18 @@ class _RestoreOptionsViewState extends ConsumerState { passwordFocusNode = FocusNode(); _blockHeightController = TextEditingController(); _blockHeightFocusNode = FocusNode(); - _isUsingDateNotifier.value = true; - super.initState(); + _blockHeightController.addListener(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + if (!ref.read(_pIsUsingDate)) { + setState(() { + _hasBlockHeight = _blockHeightController.text.isNotEmpty; + }); + } + } + }); + }); } @override @@ -132,7 +143,7 @@ class _RestoreOptionsViewState extends ConsumerState { if (mounted) { int height = 0; - if (_isUsingDateNotifier.value) { + if (ref.read(_pIsUsingDate)) { height = getBlockHeightFromDate(_restoreFromDate); } else { height = int.tryParse(_blockHeightController.text) ?? 0; @@ -195,9 +206,7 @@ class _RestoreOptionsViewState extends ConsumerState { backgroundColor: Colors.transparent, context: context, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) { return MnemonicWordCountSelectSheet( @@ -212,22 +221,17 @@ class _RestoreOptionsViewState extends ConsumerState { int height = 0; if (date != null) { if (widget.coin is Monero) { - height = cs_monero_deprecated.getMoneroHeightByDate( - date: date, - ); + height = cs_monero_deprecated.getMoneroHeightByDate(date: date); } if (widget.coin is Wownero) { - height = cs_monero_deprecated.getWowneroHeightByDate( - date: date, - ); + height = cs_monero_deprecated.getWowneroHeightByDate(date: date); } if (height < 0) { height = 0; } if (widget.coin is Epiccash) { - final int secondsSinceEpoch = - date.millisecondsSinceEpoch ~/ 1000; + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; const int epicCashFirstBlock = 1565370278; const double overestimateSecondsPerBlock = 61; final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; @@ -235,7 +239,7 @@ class _RestoreOptionsViewState extends ConsumerState { chosenSeconds ~/ overestimateSecondsPerBlock; height = approximateHeight; - if (height < 0) { + if (height < 0) { height = 0; } } @@ -244,7 +248,10 @@ class _RestoreOptionsViewState extends ConsumerState { } return height; } catch (e) { - Logging.instance.log(Level.info, "Error getting block height from date: $e"); + Logging.instance.log( + Level.info, + "Error getting block height from date: $e", + ); return 0; } } @@ -257,25 +264,27 @@ class _RestoreOptionsViewState extends ConsumerState { return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () { - if (textFieldFocusNode.hasFocus) { - textFieldFocusNode.unfocus(); - Future.delayed(const Duration(milliseconds: 100)) - .then((value) => Navigator.of(context).pop()); - } else { - Navigator.of(context).pop(); - } - }, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + if (textFieldFocusNode.hasFocus) { + textFieldFocusNode.unfocus(); + Future.delayed( + const Duration(milliseconds: 100), + ).then((value) => Navigator.of(context).pop()); + } else { + Navigator.of(context).pop(); + } + }, + ), ), - ), body: RestoreOptionsPlatformLayout( isDesktop: isDesktop, child: ConstrainedBox( @@ -285,28 +294,18 @@ class _RestoreOptionsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Spacer( - flex: isDesktop ? 10 : 1, - ), - if (!isDesktop) - CoinImage( - coin: coin, - height: 100, - width: 100, - ), - SizedBox( - height: isDesktop ? 0 : 16, - ), + Spacer(flex: isDesktop ? 10 : 1), + if (!isDesktop) CoinImage(coin: coin, height: 100, width: 100), + SizedBox(height: isDesktop ? 0 : 16), Text( "Restore options", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 40 : 24, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 40 : 24), if (coin is ViewOnlyOptionCurrencyInterface) SizedBox( height: isDesktop ? 56 : 48, @@ -317,9 +316,10 @@ class _RestoreOptionsViewState extends ConsumerState { offText: "View Only", onColor: Theme.of(context).extension()!.popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + offColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, isOn: _showViewOnlyOption, onValueChanged: (value) { setState(() { @@ -335,51 +335,41 @@ class _RestoreOptionsViewState extends ConsumerState { ), ), if (coin is ViewOnlyOptionCurrencyInterface) - SizedBox( - height: isDesktop ? 40 : 24, - ), + SizedBox(height: isDesktop ? 40 : 24), _showViewOnlyOption ? ViewOnlyRestoreOption( - coin: coin, - dateController: _dateController, - dateChooserFunction: - isDesktop ? chooseDesktopDate : chooseDate, - blockHeightController: _blockHeightController, - blockHeightFocusNode: _blockHeightFocusNode, - isUsingDateNotifier: _isUsingDateNotifier, - ) + coin: coin, + dateController: _dateController, + dateChooserFunction: + isDesktop ? chooseDesktopDate : chooseDate, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + ) : SeedRestoreOption( - coin: coin, - dateController: _dateController, - blockHeightController: _blockHeightController, - blockHeightFocusNode: _blockHeightFocusNode, - isUsingDateNotifier: _isUsingDateNotifier, - pwController: passwordController, - pwFocusNode: passwordFocusNode, - supportsMnemonicPassphrase: supportsMnemonicPassphrase, - dateChooserFunction: - isDesktop ? chooseDesktopDate : chooseDate, - chooseMnemonicLength: chooseMnemonicLength, - lelScanChanged: (value) { - enableLelantusScanning = value; - }, - ), - if (!isDesktop) - const Spacer( - flex: 3, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), + coin: coin, + dateController: _dateController, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + pwController: passwordController, + pwFocusNode: passwordFocusNode, + supportsMnemonicPassphrase: supportsMnemonicPassphrase, + dateChooserFunction: + isDesktop ? chooseDesktopDate : chooseDate, + chooseMnemonicLength: chooseMnemonicLength, + lelScanChanged: (value) { + enableLelantusScanning = value; + }, + ), + if (!isDesktop) const Spacer(flex: 3), + SizedBox(height: isDesktop ? 32 : 12), RestoreOptionsNextButton( isDesktop: isDesktop, - onPressed: _nextEnabled ? nextPressed : null, + onPressed: + ref.watch(_pIsUsingDate) || _hasBlockHeight + ? nextPressed + : null, ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), @@ -395,7 +385,6 @@ class SeedRestoreOption extends ConsumerStatefulWidget { required this.dateController, required this.blockHeightController, required this.blockHeightFocusNode, - required this.isUsingDateNotifier, required this.pwController, required this.pwFocusNode, required this.supportsMnemonicPassphrase, @@ -408,7 +397,6 @@ class SeedRestoreOption extends ConsumerStatefulWidget { final TextEditingController dateController; final TextEditingController blockHeightController; final FocusNode blockHeightFocusNode; - final ValueNotifier isUsingDateNotifier; final TextEditingController pwController; final FocusNode pwFocusNode; final bool supportsMnemonicPassphrase; @@ -431,9 +419,11 @@ class _SeedRestoreOptionState extends ConsumerState { Widget build(BuildContext context) { final lengths = widget.coin.possibleMnemonicLengths; - final isMoneroAnd25 = widget.coin is Monero && + final isMoneroAnd25 = + widget.coin is Monero && ref.watch(mnemonicWordCountStateProvider.state).state == 25; - final isWowneroAnd25 = widget.coin is Wownero && + final isWowneroAnd25 = + widget.coin is Wownero && ref.watch(mnemonicWordCountStateProvider.state).state == 25; return Column( @@ -443,41 +433,37 @@ class _SeedRestoreOptionState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - widget.isUsingDateNotifier.value - ? "Choose start date" - : "Block height", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), + ref.watch(_pIsUsingDate) ? "Choose start date" : "Block height", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), CustomTextButton( - text: widget.isUsingDateNotifier.value - ? "Use block height" - : "Use date", - onTap: () { - setState(() { - widget.isUsingDateNotifier.value = - !widget.isUsingDateNotifier.value; - }); - }, + text: + ref.watch(_pIsUsingDate) ? "Use block height" : "Use date", + onTap: + () => + ref.read(_pIsUsingDate.notifier).state = + !ref.read(_pIsUsingDate), ), ], ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - widget.isUsingDateNotifier.value ? - RestoreFromDatePicker( - onTap: widget.dateChooserFunction, - controller: widget.dateController, - ) : - ClipRRect( + ref.watch(_pIsUsingDate) + ? RestoreFromDatePicker( + onTap: widget.dateChooserFunction, + controller: widget.dateController, + ) + : ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -485,15 +471,14 @@ class _SeedRestoreOptionState extends ConsumerState { focusNode: widget.blockHeightFocusNode, controller: widget.blockHeightController, keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], + inputFormatters: [FilteringTextInputFormatter.digitsOnly], textInputAction: TextInputAction.done, - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) - : STextStyles.field(context), + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), onChanged: (value) { setState(() { _blockFieldEmpty = value.isEmpty; @@ -505,17 +490,19 @@ class _SeedRestoreOptionState extends ConsumerState { context, ).copyWith( suffixIcon: UnconstrainedBox( - child: TextFieldIconButton(child: - Semantics( - label: "Clear Block Height Field Button. Clears the block height field", - excludeSemantics: true, - child: !_blockFieldEmpty - ? XIcon( - width: Util.isDesktop ? 24 : 16, - height: Util.isDesktop ? 24 : 16, - ) - : const SizedBox.shrink(), - ), + child: TextFieldIconButton( + child: Semantics( + label: + "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: + !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), onTap: () { widget.blockHeightController.text = ""; setState(() { @@ -528,44 +515,42 @@ class _SeedRestoreOptionState extends ConsumerState { ), ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) RoundedWhiteContainer( child: Center( child: Text( - widget.isUsingDateNotifier.value + ref.watch(_pIsUsingDate) ? "Choose the date you made the wallet (approximate is fine)" : "Enter the initial block height of the wallet", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.smallMed12(context).copyWith( - fontSize: 10, - ), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.smallMed12( + context, + ).copyWith(fontSize: 10), ), ), ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), Text( "Choose recovery phrase length", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), if (Util.isDesktop) DropdownButtonHideUnderline( child: DropdownButton2( @@ -592,28 +577,27 @@ class _SeedRestoreOptionState extends ConsumerState { Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), @@ -622,9 +606,7 @@ class _SeedRestoreOptionState extends ConsumerState { chooseMnemonicLength: widget.chooseMnemonicLength, ), if (widget.supportsMnemonicPassphrase) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), if (widget.supportsMnemonicPassphrase) Expandable( onExpandChanged: (state) { @@ -645,15 +627,17 @@ class _SeedRestoreOptionState extends ConsumerState { children: [ Text( "Advanced", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.smallMed12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), SvgPicture.asset( @@ -662,9 +646,10 @@ class _SeedRestoreOptionState extends ConsumerState { : Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ], ), @@ -685,10 +670,7 @@ class _SeedRestoreOptionState extends ConsumerState { widget.lelScanChanged(_enableLelantusScanning); }, ), - if (widget.coin is Firo) - const SizedBox( - height: 8, - ), + if (widget.coin is Firo) const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -697,11 +679,12 @@ class _SeedRestoreOptionState extends ConsumerState { key: const Key("mnemonicPassphraseFieldKey1"), focusNode: widget.pwFocusNode, controller: widget.pwController, - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) - : STextStyles.field(context), + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), obscureText: _hidePassword, enableSuggestions: false, autocorrect: false, @@ -713,15 +696,11 @@ class _SeedRestoreOptionState extends ConsumerState { suffixIcon: UnconstrainedBox( child: ConditionalParent( condition: Util.isDesktop, - builder: (child) => SizedBox( - height: 70, - child: child, - ), + builder: + (child) => SizedBox(height: 70, child: child), child: Row( children: [ - SizedBox( - width: Util.isDesktop ? 24 : 16, - ), + SizedBox(width: Util.isDesktop ? 24 : 16), GestureDetector( key: const Key( "mnemonicPassphraseFieldShowPasswordButtonKey", @@ -735,16 +714,15 @@ class _SeedRestoreOptionState extends ConsumerState { _hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: Util.isDesktop ? 24 : 16, height: Util.isDesktop ? 24 : 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -752,29 +730,28 @@ class _SeedRestoreOptionState extends ConsumerState { ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Center( child: Text( "If the recovery phrase you are about to restore " "was created with an optional BIP39 passphrase " "you can enter it here.", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.itemSubtitle(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.itemSubtitle(context), ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), @@ -791,7 +768,7 @@ class _SeedRestoreOptionState extends ConsumerState { } } -class ViewOnlyRestoreOption extends StatefulWidget { +class ViewOnlyRestoreOption extends ConsumerStatefulWidget { const ViewOnlyRestoreOption({ super.key, required this.coin, @@ -799,22 +776,21 @@ class ViewOnlyRestoreOption extends StatefulWidget { required this.dateChooserFunction, required this.blockHeightController, required this.blockHeightFocusNode, - required this.isUsingDateNotifier, }); final CryptoCurrency coin; final TextEditingController dateController; final TextEditingController blockHeightController; final FocusNode blockHeightFocusNode; - final ValueNotifier isUsingDateNotifier; final Future Function() dateChooserFunction; @override - State createState() => _ViewOnlyRestoreOptionState(); + ConsumerState createState() => + _ViewOnlyRestoreOptionState(); } -class _ViewOnlyRestoreOptionState extends State { +class _ViewOnlyRestoreOptionState extends ConsumerState { bool _blockFieldEmpty = true; @override @@ -827,118 +803,111 @@ class _ViewOnlyRestoreOptionState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - widget.isUsingDateNotifier.value - ? "Choose start date" - : "Block height", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), + ref.watch(_pIsUsingDate) ? "Choose start date" : "Block height", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), CustomTextButton( - text: widget.isUsingDateNotifier.value - ? "Use block height" - : "Use date", + text: + ref.watch(_pIsUsingDate) ? "Use block height" : "Use date", onTap: () { - setState(() { - widget.isUsingDateNotifier.value = - !widget.isUsingDateNotifier.value; - }); + ref.read(_pIsUsingDate.notifier).state = + !ref.read(_pIsUsingDate); }, ), ], ), + if (showDateOption) SizedBox(height: Util.isDesktop ? 16 : 8), if (showDateOption) - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), - if (showDateOption) - widget.isUsingDateNotifier.value + ref.watch(_pIsUsingDate) ? RestoreFromDatePicker( - onTap: widget.dateChooserFunction, - controller: widget.dateController, - ) - : ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - focusNode: widget.blockHeightFocusNode, - controller: widget.blockHeightController, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - textInputAction: TextInputAction.done, - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, + onTap: widget.dateChooserFunction, + controller: widget.dateController, ) - : STextStyles.field(context), - onChanged: (value) { - setState(() { - _blockFieldEmpty = value.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Start scanning from...", - widget.blockHeightFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: TextFieldIconButton( - child: Semantics( - label: - "Clear Block Height Field Button. Clears the block height field", - excludeSemantics: true, - child: !_blockFieldEmpty - ? XIcon( - width: Util.isDesktop ? 24 : 16, - height: Util.isDesktop ? 24 : 16, - ) - : const SizedBox.shrink(), + : ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: widget.blockHeightFocusNode, + controller: widget.blockHeightController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textInputAction: TextInputAction.done, + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), + onChanged: (value) { + setState(() { + _blockFieldEmpty = value.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Start scanning from...", + widget.blockHeightFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: TextFieldIconButton( + child: Semantics( + label: + "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: + !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), + onTap: () { + widget.blockHeightController.text = ""; + setState(() { + _blockFieldEmpty = true; + }); + }, + ), ), - onTap: () { - widget.blockHeightController.text = ""; - setState(() { - _blockFieldEmpty = true; - }); - }, ), ), ), - ), - ), - if (showDateOption) - const SizedBox( - height: 8, - ), + if (showDateOption) const SizedBox(height: 8), if (showDateOption) RoundedWhiteContainer( child: Center( child: Text( - widget.isUsingDateNotifier.value + ref.watch(_pIsUsingDate) ? "Choose the date you made the wallet (approximate is fine)" : "Enter the initial block height of the wallet", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.smallMed12(context).copyWith( - fontSize: 10, - ), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.smallMed12( + context, + ).copyWith(fontSize: 10), ), ), ), - if (showDateOption) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (showDateOption) SizedBox(height: Util.isDesktop ? 24 : 16), ], ); } From 64eea6a236b11499608cb838b1cb0dede33be4d0 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 16:59:50 -0600 Subject: [PATCH 4/6] monero/wownero seed offset option on restore --- .../restore_options_view.dart | 73 ++-- lib/wallets/wallet/impl/monero_wallet.dart | 41 +- lib/wallets/wallet/impl/wownero_wallet.dart | 41 +- .../intermediate/lib_monero_wallet.dart | 382 +++++++++--------- pubspec.lock | 4 +- scripts/app_config/templates/pubspec.template | 2 +- 6 files changed, 275 insertions(+), 268 deletions(-) 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 6d424a2c8..40607b103 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 @@ -86,8 +86,6 @@ class _RestoreOptionsViewState extends ConsumerState { DateTime? _restoreFromDate; bool hidePassword = true; - bool get supportsMnemonicPassphrase => coin.hasMnemonicPassphraseSupport; - bool enableLelantusScanning = false; bool get supportsLelantus => coin is Firo; @@ -352,7 +350,6 @@ class _RestoreOptionsViewState extends ConsumerState { blockHeightFocusNode: _blockHeightFocusNode, pwController: passwordController, pwFocusNode: passwordFocusNode, - supportsMnemonicPassphrase: supportsMnemonicPassphrase, dateChooserFunction: isDesktop ? chooseDesktopDate : chooseDate, chooseMnemonicLength: chooseMnemonicLength, @@ -387,7 +384,6 @@ class SeedRestoreOption extends ConsumerStatefulWidget { required this.blockHeightFocusNode, required this.pwController, required this.pwFocusNode, - required this.supportsMnemonicPassphrase, required this.dateChooserFunction, required this.chooseMnemonicLength, required this.lelScanChanged, @@ -399,7 +395,6 @@ class SeedRestoreOption extends ConsumerStatefulWidget { final FocusNode blockHeightFocusNode; final TextEditingController pwController; final FocusNode pwFocusNode; - final bool supportsMnemonicPassphrase; final Future Function() dateChooserFunction; final Future Function() chooseMnemonicLength; @@ -419,12 +414,21 @@ class _SeedRestoreOptionState extends ConsumerState { Widget build(BuildContext context) { final lengths = widget.coin.possibleMnemonicLengths; - final isMoneroAnd25 = - widget.coin is Monero && - ref.watch(mnemonicWordCountStateProvider.state).state == 25; - final isWowneroAnd25 = - widget.coin is Wownero && - ref.watch(mnemonicWordCountStateProvider.state).state == 25; + final currentLength = ref.watch(mnemonicWordCountStateProvider); + + final isMoneroAnd25 = widget.coin is Monero && currentLength == 25; + final isWowneroAnd25 = widget.coin is Wownero && currentLength == 25; + + final bool supportsPassphrase; + if (widget.coin.hasMnemonicPassphraseSupport) { + supportsPassphrase = true; + } else if (widget.coin is CryptonoteCurrency) { + // partial see offset support. Currently only on restore + // and not wownero 14 word seeds + supportsPassphrase = currentLength == 16 || currentLength == 25; + } else { + supportsPassphrase = false; + } return Column( children: [ @@ -573,14 +577,22 @@ class _SeedRestoreOptionState extends ConsumerState { }, isExpanded: true, iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + icon: ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.only(right: 10), + child: child, + ), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), ), dropdownStyleData: DropdownStyleData( @@ -605,9 +617,8 @@ class _SeedRestoreOptionState extends ConsumerState { MobileMnemonicLengthSelector( chooseMnemonicLength: widget.chooseMnemonicLength, ), - if (widget.supportsMnemonicPassphrase) - SizedBox(height: Util.isDesktop ? 24 : 16), - if (widget.supportsMnemonicPassphrase) + if (supportsPassphrase) SizedBox(height: Util.isDesktop ? 24 : 16), + if (supportsPassphrase) Expandable( onExpandChanged: (state) { setState(() { @@ -617,10 +628,11 @@ class _SeedRestoreOptionState extends ConsumerState { header: Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.only( + padding: EdgeInsets.only( top: 8.0, bottom: 8.0, right: 10, + left: Util.isDesktop ? 16 : 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -689,7 +701,9 @@ class _SeedRestoreOptionState extends ConsumerState { enableSuggestions: false, autocorrect: false, decoration: standardInputDecoration( - "BIP39 passphrase", + widget.coin is CryptonoteCurrency + ? "Seed Offset" + : "BIP39 passphrase", widget.pwFocusNode, context, ).copyWith( @@ -734,9 +748,14 @@ class _SeedRestoreOptionState extends ConsumerState { RoundedWhiteContainer( child: Center( child: Text( - "If the recovery phrase you are about to restore " - "was created with an optional BIP39 passphrase " - "you can enter it here.", + widget.coin is CryptonoteCurrency + ? "(Optional) An offset used to derive a different " + "wallet from the given mnemonic, allowing recovery " + "of a hidden or alternate wallet based on the same " + "seed phrase." + : "If the recovery phrase you are about to restore " + "was created with an optional BIP39 passphrase " + "you can enter it here.", style: Util.isDesktop ? STextStyles.desktopTextExtraSmall( diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index f1de036aa..a99cf30a3 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -9,18 +9,13 @@ import '../intermediate/lib_monero_wallet.dart'; class MoneroWallet extends LibMoneroWallet { MoneroWallet(CryptoCurrencyNetwork network) - : super( - Monero(network), - lib_monero_compat.WalletType.monero, - ); + : super(Monero(network), lib_monero_compat.WalletType.monero); @override Future estimateFeeFor(Amount amount, int feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { - return Amount.zeroWith( - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); } lib_monero.TransactionPriority priority; @@ -76,6 +71,7 @@ class MoneroWallet extends LibMoneroWallet { required String path, required String password, required int wordCount, + required String seedOffset, }) async { final lib_monero.MoneroSeedType type; switch (wordCount) { @@ -95,6 +91,7 @@ class MoneroWallet extends LibMoneroWallet { path: path, password: password, seedType: type, + seedOffset: seedOffset, ); } @@ -103,14 +100,15 @@ class MoneroWallet extends LibMoneroWallet { required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, - }) async => - await lib_monero.MoneroWallet.restoreWalletFromSeed( - path: path, - password: password, - seed: mnemonic, - restoreHeight: height, - ); + }) async => await lib_monero.MoneroWallet.restoreWalletFromSeed( + path: path, + password: password, + seed: mnemonic, + restoreHeight: height, + seedOffset: seedOffset, + ); @override Future getRestoredFromViewKeyWallet({ @@ -119,14 +117,13 @@ class MoneroWallet extends LibMoneroWallet { required String address, required String privateViewKey, int height = 0, - }) async => - lib_monero.MoneroWallet.createViewOnlyWallet( - path: path, - password: password, - address: address, - viewKey: privateViewKey, - restoreHeight: height, - ); + }) async => lib_monero.MoneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); @override void invalidSeedLengthCheck(int length) { diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index 81407f837..73b1c4f7b 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -11,18 +11,13 @@ import '../intermediate/lib_monero_wallet.dart'; class WowneroWallet extends LibMoneroWallet { WowneroWallet(CryptoCurrencyNetwork network) - : super( - Wownero(network), - lib_monero_compat.WalletType.wownero, - ); + : super(Wownero(network), lib_monero_compat.WalletType.wownero); @override Future estimateFeeFor(Amount amount, int feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { - return Amount.zeroWith( - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); } lib_monero.TransactionPriority priority; @@ -112,6 +107,7 @@ class WowneroWallet extends LibMoneroWallet { required String path, required String password, required int wordCount, + required String seedOffset, }) async { final lib_monero.WowneroSeedType type; switch (wordCount) { @@ -132,6 +128,7 @@ class WowneroWallet extends LibMoneroWallet { password: password, seedType: type, overrideDeprecated14WordSeedException: true, + seedOffset: seedOffset, ); } @@ -140,14 +137,15 @@ class WowneroWallet extends LibMoneroWallet { required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, - }) async => - await lib_monero.WowneroWallet.restoreWalletFromSeed( - path: path, - password: password, - seed: mnemonic, - restoreHeight: height, - ); + }) async => await lib_monero.WowneroWallet.restoreWalletFromSeed( + path: path, + password: password, + seed: mnemonic, + restoreHeight: height, + seedOffset: seedOffset, + ); @override Future getRestoredFromViewKeyWallet({ @@ -156,14 +154,13 @@ class WowneroWallet extends LibMoneroWallet { required String address, required String privateViewKey, int height = 0, - }) async => - lib_monero.WowneroWallet.createViewOnlyWallet( - path: path, - password: password, - address: address, - viewKey: privateViewKey, - restoreHeight: height, - ); + }) async => lib_monero.WowneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); @override void invalidSeedLengthCheck(int length) { diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index b6b30de34..55812fe19 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -51,33 +51,33 @@ abstract class LibMoneroWallet final bus = GlobalEventBus.instance; // Listen for tor status changes. - _torStatusListener = bus.on().listen( - (event) async { - switch (event.newStatus) { - case TorConnectionStatus.connecting: - if (!_torConnectingLock.isLocked) { - await _torConnectingLock.acquire(); - } - _requireMutex = true; - break; + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + _requireMutex = true; + break; - case TorConnectionStatus.connected: - case TorConnectionStatus.disconnected: - if (_torConnectingLock.isLocked) { - _torConnectingLock.release(); - } - _requireMutex = false; - break; - } - }, - ); + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + }); // Listen for tor preference changes. - _torPreferenceListener = bus.on().listen( - (event) async { - await updateNode(); - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + await updateNode(); + }); // Potentially dangerous hack. See comments in _startInit() _startInit(); @@ -93,17 +93,13 @@ abstract class LibMoneroWallet .walletIdEqualTo(walletId) .watch(fireImmediately: true) .listen((utxos) async { - try { - await onUTXOsChanged(utxos); - await updateBalance(shouldUpdateUtxos: false); - } catch (e, s) { - lib_monero.Logging.log?.i( - "_startInit", - error: e, - stackTrace: s, - ); - } - }); + try { + await onUTXOsChanged(utxos); + await updateBalance(shouldUpdateUtxos: false); + } catch (e, s) { + lib_monero.Logging.log?.i("_startInit", error: e, stackTrace: s); + } + }); }); } @@ -143,12 +139,14 @@ abstract class LibMoneroWallet required String path, required String password, required int wordCount, + required String seedOffset, }); Future getRestoredWallet({ required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, }); @@ -179,11 +177,7 @@ abstract class LibMoneroWallet onNewBlock: onNewBlock, onBalancesChanged: onBalancesChanged, onError: (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); }, ), ); @@ -197,16 +191,14 @@ abstract class LibMoneroWallet if (libMoneroWallet == null) { wasNull = true; // libMoneroWalletT?.close(); - final path = await pathForWallet( - name: walletId, - type: compatType, - ); + final path = await pathForWallet(name: walletId, type: compatType); final String password; try { - password = (await secureStorageInterface.read( - key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), - ))!; + password = + (await secureStorageInterface.read( + key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), + ))!; } catch (e, s) { throw Exception("Password not found $e, $s"); } @@ -247,9 +239,7 @@ abstract class LibMoneroWallet } @Deprecated("Only used in the case of older wallets") - lib_monero_compat.WalletInfo? getLibMoneroWalletInfo( - String walletId, - ) { + lib_monero_compat.WalletInfo? getLibMoneroWalletInfo(String walletId) { try { return DB.instance.moneroWalletInfoBox.values.firstWhere( (info) => info.id == lib_monero_compat.hiveIdFor(walletId, compatType), @@ -297,9 +287,7 @@ abstract class LibMoneroWallet Future getKeys() async { final base = libMoneroWallet; - final oldInfo = getLibMoneroWalletInfo( - walletId, - ); + final oldInfo = getLibMoneroWalletInfo(walletId); if (base == null || (oldInfo != null && oldInfo.name != walletId)) { return null; } @@ -324,13 +312,14 @@ abstract class LibMoneroWallet } Future<(String, String)> - hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async { + hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async { final path = await pathForWallet(name: walletId, type: compatType); final String password; try { - password = (await secureStorageInterface.read( - key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), - ))!; + password = + (await secureStorageInterface.read( + key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), + ))!; } catch (e, s) { throw Exception("Password not found $e, $s"); } @@ -341,10 +330,7 @@ abstract class LibMoneroWallet @override Future init({bool? isRestore, int? wordCount}) async { - final path = await pathForWallet( - name: walletId, - type: compatType, - ); + final path = await pathForWallet(name: walletId, type: compatType); if (!(walletExists(path)) && isRestore != true) { if (wordCount == null) { throw Exception("Missing word count for new xmr/wow wallet!"); @@ -359,6 +345,7 @@ abstract class LibMoneroWallet path: path, password: password, wordCount: wordCount, + seedOffset: "", // default for non restored wallets for now ); final height = wallet.getRefreshFromBlockHeight(); @@ -410,6 +397,7 @@ abstract class LibMoneroWallet await refreshMutex.protect(() async { final mnemonic = await getMnemonic(); + final seedOffset = await getMnemonicPassphrase(); final seedLength = mnemonic.trim().split(" ").length; invalidSeedLengthCheck(seedLength); @@ -424,10 +412,7 @@ abstract class LibMoneroWallet final String name = walletId; - final path = await pathForWallet( - name: name, - type: compatType, - ); + final path = await pathForWallet(name: name, type: compatType); try { final password = generatePassword(); @@ -440,6 +425,7 @@ abstract class LibMoneroWallet password: password, mnemonic: mnemonic, height: height, + seedOffset: seedOffset, ); if (libMoneroWallet != null) { @@ -450,7 +436,8 @@ abstract class LibMoneroWallet _setListener(); - final newReceivingAddress = await getCurrentReceivingAddress() ?? + final newReceivingAddress = + await getCurrentReceivingAddress() ?? Address( walletId: walletId, derivationIndex: 0, @@ -481,8 +468,11 @@ abstract class LibMoneroWallet libMoneroWallet?.startListeners(); libMoneroWallet?.startAutoSaving(); } catch (e, s) { - Logging.instance.e("Exception rethrown from recoverFromMnemonic(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from recoverFromMnemonic(): ", + error: e, + stackTrace: s, + ); rethrow; } }); @@ -551,8 +541,11 @@ abstract class LibMoneroWallet _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); } catch (e, s) { _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - Logging.instance.e("Exception caught in $runtimeType.updateNode(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, + ); } return; @@ -566,13 +559,14 @@ abstract class LibMoneroWallet return; } - final localTxids = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightGreaterThan(0) - .txidProperty() - .findAll(); + final localTxids = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightGreaterThan(0) + .txidProperty() + .findAll(); final allTxids = await base.getAllTxids(refresh: true); @@ -582,10 +576,7 @@ abstract class LibMoneroWallet return; } - final transactions = await base.getTxs( - txids: txidsToFetch, - refresh: false, - ); + final transactions = await base.getTxs(txids: txidsToFetch, refresh: false); final allOutputs = await base.getOutputs(includeSpent: true, refresh: true); @@ -673,14 +664,16 @@ abstract class LibMoneroWallet type: type, subType: TransactionSubType.none, otherData: jsonEncode({ - TxV2OdKeys.overrideFee: Amount( - rawValue: tx.fee, - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), - TxV2OdKeys.moneroAmount: Amount( - rawValue: tx.amount, - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + TxV2OdKeys.overrideFee: + Amount( + rawValue: tx.fee, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + TxV2OdKeys.moneroAmount: + Amount( + rawValue: tx.amount, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), TxV2OdKeys.moneroAccountIndex: tx.accountIndex, TxV2OdKeys.isMoneroTransaction: true, }), @@ -756,9 +749,10 @@ abstract class LibMoneroWallet Future pathForWallet({ required String name, required lib_monero_compat.WalletType type, - }) async => - await pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); + }) async => await pathForWalletDir( + name: name, + type: type, + ).then((path) => '$path/$name'); void onSyncingUpdate({ required int syncHeight, @@ -903,16 +897,10 @@ abstract class LibMoneroWallet } GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highest, - walletId, - ), + RefreshPercentChangedEvent(highest, walletId), ); GlobalEventBus.instance.fire( - BlocksRemainingEvent( - blocksLeft, - walletId, - ), + BlocksRemainingEvent(blocksLeft, walletId), ); } else if (_syncStatus is lib_monero_compat.SyncedSyncStatus) { status = WalletSyncStatus.synced; @@ -922,10 +910,7 @@ abstract class LibMoneroWallet } else if (_syncStatus is lib_monero_compat.StartingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.FailedSyncStatus) { status = WalletSyncStatus.unableToSync; @@ -933,18 +918,12 @@ abstract class LibMoneroWallet } else if (_syncStatus is lib_monero_compat.ConnectingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.ConnectedSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.LostConnectionSyncStatus) { status = WalletSyncStatus.unableToSync; @@ -953,11 +932,7 @@ abstract class LibMoneroWallet if (status != null) { GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - status, - walletId, - info.coin, - ), + WalletSyncStatusChangedEvent(status, walletId, info.coin), ); } } @@ -1019,24 +994,27 @@ abstract class LibMoneroWallet @override Future updateUTXOs() async { final List outputArray = []; - final utxos = await libMoneroWallet?.getOutputs(refresh: true) ?? + final utxos = + await libMoneroWallet?.getOutputs(refresh: true) ?? []; for (final rawUTXO in utxos) { if (!rawUTXO.spent) { - final current = await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .voutEqualTo(rawUTXO.vout) - .and() - .txidEqualTo(rawUTXO.hash) - .findFirst(); - final tx = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(rawUTXO.hash) - .findFirst(); + final current = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .voutEqualTo(rawUTXO.vout) + .and() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + final tx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(rawUTXO.hash) + .findFirst(); final otherDataMap = { UTXOOtherDataKeys.keyImage: rawUTXO.keyImage, @@ -1161,8 +1139,11 @@ abstract class LibMoneroWallet isar: mainDB.isar, ); } catch (e, s) { - Logging.instance - .e("Exception in generateNewAddress(): ", error: e, stackTrace: s); + Logging.instance.e( + "Exception in generateNewAddress(): ", + error: e, + stackTrace: s, + ); } } @@ -1173,9 +1154,10 @@ abstract class LibMoneroWallet throw Exception(); } catch (_, s) { Logging.instance.e( - "checkReceivingAddressForTransactions called but reuse address flag set: $s", - error: e, - stackTrace: s); + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + error: e, + stackTrace: s, + ); } } @@ -1185,9 +1167,10 @@ abstract class LibMoneroWallet if (entries != null) { for (final element in entries) { if (!element.isSpend) { - final int curAddressIndex = element.addressIndexes.isEmpty - ? 0 - : element.addressIndexes.reduce(max); + final int curAddressIndex = + element.addressIndexes.isEmpty + ? 0 + : element.addressIndexes.reduce(max); if (curAddressIndex > highestIndex) { highestIndex = curAddressIndex; } @@ -1206,11 +1189,12 @@ abstract class LibMoneroWallet // Use new index to derive a new receiving address final newReceivingAddress = addressFor(index: newReceivingIndex); - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); + final existing = + await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(newReceivingAddress.value) + .findFirst(); if (existing == null) { // Add that new change address await mainDB.putAddress(newReceivingAddress); @@ -1225,15 +1209,17 @@ abstract class LibMoneroWallet } } on SocketException catch (se, s) { Logging.instance.e( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - error: e, - stackTrace: s); + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + error: e, + stackTrace: s, + ); return; } catch (e, s) { Logging.instance.e( - "Exception rethrown from _checkReceivingAddressForTransactions(): ", - error: e, - stackTrace: s); + "Exception rethrown from _checkReceivingAddressForTransactions(): ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -1241,13 +1227,13 @@ abstract class LibMoneroWallet // TODO: this needs some work. Prio's may need to be changed as well as estimated blocks @override Future get fees async => FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: lib_monero.TransactionPriority.high.value, - medium: lib_monero.TransactionPriority.medium.value, - slow: lib_monero.TransactionPriority.normal.value, - ); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: lib_monero.TransactionPriority.high.value, + medium: lib_monero.TransactionPriority.medium.value, + slow: lib_monero.TransactionPriority.normal.value, + ); @override Future updateChainHeight() async { @@ -1321,25 +1307,27 @@ abstract class LibMoneroWallet } final height = await chainHeight; - final inputs = txData.utxos - ?.map( - (e) => lib_monero.Output( - address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, - isUnlocked: e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= - cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, - spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, - ), - ) - .toList(); + final inputs = + txData.utxos + ?.map( + (e) => lib_monero.Output( + address: e.address!, + hash: e.txid, + keyImage: e.keyImage!, + value: BigInt.from(e.value), + isFrozen: e.isBlocked, + isUnlocked: + e.blockHeight != null && + (height - (e.blockHeight ?? 0)) >= + cryptoCurrency.minConfirms, + height: e.blockHeight ?? 0, + vout: e.vout, + spent: e.used ?? false, + spentHeight: null, // doesn't matter here + coinbase: e.isCoinbase, + ), + ) + .toList(); return await prepareSendMutex.protect(() async { final lib_monero.PendingTransaction pendingTransaction; @@ -1380,8 +1368,11 @@ abstract class LibMoneroWallet throw ArgumentError("Invalid fee rate argument provided!"); } } catch (e, s) { - Logging.instance.i("Exception rethrown from prepare send(): ", - error: e, stackTrace: s); + Logging.instance.i( + "Exception rethrown from prepare send(): ", + error: e, + stackTrace: s, + ); if (e.toString().contains("Incorrect unlocked balance")) { throw Exception("Insufficient balance!"); @@ -1395,9 +1386,7 @@ abstract class LibMoneroWallet Future confirmSend({required TxData txData}) async { try { try { - await libMoneroWallet!.commitTx( - txData.pendingTransaction!, - ); + await libMoneroWallet!.commitTx(txData.pendingTransaction!); Logging.instance.d( "transaction ${txData.pendingTransaction!.txid} has been sent", @@ -1405,14 +1394,18 @@ abstract class LibMoneroWallet return txData.copyWith(txid: txData.pendingTransaction!.txid); } catch (e, s) { Logging.instance.e( - "${info.name} ${compatType.name.toLowerCase()} confirmSend: ", - error: e, - stackTrace: s); + "${info.name} ${compatType.name.toLowerCase()} confirmSend: ", + error: e, + stackTrace: s, + ); rethrow; } } catch (e, s) { - Logging.instance.e("Exception rethrown from confirmSend(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from confirmSend(): ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -1435,10 +1428,7 @@ abstract class LibMoneroWallet final String name = walletId; - final path = await pathForWallet( - name: name, - type: compatType, - ); + final path = await pathForWallet(name: name, type: compatType); final password = generatePassword(); await secureStorageInterface.write( @@ -1461,7 +1451,8 @@ abstract class LibMoneroWallet _setListener(); - final newReceivingAddress = await getCurrentReceivingAddress() ?? + final newReceivingAddress = + await getCurrentReceivingAddress() ?? Address( walletId: walletId, derivationIndex: 0, @@ -1488,8 +1479,11 @@ abstract class LibMoneroWallet libMoneroWallet?.startListeners(); libMoneroWallet?.startAutoSaving(); } catch (e, s) { - Logging.instance.e("Exception rethrown from recoverViewOnly(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from recoverViewOnly(): ", + error: e, + stackTrace: s, + ); rethrow; } }); diff --git a/pubspec.lock b/pubspec.lock index ffcc7e9a8..9d65621e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: cs_monero - sha256: ed81d9e74ea71a8b8b0bfed07a284e14b6e5f4d0dbde774735f9f0a9ab60b7fb + sha256: f48495ed6744a47598b36eaf28adc1e9e55f0d4ea3c18fe42eda0d3d8f714206 url: "https://pub.dev" source: hosted - version: "1.0.0-pre.2" + version: "1.0.0-pre.3" cs_monero_flutter_libs: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 9c6c48cb4..86b62194d 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -205,7 +205,7 @@ dependencies: blockchain_utils: ^3.3.0 on_chain: ^4.0.1 cbor: ^6.3.3 - cs_monero: 1.0.0-pre.2 + cs_monero: 1.0.0-pre.3 cs_monero_flutter_libs: 1.0.0-pre.0 monero_rpc: ^2.0.0 digest_auth: ^1.0.1 From e765175d81aa328f050a0046ebc69b41f7b6e8dc Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 18:31:19 -0600 Subject: [PATCH 5/6] parse port hack --- .../add_edit_node_view.dart | 1091 ++++++++--------- 1 file changed, 542 insertions(+), 549 deletions(-) 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 0f00f75db..4fdeec183 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 @@ -109,111 +109,107 @@ class _AddEditNodeViewState extends ConsumerState { context: context, useSafeArea: true, barrierDismissible: true, - builder: (_) => isDesktop - ? DesktopDialog( - maxWidth: 440, - maxHeight: 300, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 32, - ), - child: Row( + builder: + (_) => + isDesktop + ? DesktopDialog( + maxWidth: 440, + maxHeight: 300, + child: Column( children: [ - const SizedBox( - width: 32, - ), - Text( - "Server currently unreachable", - style: STextStyles.desktopH3(context), - ), - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 16, - bottom: 32, - ), - child: Column( - children: [ - const Spacer(), - Text( - "Would you like to save this node anyways?", - style: STextStyles.desktopTextMedium(context), - ), - const Spacer( - flex: 2, - ), - Row( + Padding( + padding: const EdgeInsets.only(top: 32), + child: Row( children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: - isDesktop ? ButtonHeight.l : null, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(false), - ), - ), - const SizedBox( - width: 16, + const SizedBox(width: 32), + Text( + "Server currently unreachable", + style: STextStyles.desktopH3(context), ), - Expanded( - child: PrimaryButton( - label: "Save", - buttonHeight: - isDesktop ? ButtonHeight.l : null, - onPressed: () => Navigator.of( + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: Column( + children: [ + const Spacer(), + Text( + "Would you like to save this node anyways?", + style: STextStyles.desktopTextMedium( context, - rootNavigator: true, - ).pop(true), + ), ), - ), - ], + const Spacer(flex: 2), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], + ), ), - ], + ), + ], + ), + ) + : StackDialog( + title: "Server currently unreachable", + message: "Would you like to save this node anyways?", + leftButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(false); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), ), ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(true); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text("Save", style: STextStyles.button(context)), + ), ), - ], - ), - ) - : StackDialog( - title: "Server currently unreachable", - message: "Would you like to save this node anyways?", - leftButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(true); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ), ).then((value) { if (value is bool && value) { shouldSave = true; @@ -239,9 +235,11 @@ class _AddEditNodeViewState extends ConsumerState { } } - final torEnabled = formData.netOption == TorPlainNetworkOption.tor || + final torEnabled = + formData.netOption == TorPlainNetworkOption.tor || formData.netOption == TorPlainNetworkOption.both; - final plainEnabled = formData.netOption == TorPlainNetworkOption.clear || + final plainEnabled = + formData.netOption == TorPlainNetworkOption.clear || formData.netOption == TorPlainNetworkOption.both; switch (viewType) { @@ -262,15 +260,14 @@ class _AddEditNodeViewState extends ConsumerState { clearnetEnabled: plainEnabled, ); - await ref.read(nodeServiceChangeNotifierProvider).add( - node, - formData.password, - true, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .add(node, formData.password, true); await _notifyWalletsOfUpdatedNode(); if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); } break; case AddEditNodeViewType.edit: @@ -290,23 +287,24 @@ class _AddEditNodeViewState extends ConsumerState { clearnetEnabled: plainEnabled, ); - await ref.read(nodeServiceChangeNotifierProvider).add( - node, - formData.password, - true, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .add(node, formData.password, true); await _notifyWalletsOfUpdatedNode(); if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); } break; } } Future _notifyWalletsOfUpdatedNode() async { - final wallets = - ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin); + final wallets = ref + .read(pWallets) + .wallets + .where((e) => e.info.coin == widget.coin); final prefs = ref.read(prefsChangeNotifierProvider); switch (prefs.syncType) { @@ -356,24 +354,26 @@ class _AddEditNodeViewState extends ConsumerState { try { await _processQrData(qrResult); } catch (e, s) { - Logging.instance.e("Error processing QR code data: ", - error: e, stackTrace: s); + Logging.instance.e( + "Error processing QR code data: ", + error: e, + stackTrace: s, + ); } } } catch (e, s) { - Logging.instance.e("Error opening QR code scanner dialog: ", - error: e, stackTrace: s); + Logging.instance.e( + "Error opening QR code scanner dialog: ", + error: e, + stackTrace: s, + ); } } else { try { final result = await BarcodeScanner.scan(); await _processQrData(result.rawContent); } catch (e, s) { - Logging.instance.e( - "$runtimeType._scanQr()", - error: e, - stackTrace: s, - ); + Logging.instance.e("$runtimeType._scanQr()", error: e, stackTrace: s); } } } finally { @@ -401,16 +401,12 @@ class _AddEditNodeViewState extends ConsumerState { clearnetEnabled: !nodeQrData.host.endsWith(".onion"), loginName: (nodeQrData as LibMoneroNodeQrData?)?.user, ), - (nodeQrData as LibMoneroNodeQrData?)?.password ?? "" + (nodeQrData as LibMoneroNodeQrData?)?.password ?? "", ); }); } } catch (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } } @@ -446,201 +442,202 @@ class _AddEditNodeViewState extends ConsumerState { final NodeModel? node = viewType == AddEditNodeViewType.edit && nodeId != null ? ref.watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodeById(id: nodeId!)), - ) + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodeById(id: nodeId!), + ), + ) : null; return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - 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 (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (viewType == AddEditNodeViewType.add && - coin - is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("qrNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: QrCodeIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + builder: + (child) => Background( + 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 (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + viewType == AddEditNodeViewType.edit + ? "Edit node" + : "Add node", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (viewType == AddEditNodeViewType.add && + coin + is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("qrNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: QrCodeIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: _scanQr, + ), ), - onPressed: _scanQr, ), - ), - ), - if (viewType == AddEditNodeViewType.edit && - ref - .watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin)), - ) - .length > - 1) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("deleteNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: SvgPicture.asset( - Assets.svg.trash, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + if (viewType == AddEditNodeViewType.edit && + ref + .watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodesFor(coin), + ), + ) + .length > + 1) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, ), - onPressed: () async { - Navigator.popUntil( - context, - ModalRoute.withName(widget.routeOnSuccessOrDelete), - ); - - await ref - .read(nodeServiceChangeNotifierProvider) - .delete( - nodeId!, - true, + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + Navigator.popUntil( + context, + ModalRoute.withName( + widget.routeOnSuccessOrDelete, + ), ); - }, + + await ref + .read(nodeServiceChangeNotifierProvider) + .delete(nodeId!, true); + }, + ), + ), ), - ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + bottom: 12, ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - bottom: 12, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: child, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 8, + ), + child: IntrinsicHeight(child: child), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 8, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 8), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SizedBox( - width: 8, - ), - const AppBarBackButton( - iconSize: 24, - size: 40, - ), - Text( - "Add new node", - style: STextStyles.desktopH3(context), + Row( + children: [ + const SizedBox(width: 8), + const AppBarBackButton(iconSize: 24, size: 40), + Text( + "Add new node", + style: STextStyles.desktopH3(context), + ), + ], ), + if (coin + is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future + Padding( + padding: const EdgeInsets.only(right: 32), + child: AppBarIconButton( + size: 40, + color: + isDesktop + ? Theme.of(context) + .extension()! + .textFieldDefaultBG + : Theme.of( + context, + ).extension()!.background, + icon: const QrCodeIcon(width: 21, height: 21), + onPressed: _scanQr, + ), + ), ], ), - if (coin - is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future - Padding( - padding: const EdgeInsets.only(right: 32), - child: AppBarIconButton( - size: 40, - color: isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultBG - : Theme.of(context) - .extension()! - .background, - icon: const QrCodeIcon( - width: 21, - height: 21, - ), - onPressed: _scanQr, - ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, ), + child: child, + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 16, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -670,10 +667,7 @@ class _AddEditNodeViewState extends ConsumerState { }, ), if (!isDesktop) const Spacer(), - if (isDesktop) - const SizedBox( - height: 78, - ), + if (isDesktop) const SizedBox(height: 78), Row( children: [ Expanded( @@ -681,42 +675,40 @@ class _AddEditNodeViewState extends ConsumerState { label: "Test connection", enabled: testConnectionEnabled, buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: testConnectionEnabled - ? () async { - final testPassed = await testNodeConnection( - context: context, - onSuccess: _onTestSuccess, - cryptoCurrency: coin, - nodeFormData: ref.read(nodeFormDataProvider), - ref: ref, - ); - if (context.mounted) { - if (testPassed) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Server ping success", - context: context, - ), - ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Server unreachable", - context: context, - ), - ); + onPressed: + testConnectionEnabled + ? () async { + final testPassed = await testNodeConnection( + context: context, + onSuccess: _onTestSuccess, + cryptoCurrency: coin, + nodeFormData: ref.read(nodeFormDataProvider), + ref: ref, + ); + if (context.mounted) { + if (testPassed) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Server ping success", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Server unreachable", + context: context, + ), + ); + } } } - } - : null, + : null, ), ), - if (isDesktop) - const SizedBox( - width: 16, - ), + if (isDesktop) const SizedBox(width: 16), if (isDesktop) Expanded( child: PrimaryButton( @@ -728,24 +720,19 @@ class _AddEditNodeViewState extends ConsumerState { ), ], ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) TextButton( - style: saveEnabled - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + style: + saveEnabled + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), onPressed: saveEnabled ? attemptSave : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), + child: Text("Save", style: STextStyles.button(context)), ), ], ), @@ -965,24 +952,25 @@ class _NodeFormState extends ConsumerState { _nameFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _nameController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _nameController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { _updateState(); @@ -990,9 +978,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1011,26 +997,59 @@ class _NodeFormState extends ConsumerState { _hostFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _hostController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _hostController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { + // parse port hack + try { + final uri = Uri.parse(newValue); + final port = uri.hasPort ? uri.port : 0; + if (port != 0) { + _portController.text = port.toString(); + final noPortUri = Uri( + scheme: uri.scheme, + userInfo: uri.userInfo, + host: uri.host, + path: uri.path, + query: uri.hasQuery ? uri.query : null, + fragment: uri.fragment.isNotEmpty ? uri.fragment : null, + ); + _hostController.text = noPortUri.toString(); + } + } catch (_) { + if (newValue.contains(":")) { + final parts = newValue.split(":"); + if (parts.isNotEmpty) { + final maybePort = int.tryParse(parts.last); + if (maybePort != null) { + _portController.text = maybePort.toString(); + _hostController.text = newValue.substring( + 0, + newValue.lastIndexOf(":"), + ); + } + } + } + } + if (widget.coin is Epiccash) { if (newValue.startsWith("https://")) { _useSSL = true; @@ -1055,9 +1074,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1078,24 +1095,25 @@ class _NodeFormState extends ConsumerState { _portFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _portController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _portController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { _updateState(); @@ -1103,9 +1121,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (enableAuthFields) ClipRRect( borderRadius: BorderRadius.circular( @@ -1127,21 +1143,21 @@ class _NodeFormState extends ConsumerState { suffixIcon: !shouldBeReadOnly && _usernameController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _usernameController.text = ""; - _updateState(); - }, - ), - ], - ), + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _usernameController.text = ""; + _updateState(); + }, + ), + ], ), - ) + ), + ) : null, ), onChanged: (newValue) { @@ -1150,10 +1166,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - if (enableAuthFields) - const SizedBox( - height: 8, - ), + if (enableAuthFields) const SizedBox(height: 8), if (enableAuthFields) ClipRRect( borderRadius: BorderRadius.circular( @@ -1176,21 +1189,21 @@ class _NodeFormState extends ConsumerState { suffixIcon: !shouldBeReadOnly && _passwordController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _passwordController.text = ""; - _updateState(); - }, - ), - ], - ), + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _passwordController.text = ""; + _updateState(); + }, + ), + ], ), - ) + ), + ) : null, ), onChanged: (newValue) { @@ -1199,22 +1212,20 @@ class _NodeFormState extends ConsumerState { }, ), ), - if (enableAuthFields) - const SizedBox( - height: 8, - ), + if (enableAuthFields) const SizedBox(height: 8), if (widget.coin is! CryptonoteCurrency) Row( children: [ GestureDetector( - onTap: !shouldBeReadOnly && enableSSLCheckbox - ? () { - setState(() { - _useSSL = !_useSSL; - }); - _updateState(); - } - : null, + onTap: + !shouldBeReadOnly && enableSSLCheckbox + ? () { + setState(() { + _useSSL = !_useSSL; + }); + _updateState(); + } + : null, child: Container( color: Colors.transparent, child: Row( @@ -1223,29 +1234,29 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !shouldBeReadOnly && enableSSLCheckbox - ? null - : MaterialStateProperty.all( - Theme.of(context) - .extension()! - .checkboxBGDisabled, - ), + fillColor: + !shouldBeReadOnly && enableSSLCheckbox + ? null + : MaterialStateProperty.all( + Theme.of(context) + .extension()! + .checkboxBGDisabled, + ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _useSSL, - onChanged: !shouldBeReadOnly && enableSSLCheckbox - ? (newValue) { - setState(() { - _useSSL = newValue!; - }); - _updateState(); - } - : null, + onChanged: + !shouldBeReadOnly && enableSSLCheckbox + ? (newValue) { + setState(() { + _useSSL = newValue!; + }); + _updateState(); + } + : null, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Use SSL", style: STextStyles.itemSubtitle12(context), @@ -1260,14 +1271,15 @@ class _NodeFormState extends ConsumerState { Row( children: [ GestureDetector( - onTap: !widget.readOnly /*&& trustedCheckbox*/ - ? () { - setState(() { - _trusted = !_trusted; - }); - _updateState(); - } - : null, + onTap: + !widget.readOnly /*&& trustedCheckbox*/ + ? () { + setState(() { + _trusted = !_trusted; + }); + _updateState(); + } + : null, child: Container( color: Colors.transparent, child: Row( @@ -1276,29 +1288,29 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !widget.readOnly - ? null - : MaterialStateProperty.all( - Theme.of(context) - .extension()! - .checkboxBGDisabled, - ), + fillColor: + !widget.readOnly + ? null + : MaterialStateProperty.all( + Theme.of(context) + .extension()! + .checkboxBGDisabled, + ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _trusted, - onChanged: !widget.readOnly - ? (newValue) { - setState(() { - _trusted = newValue!; - }); - _updateState(); - } - : null, + onChanged: + !widget.readOnly + ? (newValue) { + setState(() { + _trusted = newValue!; + }); + _updateState(); + } + : null, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Trusted", style: STextStyles.itemSubtitle12(context), @@ -1310,9 +1322,7 @@ class _NodeFormState extends ConsumerState { ], ), if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) Row( children: [ @@ -1322,7 +1332,9 @@ class _NodeFormState extends ConsumerState { _isFailover = !_isFailover; }); if (widget.readOnly) { - ref.read(nodeServiceChangeNotifierProvider).edit( + ref + .read(nodeServiceChangeNotifierProvider) + .edit( widget.node!.copyWith( isFailover: _isFailover, loginName: widget.node!.loginName, @@ -1351,7 +1363,9 @@ class _NodeFormState extends ConsumerState { _isFailover = newValue!; }); if (widget.readOnly) { - ref.read(nodeServiceChangeNotifierProvider).edit( + ref + .read(nodeServiceChangeNotifierProvider) + .edit( widget.node!.copyWith( isFailover: _isFailover, loginName: widget.node!.loginName, @@ -1366,9 +1380,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Use as failover", style: STextStyles.itemSubtitle12(context), @@ -1379,10 +1391,7 @@ class _NodeFormState extends ConsumerState { ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 16, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 16), if (widget.coin is! Ethereum) Row( children: [ @@ -1393,19 +1402,14 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.tor, - ); + setState(() => netOption = TorPlainNetworkOption.tor); _updateState(); } }, ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 8, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 8), if (widget.coin is! Ethereum) Row( children: [ @@ -1416,19 +1420,14 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.clear, - ); + setState(() => netOption = TorPlainNetworkOption.clear); _updateState(); } }, ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 8, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 8), if (widget.coin is! Ethereum) Row( children: [ @@ -1439,9 +1438,7 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.both, - ); + setState(() => netOption = TorPlainNetworkOption.both); _updateState(); } }, @@ -1473,10 +1470,9 @@ class RadioTextButton extends StatelessWidget { Widget build(BuildContext context) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: () { if (value != groupValue) { @@ -1493,27 +1489,24 @@ class RadioTextButton extends StatelessWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of( + context, + ).extension()!.radioButtonIconEnabled, value: value, groupValue: groupValue, - onChanged: !enabled - ? null - : (_) { - if (value != groupValue) { - onChanged.call(value); - } - }, + onChanged: + !enabled + ? null + : (_) { + if (value != groupValue) { + onChanged.call(value); + } + }, ), ), - const SizedBox( - width: 14, - ), - Text( - label, - style: STextStyles.w500_14(context), - ), + const SizedBox(width: 14), + Text(label, style: STextStyles.w500_14(context)), ], ), ), From 50ff74ee5303620ddd53f1835549160fe558ce0f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 25 Apr 2025 18:37:26 -0600 Subject: [PATCH 6/6] add option to node model --- lib/models/node_model.dart | 6 ++++++ lib/models/type_adaptors/node_model.g.dart | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 1a27d1345..530c2a0c2 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -43,6 +43,8 @@ class NodeModel { final bool torEnabled; // @HiveField(12) final bool clearnetEnabled; + // @HiveField(13) + final bool forceNoTor; NodeModel({ required this.host, @@ -56,6 +58,7 @@ class NodeModel { required this.isDown, required this.torEnabled, required this.clearnetEnabled, + this.forceNoTor = false, this.loginName, this.trusted, }); @@ -72,6 +75,7 @@ class NodeModel { bool? isDown, required bool? trusted, bool? torEnabled, + bool? forceNoTor, bool? clearnetEnabled, }) { return NodeModel( @@ -88,6 +92,7 @@ class NodeModel { trusted: trusted, torEnabled: torEnabled ?? this.torEnabled, clearnetEnabled: clearnetEnabled ?? this.clearnetEnabled, + forceNoTor: forceNoTor ?? this.forceNoTor, ); } @@ -111,6 +116,7 @@ class NodeModel { map['trusted'] = trusted; map['torEnabled'] = torEnabled; map['clearEnabled'] = clearnetEnabled; + map['forceNoTor'] = forceNoTor; return map; } diff --git a/lib/models/type_adaptors/node_model.g.dart b/lib/models/type_adaptors/node_model.g.dart index 32490fd50..381091edd 100644 --- a/lib/models/type_adaptors/node_model.g.dart +++ b/lib/models/type_adaptors/node_model.g.dart @@ -30,13 +30,14 @@ class NodeModelAdapter extends TypeAdapter { trusted: fields[10] as bool?, torEnabled: fields[11] as bool? ?? true, clearnetEnabled: fields[12] as bool? ?? true, + forceNoTor: fields[13] as bool? ?? false, ); } @override void write(BinaryWriter writer, NodeModel obj) { writer - ..writeByte(13) + ..writeByte(14) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -62,7 +63,9 @@ class NodeModelAdapter extends TypeAdapter { ..writeByte(11) ..write(obj.torEnabled) ..writeByte(12) - ..write(obj.clearnetEnabled); + ..write(obj.clearnetEnabled) + ..writeByte(13) + ..write(obj.forceNoTor); } @override