From 81008841ccc5813e0b1a507b75fe0635f9c00274 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 30 Dec 2024 12:46:48 -0600 Subject: [PATCH] feat: add qr code scanning and display and correct spacing in explainer text --- .../multisig_coordinator_view.dart | 64 ++++++++- .../multisig_setup_view.dart | 2 +- .../xpub_qr_popup.dart | 134 ++++++++++++++++++ 3 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 lib/pages/wallet_view/multisig_coordinator_view/xpub_qr_popup.dart diff --git a/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart b/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart index ed99e5932..e10d2b40b 100644 --- a/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart +++ b/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:barcode_scan2/platform_wrapper.dart'; import 'package:bip48/bip48.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -19,6 +20,7 @@ import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/global/wallets_provider.dart'; import '../../../services/transaction_notification_tracker.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/logger.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/coins/bip48_bitcoin.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; @@ -27,6 +29,7 @@ import '../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/icon_widgets/copy_icon.dart'; import '../../../widgets/icon_widgets/qrcode_icon.dart'; @@ -34,6 +37,7 @@ import '../../add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_di import '../../add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart'; import '../../add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart'; import '../../home_view/home_view.dart'; +import 'xpub_qr_popup.dart'; final multisigCoordinatorStateProvider = StateNotifierProvider( @@ -310,7 +314,12 @@ class _MultisigSetupViewState extends ConsumerState { .buttonTextSecondary, ), onPressed: () { - // TODO: Implement QR code scanning + showDialog( + context: context, + builder: (context) => XpubQrPopup( + xpub: _myXpub, + ), + ); }, ), const SizedBox(width: 8), @@ -393,9 +402,7 @@ class _MultisigSetupViewState extends ConsumerState { .extension()! .buttonTextSecondary, ), - onPressed: () { - // TODO: Implement QR code scanning - }, + onPressed: () => {scanQr(i - 1)}, ), const SizedBox(width: 8), SecondaryButton( @@ -629,6 +636,55 @@ class _MultisigSetupViewState extends ConsumerState { await WakelockPlus.disable(); } } + + Future scanQr(int cosignerIndex) async { + try { + if (Platform.isAndroid || Platform.isIOS) { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + + final qrResult = await BarcodeScanner.scan(); + + xpubControllers[cosignerIndex].text = qrResult.rawContent; + + ref + .read(multisigCoordinatorStateProvider.notifier) + .addCosignerXpub(qrResult.rawContent); + + setState(() {}); // Trigger rebuild to update button state. + } else { + // Platform.isLinux, Platform.isWindows, or Platform.isMacOS. + final qrResult = await showDialog( + context: context, + builder: (context) => const QrCodeScannerDialog(), + ); + + if (qrResult == null) { + Logging.instance.log( + "QR scanning cancelled", + level: LogLevel.Info, + ); + } else { + xpubControllers[cosignerIndex].text = qrResult; + + ref + .read(multisigCoordinatorStateProvider.notifier) + .addCosignerXpub(qrResult); + + setState(() {}); // Trigger rebuild to update button state. + } + } + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + } } class MultisigCoordinatorData { diff --git a/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart b/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart index 0587ce6f8..8113b790f 100644 --- a/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart +++ b/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart @@ -157,7 +157,7 @@ class _MultisigSetupViewState extends ConsumerState { "Multisignature accounts, also called shared accounts, require " "multiple signatures to authorize a transaction. This can " "increase security by preventing a single point of failure or " - "allow multiple parties to jointly control funds." + "allow multiple parties to jointly control funds. " "For example, in a 2-of-3 multisig account, two of the three " "cosigners are required in order to sign a transaction and spend " "funds.", diff --git a/lib/pages/wallet_view/multisig_coordinator_view/xpub_qr_popup.dart b/lib/pages/wallet_view/multisig_coordinator_view/xpub_qr_popup.dart new file mode 100644 index 000000000..34e18daf2 --- /dev/null +++ b/lib/pages/wallet_view/multisig_coordinator_view/xpub_qr_popup.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/detail_item.dart'; +import '../../../../widgets/qr.dart'; +import '../../../../widgets/rounded_white_container.dart'; + +class XpubQrPopup extends StatelessWidget { + const XpubQrPopup({ + super.key, + required this.xpub, + }); + + final String xpub; + + Future _copy(BuildContext context) async { + await Clipboard.setData( + ClipboardData(text: xpub), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return DesktopDialog( + maxWidth: isDesktop ? 600 : MediaQuery.of(context).size.width - 32, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Your xPub", + style: STextStyles.desktopH2(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of(context, rootNavigator: true).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: isDesktop ? 12 : 16, + ), + DetailItem( + title: "Derivation path", + detail: "m/48'/0'/0'/2'", // TODO: Get actual derivation path + horizontal: true, + borderColor: isDesktop + ? Theme.of(context) + .extension()! + .textFieldDefaultBG + : null, + ), + SizedBox( + height: isDesktop ? 12 : 16, + ), + QR( + data: xpub, + size: + isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5, + ), + SizedBox( + height: isDesktop ? 12 : 16, + ), + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context) + .extension()! + .textFieldDefaultBG + : null, + child: SelectableText( + xpub, + style: STextStyles.w500_14(context), + ), + ), + SizedBox( + height: isDesktop ? 12 : 16, + ), + Row( + children: [ + if (isDesktop) const Spacer(), + if (isDesktop) + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () => _copy(context), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +}