feat: add qr code scanning and display

and correct spacing in explainer text
This commit is contained in:
sneurlax 2024-12-30 12:46:48 -06:00
parent 00932dbc0b
commit 81008841cc
3 changed files with 195 additions and 5 deletions

View file

@ -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<MultisigCoordinatorState, MultisigCoordinatorData>(
@ -310,7 +314,12 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
.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<MultisigCoordinatorView> {
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () {
// TODO: Implement QR code scanning
},
onPressed: () => {scanQr(i - 1)},
),
const SizedBox(width: 8),
SecondaryButton(
@ -629,6 +636,55 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
await WakelockPlus.disable();
}
}
Future<void> scanQr(int cosignerIndex) async {
try {
if (Platform.isAndroid || Platform.isIOS) {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.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<String>(
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 {

View file

@ -157,7 +157,7 @@ class _MultisigSetupViewState extends ConsumerState<MultisigSetupView> {
"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.",

View file

@ -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<void> _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<StackColors>()!
.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<StackColors>()!
.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),
),
),
],
),
],
),
),
],
),
);
}
}