diff --git a/lib/pages/settings_views/global_settings_view/xpub_view.dart b/lib/pages/settings_views/global_settings_view/xpub_view.dart new file mode 100644 index 000000000..fce352037 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/xpub_view.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class XPubView extends ConsumerStatefulWidget { + const XPubView({ + Key? key, + this.xpub, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final String? xpub; + final ClipboardInterface clipboardInterface; + + static const String routeName = "/xpub"; + + @override + ConsumerState createState() => _XPubViewState(); +} + +class _XPubViewState extends ConsumerState { + late ClipboardInterface _clipboardInterface; + + @override + void initState() { + _clipboardInterface = widget.clipboardInterface; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Future _copy() async { + await _clipboardInterface.setData(ClipboardData(text: widget.xpub)); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + } + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet xPub", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: + Theme.of(context).extension()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () async { + await _copy(); + }, + ), + ), + ), + ]), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: Column(children: [ + if (widget.xpub != null) + RoundedWhiteContainer( + padding: const EdgeInsets.all(12), + child: QrImage(data: widget.xpub!), + onPressed: () => _copy(), + ), + if (widget.xpub != null) + const SizedBox( + height: 8, + ), + if (widget.xpub != null) + RoundedWhiteContainer( + padding: const EdgeInsets.all(12), + child: Text(widget.xpub!, + style: STextStyles.largeMedium14(context)), + onPressed: () => _copy(), + ) + ]), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 5d78e5de6..ca48c8b6b 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,6 +12,7 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; @@ -57,6 +60,8 @@ class WalletSettingsView extends StatefulWidget { class _WalletSettingsViewState extends State { late final String walletId; late final Coin coin; + late String xpub; + late final bool xPubEnabled; late final EventBus eventBus; @@ -70,6 +75,9 @@ class _WalletSettingsViewState extends State { void initState() { walletId = widget.walletId; coin = widget.coin; + xPubEnabled = + coin != Coin.monero && coin != Coin.wownero && coin != Coin.epicCash; + xpub = ""; _currentSyncStatus = widget.initialSyncStatus; // _currentNodeStatus = widget.initialNodeStatus; @@ -270,6 +278,37 @@ class _WalletSettingsViewState extends State { SyncingPreferencesView.routeName); }, ), + if (xPubEnabled) + const SizedBox( + height: 8, + ), + if (xPubEnabled) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Wallet xPub", + onPressed: () async { + final List mnemonic = await ref + .read( + walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + final seed = bip39.mnemonicToSeed( + mnemonic.join(' ')); + final node = + bip32.BIP32.fromSeed(seed); + final xpub = + node.neutered().toBase58(); + + Navigator.of(context).pushNamed( + XPubView.routeName, + arguments: xpub); + }, + ); + }, + ), const SizedBox( height: 8, ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 8d12df5a6..f586eaa80 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -38,6 +38,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -71,6 +72,7 @@ class WalletView extends ConsumerStatefulWidget { required this.walletId, required this.managerProvider, this.eventBus, + this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); static const String routeName = "/wallet"; @@ -80,6 +82,8 @@ class WalletView extends ConsumerStatefulWidget { final ChangeNotifierProvider managerProvider; final EventBus? eventBus; + final ClipboardInterface clipboardInterface; + @override ConsumerState createState() => _WalletViewState(); } @@ -99,10 +103,13 @@ class _WalletViewState extends ConsumerState { bool _rescanningOnOpen = false; + late ClipboardInterface _clipboardInterface; + @override void initState() { walletId = widget.walletId; managerProvider = widget.managerProvider; + _clipboardInterface = widget.clipboardInterface; ref.read(managerProvider).isActiveWallet = true; if (!ref.read(managerProvider).shouldAutoSync) { @@ -259,7 +266,7 @@ class _WalletViewState extends ConsumerState { } void _onExchangePressed(BuildContext context) async { - final coin = ref.read(managerProvider).coin; + final Coin coin = ref.read(managerProvider).coin; if (coin.isTestNet) { await showDialog( @@ -384,7 +391,7 @@ class _WalletViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final coin = ref.watch(managerProvider.select((value) => value.coin)); + final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); return ConditionalParent( condition: _rescanningOnOpen, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_show_xpub_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_show_xpub_dialog.dart new file mode 100644 index 000000000..905d1a616 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_show_xpub_dialog.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopShowXpubDialog extends ConsumerStatefulWidget { + const DesktopShowXpubDialog({ + Key? key, + required this.xpub, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final String xpub; + + final ClipboardInterface clipboardInterface; + + static const String routeName = "/desktopShowXpubDialog"; + + @override + ConsumerState createState() => + _DesktopShowXpubDialog(); +} + +class _DesktopShowXpubDialog extends ConsumerState { + late ClipboardInterface _clipboardInterface; + + @override + void initState() { + _clipboardInterface = widget.clipboardInterface; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Future _copy() async { + await _clipboardInterface.setData(ClipboardData(text: widget.xpub)); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Wallet Xpub", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 14), + QrImage( + data: widget.xpub, + size: 300, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Copy", + onPressed: () async { + await _copy(); + }), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index f91448dda..88288506e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -1,19 +1,25 @@ import 'dart:async'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_show_xpub_dialog.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; enum _WalletOptions { addressList, - deleteWallet; + deleteWallet, + showXpub; String get prettyName { switch (this) { @@ -21,15 +27,15 @@ enum _WalletOptions { return "Address list"; case _WalletOptions.deleteWallet: return "Delete wallet"; + case _WalletOptions.showXpub: + return "Show xPub"; } } } class WalletOptionsButton extends ConsumerStatefulWidget { - const WalletOptionsButton({ - Key? key, - required this.walletId, - }) : super(key: key); + const WalletOptionsButton({Key? key, required this.walletId}) + : super(key: key); final String walletId; @@ -40,10 +46,12 @@ class WalletOptionsButton extends ConsumerStatefulWidget { class _WalletOptionsButtonState extends ConsumerState { late final String walletId; + late final Coin coin; @override void initState() { walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; super.initState(); } @@ -70,6 +78,10 @@ class _WalletOptionsButtonState extends ConsumerState { onAddressListPressed: () async { Navigator.of(context).pop(_WalletOptions.addressList); }, + onShowXpubPressed: () async { + Navigator.of(context).pop(_WalletOptions.showXpub); + }, + coin: coin, ); }, ); @@ -104,6 +116,41 @@ class _WalletOptionsButtonState extends ConsumerState { ), ); + if (result == true) { + if (mounted) { + Navigator.of(context).pop(); + } + } + break; + case _WalletOptions.showXpub: + final List mnemonic = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + final seed = bip39.mnemonicToSeed(mnemonic.join(' ')); + final node = bip32.BIP32.fromSeed(seed); + final xpub = node.neutered().toBase58(); + + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopShowXpubDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopShowXpubDialog.routeName, + arguments: xpub, + ), + ), + ]; + }, + ), + ); + if (result == true) { if (mounted) { Navigator.of(context).pop(); @@ -140,13 +187,20 @@ class WalletOptionsPopupMenu extends StatelessWidget { Key? key, required this.onDeletePressed, required this.onAddressListPressed, + required this.onShowXpubPressed, + required this.coin, }) : super(key: key); final VoidCallback onDeletePressed; final VoidCallback onAddressListPressed; + final VoidCallback onShowXpubPressed; + final Coin coin; @override Widget build(BuildContext context) { + final bool xpubEnabled = + coin != Coin.monero && coin != Coin.epicCash && coin != Coin.wownero; + return Stack( children: [ Positioned( @@ -200,6 +254,43 @@ class WalletOptionsPopupMenu extends StatelessWidget { ), ), ), + if (xpubEnabled) + const SizedBox( + height: 8, + ), + if (xpubEnabled) + TransparentButton( + onPressed: onShowXpubPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.eye, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.showXpub.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), const SizedBox( height: 8, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5d5599902..597db1b78 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -84,6 +84,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -111,6 +112,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/des import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_show_xpub_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; @@ -396,6 +398,20 @@ class RouteGenerator { builder: (_) => const DebugView(), settings: RouteSettings(name: settings.name)); + case XPubView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => XPubView( + xpub: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case AppearanceSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -1526,6 +1542,19 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopShowXpubDialog.routeName: + if (args is String) { + return FadePageRoute( + DesktopShowXpubDialog( + xpub: args, + ), + RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute(