From 7540e593a3f9066cab2ab8e0f197c8eccda07d5c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 31 Oct 2022 12:03:21 -0600 Subject: [PATCH] desktop receiving popups --- assets/svg/arrow-down.svg | 4 + .../generate_receiving_uri_qr_code_view.dart | 771 +++++++++++------- .../sub_widgets/desktop_receive.dart | 67 +- lib/route_generator.dart | 16 + lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 6 files changed, 545 insertions(+), 315 deletions(-) create mode 100644 assets/svg/arrow-down.svg diff --git a/assets/svg/arrow-down.svg b/assets/svg/arrow-down.svg new file mode 100644 index 000000000..c96e43ce3 --- /dev/null +++ b/assets/svg/arrow-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 3e29612d1..4c3c4c968 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -20,7 +20,10 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -51,6 +54,10 @@ class _GenerateUriQrCodeViewState extends State { late TextEditingController amountController; late TextEditingController noteController; + late final bool isDesktop; + late String _uriString; + bool didGenerate = false; + final _amountFocusNode = FocusNode(); final _noteFocusNode = FocusNode(); @@ -81,8 +88,151 @@ class _GenerateUriQrCodeViewState extends State { } } + String? _generateURI() { + final amountString = amountController.text; + final noteString = noteController.text; + + if (amountString.isNotEmpty && Decimal.tryParse(amountString) == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid amount", + context: context, + ); + return null; + } + + String query = ""; + + if (amountString.isNotEmpty) { + query += "amount=$amountString"; + } + if (noteString.isNotEmpty) { + if (query.isNotEmpty) { + query += "&"; + } + query += "message=$noteString"; + } + + final uri = Uri( + scheme: widget.coin.uriScheme, + host: widget.receivingAddress, + query: query.isNotEmpty ? query : null, + ); + + final uriString = uri.toString().replaceFirst("://", ":"); + + Logging.instance.log("Generated receiving QR code for: $uriString", + level: LogLevel.Info); + + return uriString; + } + + void onGeneratePressed() { + final uriString = _generateURI(); + + if (uriString == null) { + return; + } + + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "New QR code", + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: uriString, + size: width, + backgroundColor: + Theme.of(context).extension()!.popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // TODO: add save button as well + await _capturePng(true); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + ), + ), + const SizedBox( + width: 4, + ), + Column( + children: [ + Text( + "Share", + textAlign: TextAlign.center, + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + const SizedBox( + height: 2, + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + @override void initState() { + isDesktop = Util.isDesktop; + _uriString = Uri( + scheme: widget.coin.uriScheme, + host: widget.receivingAddress, + ).toString().replaceFirst("://", ":"); amountController = TextEditingController(); noteController = TextEditingController(); super.initState(); @@ -101,315 +251,330 @@ class _GenerateUriQrCodeViewState extends State { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + + return ConditionalParent( + condition: !isDesktop, + builder: (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: 70)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Generate QR code", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Generate QR code", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (buildContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "The new QR code with your address, amount and note will appear in the pop up window.", - style: STextStyles.itemSubtitle(context), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Amount (Optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: amountController, - focusNode: _amountFocusNode, - style: STextStyles.field(context), - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Amount", - _amountFocusNode, - context, - ).copyWith( - suffixIcon: amountController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - amountController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Note (Optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Note", - _noteFocusNode, - context, - ).copyWith( - 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, - ), - ), - ), - // SizedBox() - // Spacer(), - const SizedBox( - height: 8, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - final amountString = amountController.text; - final noteString = noteController.text; - - if (amountString.isNotEmpty && - Decimal.tryParse(amountString) == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Invalid amount", - context: context, - ); - return; - } - - String query = ""; - - if (amountString.isNotEmpty) { - query += "amount=$amountString"; - } - if (noteString.isNotEmpty) { - if (query.isNotEmpty) { - query += "&"; - } - query += "message=$noteString"; - } - - final uri = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - query: query.isNotEmpty ? query : null, - ); - - final uriString = - uri.toString().replaceFirst("://", ":"); - - Logging.instance.log( - "Generated receiving QR code for: $uriString", - level: LogLevel.Info); - - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = - MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "New QR code", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImage( - data: uriString, - size: width, - backgroundColor: Theme.of( - context) - .extension()! - .popupBG, - foregroundColor: Theme.of( - context) - .extension()! - .accentColorDark), - ), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // TODO: add save button as well - await _capturePng(true); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor( - context), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Center( - child: SvgPicture.asset( - Assets.svg.share, - width: 14, - height: 14, - ), - ), - const SizedBox( - width: 4, - ), - Column( - children: [ - Text( - "Share", - textAlign: - TextAlign.center, - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - const SizedBox( - height: 2, - ), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, - child: Text( - "Generate QR Code", - style: STextStyles.button(context), - ), - ), - ], + body: LayoutBuilder( + builder: (buildContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + top: 12, + left: 32, + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + RoundedWhiteContainer( + child: Text( + "The new QR code with your address, amount and note will appear in the pop up window.", + style: STextStyles.itemSubtitle(context), + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + Text( + "Amount (Optional)", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - ); - }, + SizedBox( + height: isDesktop ? 10 : 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: amountController, + focusNode: _amountFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Amount", + _amountFocusNode, + context, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: amountController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + amountController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + Text( + "Note (Optional)", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Note", + _noteFocusNode, + context, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + 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, + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 8, + ), + PrimaryButton( + label: "Generate QR code", + onPressed: isDesktop + ? () { + final uriString = _generateURI(); + if (uriString == null) { + return; + } + + setState(() { + didGenerate = true; + _uriString = uriString; + }); + } + : onGeneratePressed, + desktopMed: true, + ), + if (isDesktop && didGenerate) + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .background, + child: Column( + children: [ + Text( + "New QR Code", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: SizedBox( + width: 234, + height: 234, + child: QrImage( + data: _uriString, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SecondaryButton( + width: 170, + desktopMed: true, + onPressed: () async { + await _capturePng(false); + }, + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + const SizedBox( + width: 16, + ), + PrimaryButton( + width: 170, + desktopMed: true, + onPressed: () async { + // TODO: add save functionality instead of share + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ], + ), + ], + ), ), ); } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 46b4cfcfc..9a59c3ec1 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -16,9 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class DesktopReceive extends ConsumerStatefulWidget { const DesktopReceive({ @@ -216,20 +220,59 @@ class _DesktopReceiveState extends ConsumerState { // TODO: create transparent button class to account for hover GestureDetector( onTap: () async { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: receivingAddress, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const AppBarBackButton( + size: 40, + iconSize: 24, + ), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], + ), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, receivingAddress), + ), + ), + ], + ), + ), + ], ), ), - ), - ); + ); + } else { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + ), + ); + } }, child: Container( color: Colors.transparent, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5d0f81e20..91d1a3768 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -37,6 +37,7 @@ import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; +import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; @@ -955,6 +956,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case GenerateUriQrCodeView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: return getRoute( diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index c2d488305..432ebbec9 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -145,6 +145,7 @@ class _SVG { String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg"; String get keys => "assets/svg/keys.svg"; + String get arrowDown => "assets/svg/arrow-down.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 88951b29b..cc0138b75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -310,6 +310,7 @@ flutter: - assets/svg/wallet-desktop.svg - assets/svg/exit-desktop.svg - assets/svg/keys.svg + - assets/svg/arrow-down.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg