mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-22 19:39:22 +00:00
Merge branch 'staging' into staging
This commit is contained in:
commit
2af6e32392
21 changed files with 941 additions and 552 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 982f5ab19fe0dd3dd3f6be2c46f8dff13d49027c
|
||||
Subproject commit db7585d8cd493b143e0a0652c618904d1f636d1d
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -32,6 +33,7 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
|||
import '../../../../widgets/desktop/desktop_app_bar.dart';
|
||||
import '../../../../widgets/desktop/desktop_scaffold.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
|
||||
import '../../../../widgets/frost_mascot.dart';
|
||||
import '../../../../widgets/icon_widgets/clipboard_icon.dart';
|
||||
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
|
||||
|
@ -207,6 +209,54 @@ class _RestoreFrostMsWalletViewState
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> scanQr() 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();
|
||||
|
||||
configFieldController.text = qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
} else {
|
||||
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return QrCodeScannerDialog(
|
||||
onQrCodeDetected: (qrCodeData) {
|
||||
try {
|
||||
// TODO [prio=low]: Validate QR code data.
|
||||
configFieldController.text = qrCodeData;
|
||||
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Error processing QR code data: $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
|
@ -351,31 +401,7 @@ class _RestoreFrostMsWalletViewState
|
|||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75),
|
||||
);
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: scanQr,
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -304,7 +304,7 @@ class _AddressCardState extends ConsumerState<AddressCard> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
address.value,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -97,7 +97,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
ref.watch(pWalletCoin(widget.walletId)),
|
||||
ref.watch(pWalletCoin(widget.walletId)).uriScheme,
|
||||
address.value,
|
||||
{},
|
||||
),
|
||||
|
@ -289,7 +289,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address.value,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -142,7 +142,7 @@ class _AddressQrPopupState extends State<AddressQrPopup> {
|
|||
key: _qrKey,
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
widget.addressString,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -170,7 +170,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
|||
}
|
||||
|
||||
final uriString = AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
receivingAddress,
|
||||
queryParams,
|
||||
);
|
||||
|
@ -263,7 +263,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
|
|||
}
|
||||
|
||||
_uriString = AddressUtils.buildUriString(
|
||||
widget.coin,
|
||||
widget.coin.uriScheme,
|
||||
receivingAddress,
|
||||
{},
|
||||
);
|
||||
|
|
|
@ -577,7 +577,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
children: [
|
||||
QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -16,11 +16,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_libepiccash/lib.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../models/isar/models/transaction_note.dart';
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../pinpad_views/lock_screen_view.dart';
|
||||
import 'sub_widgets/sending_transaction_dialog.dart';
|
||||
import '../wallet_view/wallet_view.dart';
|
||||
import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
|
||||
import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart';
|
||||
import '../../providers/db/main_db_provider.dart';
|
||||
|
@ -53,6 +51,9 @@ import '../../widgets/rounded_white_container.dart';
|
|||
import '../../widgets/stack_dialog.dart';
|
||||
import '../../widgets/stack_text_field.dart';
|
||||
import '../../widgets/textfield_icon_button.dart';
|
||||
import '../pinpad_views/lock_screen_view.dart';
|
||||
import '../wallet_view/wallet_view.dart';
|
||||
import 'sub_widgets/sending_transaction_dialog.dart';
|
||||
|
||||
class ConfirmTransactionView extends ConsumerStatefulWidget {
|
||||
const ConfirmTransactionView({
|
||||
|
|
|
@ -163,25 +163,23 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
final results = AddressUtils.parseUri(qrResult.rawContent);
|
||||
final paymentData = AddressUtils.parsePaymentUri(qrResult.rawContent);
|
||||
|
||||
Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info);
|
||||
|
||||
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
|
||||
if (paymentData.coin.uriScheme == coin.uriScheme) {
|
||||
// auto fill address
|
||||
_address = (results["address"] ?? "").trim();
|
||||
_address = paymentData.address.trim();
|
||||
sendToController.text = _address!;
|
||||
|
||||
// autofill notes field
|
||||
if (results["message"] != null) {
|
||||
noteController.text = results["message"]!;
|
||||
} else if (results["label"] != null) {
|
||||
noteController.text = results["label"]!;
|
||||
if (paymentData.message != null) {
|
||||
noteController.text = paymentData.message!;
|
||||
} else if (paymentData.label != null) {
|
||||
noteController.text = paymentData.label!;
|
||||
}
|
||||
|
||||
// autofill amount field
|
||||
if (results["amount"] != null) {
|
||||
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
|
||||
if (paymentData.amount != null) {
|
||||
final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
|
||||
fractionDigits: coin.fractionDigits,
|
||||
);
|
||||
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
|
||||
|
@ -1071,7 +1069,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
_address = _address!.substring(0, _address!.indexOf("\n"));
|
||||
}
|
||||
|
||||
sendToController.text = formatAddress(_address!);
|
||||
sendToController.text = AddressUtils().formatAddress(_address!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1402,7 +1400,8 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
|
||||
if (coin is Epiccash) {
|
||||
// strip http:// and https:// if content contains @
|
||||
content = formatAddress(
|
||||
content = AddressUtils()
|
||||
.formatAddress(
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
@ -2421,22 +2420,3 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
String formatAddress(String epicAddress) {
|
||||
// strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address)
|
||||
if ((epicAddress.startsWith("http://") ||
|
||||
epicAddress.startsWith("https://")) &&
|
||||
epicAddress.contains("@")) {
|
||||
epicAddress = epicAddress.replaceAll("http://", "");
|
||||
epicAddress = epicAddress.replaceAll("https://", "");
|
||||
}
|
||||
// strip mailto: prefix
|
||||
if (epicAddress.startsWith("mailto:")) {
|
||||
epicAddress = epicAddress.replaceAll("mailto:", "");
|
||||
}
|
||||
// strip / suffix if the address contains an @ symbol (and is thus an epicbox address)
|
||||
if (epicAddress.endsWith("/") && epicAddress.contains("@")) {
|
||||
epicAddress = epicAddress.substring(0, epicAddress.length - 1);
|
||||
}
|
||||
return epicAddress;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import '../../widgets/stack_dialog.dart';
|
|||
import '../../widgets/stack_text_field.dart';
|
||||
import '../../widgets/textfield_icon_button.dart';
|
||||
import '../address_book_views/address_book_view.dart';
|
||||
import '../token_view/token_view.dart';
|
||||
import 'confirm_transaction_view.dart';
|
||||
import 'sub_widgets/building_transaction_dialog.dart';
|
||||
import 'sub_widgets/transaction_fee_selection_sheet.dart';
|
||||
|
@ -522,6 +523,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
|
|||
walletId: walletId,
|
||||
isTokenTx: true,
|
||||
onSuccess: clearSendForm,
|
||||
routeOnSuccessName: TokenView.routeName,
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: ConfirmTransactionView.routeName,
|
||||
|
|
|
@ -52,6 +52,8 @@ import '../../../../../wallets/isar/models/frost_wallet_info.dart';
|
|||
import '../../../../../wallets/isar/models/wallet_info.dart';
|
||||
import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import '../../../../../wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import '../../../../../wallets/wallet/impl/monero_wallet.dart';
|
||||
import '../../../../../wallets/wallet/impl/wownero_wallet.dart';
|
||||
import '../../../../../wallets/wallet/wallet.dart';
|
||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
|
@ -486,7 +488,13 @@ abstract class SWB {
|
|||
privateKey: privateKey,
|
||||
);
|
||||
|
||||
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {
|
||||
await wallet.init(isRestore: true);
|
||||
} else if (wallet is WowneroWallet) {
|
||||
await wallet.init(isRestore: true);
|
||||
} else {
|
||||
await wallet.init();
|
||||
}
|
||||
|
||||
int restoreHeight = walletbackup['restoreHeight'] as int? ?? 0;
|
||||
if (restoreHeight <= 0) {
|
||||
|
|
|
@ -448,7 +448,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
Center(
|
||||
child: QR(
|
||||
data: AddressUtils.buildUriString(
|
||||
coin,
|
||||
coin.uriScheme,
|
||||
address,
|
||||
{},
|
||||
),
|
||||
|
|
|
@ -9,26 +9,14 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera_linux/camera_linux.dart';
|
||||
import 'package:camera_macos/camera_macos_arguments.dart';
|
||||
import 'package:camera_macos/camera_macos_controller.dart';
|
||||
import 'package:camera_macos/camera_macos_device.dart';
|
||||
import 'package:camera_macos/camera_macos_platform_interface.dart';
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
import 'package:camera_windows/camera_windows.dart';
|
||||
import 'package:cw_core/monero_transaction_priority.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../../../models/isar/models/contact_entry.dart';
|
||||
import '../../../../models/paynym/paynym_account_lite.dart';
|
||||
|
@ -71,6 +59,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart';
|
|||
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import '../../../../widgets/desktop/desktop_fee_dialog.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
|
||||
import '../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart';
|
||||
import '../../../../widgets/fee_slider.dart';
|
||||
|
@ -79,7 +68,6 @@ import '../../../../widgets/icon_widgets/clipboard_icon.dart';
|
|||
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
|
||||
import '../../../../widgets/icon_widgets/x_icon.dart';
|
||||
import '../../../../widgets/rounded_container.dart';
|
||||
import '../../../../widgets/stack_dialog.dart';
|
||||
import '../../../../widgets/stack_text_field.dart';
|
||||
import '../../../../widgets/textfield_icon_button.dart';
|
||||
import '../../../coin_control/desktop_coin_control_use_dialog.dart';
|
||||
|
@ -160,7 +148,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return QrCodeScannerDialog(
|
||||
walletId: widget.walletId,
|
||||
onQrCodeDetected: (qrCodeData) {
|
||||
try {
|
||||
_processQrCodeData(qrCodeData);
|
||||
|
@ -743,13 +730,15 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
|
||||
void _processQrCodeData(String qrCodeData) {
|
||||
try {
|
||||
var results = AddressUtils.parseUri(qrCodeData);
|
||||
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
|
||||
_address = (results["address"] ?? "").trim();
|
||||
final paymentData = AddressUtils.parsePaymentUri(qrCodeData);
|
||||
if (paymentData.coin.uriScheme == coin.uriScheme) {
|
||||
// Auto fill address.
|
||||
_address = paymentData.address.trim();
|
||||
sendToController.text = _address!;
|
||||
|
||||
if (results["amount"] != null) {
|
||||
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
|
||||
// Amount.
|
||||
if (paymentData.amount != null) {
|
||||
final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
|
||||
fractionDigits: coin.fractionDigits,
|
||||
);
|
||||
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
|
||||
|
@ -759,6 +748,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
ref.read(pSendAmount.notifier).state = amount;
|
||||
}
|
||||
|
||||
// Note/message.
|
||||
if (paymentData.message != null) {
|
||||
_note = paymentData.message;
|
||||
} else if (paymentData.label != null) {
|
||||
_note = paymentData.label;
|
||||
}
|
||||
|
||||
_setValidAddressProviders(_address);
|
||||
setState(() {
|
||||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||
|
@ -809,9 +805,39 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
content = content.substring(0, content.indexOf("\n"));
|
||||
}
|
||||
|
||||
try {
|
||||
final paymentData = AddressUtils.parsePaymentUri(content);
|
||||
if (paymentData.coin.uriScheme == coin.uriScheme) {
|
||||
// auto fill address
|
||||
_address = paymentData.address;
|
||||
sendToController.text = _address!;
|
||||
|
||||
// autofill notes field.
|
||||
if (paymentData.message != null) {
|
||||
_note = paymentData.message;
|
||||
} else if (paymentData.label != null) {
|
||||
_note = paymentData.label;
|
||||
}
|
||||
|
||||
// autofill amoutn field
|
||||
if (paymentData.amount != null) {
|
||||
final amount = Decimal.parse(paymentData.amount!).toAmount(
|
||||
fractionDigits: coin.fractionDigits,
|
||||
);
|
||||
cryptoAmountController.text = ref
|
||||
.read(pAmountFormatter(coin))
|
||||
.format(amount, withUnitName: false);
|
||||
ref.read(pSendAmount.notifier).state = amount;
|
||||
}
|
||||
|
||||
// Trigger validation after pasting.
|
||||
_setValidAddressProviders(_address);
|
||||
setState(() {
|
||||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||
});
|
||||
} else {
|
||||
if (coin is Epiccash) {
|
||||
// strip http:// and https:// if content contains @
|
||||
content = formatAddress(content);
|
||||
content = AddressUtils().formatAddress(content);
|
||||
}
|
||||
|
||||
sendToController.text = content;
|
||||
|
@ -822,6 +848,23 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// If parsing fails, treat it as a plain address.
|
||||
if (coin is Epiccash) {
|
||||
// strip http:// and https:// if content contains @
|
||||
content = AddressUtils().formatAddress(content);
|
||||
}
|
||||
|
||||
sendToController.text = content;
|
||||
_address = content;
|
||||
|
||||
// Trigger validation after pasting.
|
||||
_setValidAddressProviders(_address);
|
||||
setState(() {
|
||||
_addressToggleFlag = sendToController.text.isNotEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pasteMemo() async {
|
||||
|
@ -1968,389 +2011,3 @@ String formatAddress(String epicAddress) {
|
|||
}
|
||||
return epicAddress;
|
||||
}
|
||||
|
||||
class QrCodeScannerDialog extends StatefulWidget {
|
||||
final String walletId;
|
||||
final Function(String) onQrCodeDetected;
|
||||
|
||||
QrCodeScannerDialog({
|
||||
required this.walletId,
|
||||
required this.onQrCodeDetected,
|
||||
});
|
||||
|
||||
@override
|
||||
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
|
||||
}
|
||||
|
||||
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||
final CameraLinux? _cameraLinuxPlugin =
|
||||
Platform.isLinux ? CameraLinux() : null;
|
||||
final CameraWindows? _cameraWindowsPlugin =
|
||||
Platform.isWindows ? CameraWindows() : null;
|
||||
CameraMacOSController? _macOSController;
|
||||
bool _isCameraOpen = false;
|
||||
Image? _image;
|
||||
bool _isScanning = false;
|
||||
int _cameraId = -1;
|
||||
String? _macOSDeviceId;
|
||||
int _imageDelayInMs = 250;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isCameraOpen = false;
|
||||
_isScanning = false;
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopCamera();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isScanning = true; // Show the progress indicator
|
||||
});
|
||||
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
await _cameraLinuxPlugin!.initializeCamera();
|
||||
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
|
||||
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
|
||||
final List<CameraDescription> cameras =
|
||||
await _cameraWindowsPlugin!.availableCameras();
|
||||
if (cameras.isEmpty) {
|
||||
throw CameraException('No cameras available', 'No cameras found.');
|
||||
}
|
||||
final CameraDescription camera = cameras[0]; // Could be user-selected.
|
||||
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings(
|
||||
camera,
|
||||
const MediaSettings(
|
||||
resolutionPreset: ResolutionPreset.low,
|
||||
fps: 4,
|
||||
videoBitrate: 200000,
|
||||
enableAudio: false,
|
||||
),
|
||||
);
|
||||
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
|
||||
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
|
||||
// TODO [prio=low]: Make this work. ^^^
|
||||
Logging.instance.log("Windows Camera initialized with ID: $_cameraId",
|
||||
level: LogLevel.Info);
|
||||
} else if (Platform.isMacOS) {
|
||||
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
|
||||
.listDevices(deviceType: CameraMacOSDeviceType.video);
|
||||
if (videoDevices.isEmpty) {
|
||||
throw Exception('No cameras available');
|
||||
}
|
||||
_macOSDeviceId = videoDevices.first.deviceId;
|
||||
await CameraMacOS.instance
|
||||
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
|
||||
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
});
|
||||
|
||||
Logging.instance.log(
|
||||
"macOS Camera initialized with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
_isScanning = true;
|
||||
});
|
||||
}
|
||||
unawaited(_captureAndScanImage()); // Could be awaited.
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
|
||||
if (mounted) {
|
||||
// widget.onSnackbar("Failed to initialize camera. Please try again.");
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopCamera() async {
|
||||
try {
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
_cameraLinuxPlugin!.stopCamera();
|
||||
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
|
||||
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
|
||||
// if (_cameraId >= 0) {
|
||||
await _cameraWindowsPlugin!.dispose(_cameraId);
|
||||
Logging.instance.log("Windows Camera stopped with ID: $_cameraId",
|
||||
level: LogLevel.Info);
|
||||
// } else {
|
||||
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
|
||||
// level: LogLevel.Error);
|
||||
// }
|
||||
} else if (Platform.isMacOS) {
|
||||
// if (_macOSDeviceId != null) {
|
||||
await CameraMacOS.instance.stopImageStream();
|
||||
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
// } else {
|
||||
// Logging.instance.log("macOS Camera ID is null. Cannot stop.",
|
||||
// level: LogLevel.Error);
|
||||
// }
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
_isCameraOpen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _captureAndScanImage() async {
|
||||
while (_isCameraOpen && _isScanning) {
|
||||
try {
|
||||
String? base64Image;
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
base64Image = await _cameraLinuxPlugin!.captureImage();
|
||||
} else if (Platform.isWindows) {
|
||||
final XFile xfile =
|
||||
await _cameraWindowsPlugin!.takePicture(_cameraId);
|
||||
final bytes = await xfile.readAsBytes();
|
||||
base64Image = base64Encode(bytes);
|
||||
// We could use a Uint8List to optimize for Windows and macOS.
|
||||
} else if (Platform.isMacOS) {
|
||||
final macOSimg = await CameraMacOS.instance.takePicture();
|
||||
if (macOSimg == null) {
|
||||
Logging.instance
|
||||
.log("Failed to capture image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
final img.Image? image = img.decodeImage(macOSimg.bytes!);
|
||||
if (image == null) {
|
||||
Logging.instance
|
||||
.log("Failed to capture image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
base64Image = base64Encode(Uint8List.fromList(img.encodePng(image)));
|
||||
}
|
||||
if (base64Image == null || base64Image.isEmpty) {
|
||||
// Logging.instance
|
||||
// .log("Failed to capture image", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
final img.Image? image = img.decodeImage(base64Decode(base64Image));
|
||||
// TODO [prio=low]: Optimize this process. Docs say:
|
||||
// > WARNING Since this will check the image data against all known
|
||||
// > decoders, it is much slower than using an explicit decoder
|
||||
if (image == null) {
|
||||
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_image = Image.memory(
|
||||
base64Decode(base64Image!),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final String? scanResult = await _scanImage(image);
|
||||
if (scanResult != null && scanResult.isNotEmpty) {
|
||||
widget.onQrCodeDetected(scanResult);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// Logging.instance.log("No QR code found in the image", level: LogLevel.Info);
|
||||
// if (mounted) {
|
||||
// widget.onSnackbar("No QR code found in the image.");
|
||||
// }
|
||||
// Spammy.
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
} catch (e, s) {
|
||||
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
|
||||
// if (mounted) {
|
||||
// widget.onSnackbar(
|
||||
// "Error capturing or scanning the image. Please try again.");
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _scanImage(img.Image image) async {
|
||||
try {
|
||||
final LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List(),
|
||||
);
|
||||
final BinaryBitmap bitmap =
|
||||
BinaryBitmap(GlobalHistogramBinarizer(source));
|
||||
|
||||
final QRCodeReader reader = QRCodeReader();
|
||||
final qrDecode = reader.decode(bitmap);
|
||||
if (qrDecode.text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return qrDecode.text;
|
||||
} catch (e, s) {
|
||||
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 696,
|
||||
maxHeight: 600,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Text(
|
||||
"Scan QR code",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _isCameraOpen
|
||||
? _image != null
|
||||
? _image!
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Center(
|
||||
child:
|
||||
CircularProgressIndicator(), // Show progress indicator immediately
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Container()),
|
||||
// "Select file" button.
|
||||
SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Select file",
|
||||
width: 200,
|
||||
onPressed: () async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["png", "jpg", "jpeg"],
|
||||
);
|
||||
|
||||
if (result == null || result.files.single.path == null) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error scanning QR code",
|
||||
message: "No file selected.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final filePath = result?.files.single.path!;
|
||||
if (filePath == null) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error scanning QR code",
|
||||
message: "Error selecting file.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final img.Image? image =
|
||||
img.decodeImage(File(filePath!).readAsBytesSync());
|
||||
if (image == null) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error scanning QR code",
|
||||
message: "Failed to decode image.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final String? scanResult = await _scanImage(image);
|
||||
if (scanResult != null && scanResult.isNotEmpty) {
|
||||
widget.onQrCodeDetected(scanResult);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error scanning QR code",
|
||||
message: "No QR code found in the image.",
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Failed to decode image: $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error scanning QR code",
|
||||
message:
|
||||
"Error processing the image. Please try again.",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Close button.
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Close",
|
||||
width: 272.5,
|
||||
onPressed: () {
|
||||
_stopCamera();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,21 @@
|
|||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'logger.dart';
|
||||
import '../app_config.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
class AddressUtils {
|
||||
static final Set<String> recognizedParams = {
|
||||
'amount',
|
||||
'label',
|
||||
'message',
|
||||
'tx_amount', // For Monero/Wownero.
|
||||
'tx_payment_id',
|
||||
'recipient_name',
|
||||
'tx_description',
|
||||
// TODO [prio=med]: Add more recognized params for other coins.
|
||||
};
|
||||
|
||||
static String condenseAddress(String address) {
|
||||
return '${address.substring(0, 5)}...${address.substring(address.length - 5)}';
|
||||
}
|
||||
|
@ -170,42 +181,150 @@ class AddressUtils {
|
|||
// // }
|
||||
// }
|
||||
|
||||
/// parse an address uri
|
||||
/// returns an empty map if the input string does not begin with "firo:"
|
||||
/// Return only recognized parameters.
|
||||
static Map<String, String> filterParams(Map<String, String> params) {
|
||||
return Map.fromEntries(params.entries
|
||||
.where((entry) => recognizedParams.contains(entry.key.toLowerCase())));
|
||||
}
|
||||
|
||||
/// Parses a URI string and returns a map with parsed components.
|
||||
static Map<String, String> parseUri(String uri) {
|
||||
final Map<String, String> result = {};
|
||||
try {
|
||||
final u = Uri.parse(uri);
|
||||
if (u.hasScheme) {
|
||||
result["scheme"] = u.scheme.toLowerCase();
|
||||
|
||||
// Handle different URI formats.
|
||||
if (result["scheme"] == "bitcoin" ||
|
||||
result["scheme"] == "bitcoincash") {
|
||||
result["address"] = u.path;
|
||||
result.addAll(u.queryParameters);
|
||||
} else if (result["scheme"] == "monero") {
|
||||
// Monero addresses can contain '?' which Uri.parse interprets as query start.
|
||||
final addressEnd =
|
||||
uri.indexOf('?', 7); // 7 is the length of "monero:".
|
||||
if (addressEnd != -1) {
|
||||
result["address"] = uri.substring(7, addressEnd);
|
||||
} else {
|
||||
result["address"] = uri.substring(7);
|
||||
}
|
||||
} else {
|
||||
// Default case, treat path as address.
|
||||
result["address"] = u.path;
|
||||
}
|
||||
|
||||
// Parse query parameters.
|
||||
result.addAll(_parseQueryParameters(u.queryParameters));
|
||||
|
||||
// Handle Monero-specific fragment (tx_description).
|
||||
if (u.fragment.isNotEmpty && result["scheme"] == "monero") {
|
||||
result["tx_description"] = Uri.decodeComponent(u.fragment);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logging.instance
|
||||
.log("Exception caught in parseUri($uri): $e", level: LogLevel.Error);
|
||||
print("Exception caught in parseUri($uri): $e");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// builds a uri string with the given address and query parameters if any
|
||||
/// Helper method to parse and normalize query parameters.
|
||||
static Map<String, String> _parseQueryParameters(Map<String, String> params) {
|
||||
final Map<String, String> result = {};
|
||||
params.forEach((key, value) {
|
||||
final lowerKey = key.toLowerCase();
|
||||
if (recognizedParams.contains(lowerKey)) {
|
||||
switch (lowerKey) {
|
||||
case 'amount':
|
||||
case 'tx_amount':
|
||||
result['amount'] = _normalizeAmount(value);
|
||||
break;
|
||||
case 'label':
|
||||
case 'recipient_name':
|
||||
result['label'] = Uri.decodeComponent(value);
|
||||
break;
|
||||
case 'message':
|
||||
case 'tx_description':
|
||||
result['message'] = Uri.decodeComponent(value);
|
||||
break;
|
||||
case 'tx_payment_id':
|
||||
result['tx_payment_id'] = Uri.decodeComponent(value);
|
||||
break;
|
||||
default:
|
||||
result[lowerKey] = Uri.decodeComponent(value);
|
||||
}
|
||||
} else {
|
||||
// Include unrecognized parameters as-is.
|
||||
result[key] = Uri.decodeComponent(value);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Normalizes amount value to a standard format.
|
||||
static String _normalizeAmount(String amount) {
|
||||
// Remove any non-numeric characters except for '.'
|
||||
final sanitized = amount.replaceAll(RegExp(r'[^\d.]'), '');
|
||||
// Ensure only one decimal point
|
||||
final parts = sanitized.split('.');
|
||||
if (parts.length > 2) {
|
||||
return '${parts[0]}.${parts.sublist(1).join()}';
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/// Centralized method to handle various cryptocurrency URIs and return a common object.
|
||||
static PaymentUriData parsePaymentUri(String uri) {
|
||||
final Map<String, String> parsedData = parseUri(uri);
|
||||
|
||||
// Normalize the URI scheme.
|
||||
final String scheme = parsedData['scheme'] ?? '';
|
||||
parsedData.remove('scheme');
|
||||
|
||||
// Determine the coin type based on the URI scheme.
|
||||
final CryptoCurrency coin = _getCryptoCurrencyByScheme(scheme);
|
||||
|
||||
// Filter out unrecognized parameters.
|
||||
final filteredParams = filterParams(parsedData);
|
||||
|
||||
return PaymentUriData(
|
||||
coin: coin,
|
||||
address: parsedData['address'] ?? '',
|
||||
amount: filteredParams['amount'] ?? filteredParams['tx_amount'],
|
||||
label: filteredParams['label'] ?? filteredParams['recipient_name'],
|
||||
message: filteredParams['message'] ?? filteredParams['tx_description'],
|
||||
paymentId: filteredParams['tx_payment_id'], // Specific to Monero
|
||||
additionalParams: filteredParams,
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a uri string with the given address and query parameters (if any)
|
||||
static String buildUriString(
|
||||
CryptoCurrency coin,
|
||||
String scheme,
|
||||
String address,
|
||||
Map<String, String> params,
|
||||
) {
|
||||
// TODO: other sanitation as well ?
|
||||
String sanitizedAddress = address;
|
||||
if (coin is Bitcoincash || coin is Ecash) {
|
||||
final prefix = "${coin.uriScheme}:";
|
||||
if (address.startsWith(prefix)) {
|
||||
sanitizedAddress = address.replaceFirst(prefix, "");
|
||||
// Filter unrecognized parameters.
|
||||
final filteredParams = filterParams(params);
|
||||
String uriString = "$scheme:$address";
|
||||
|
||||
if (scheme.toLowerCase() == "monero") {
|
||||
// Handle Monero-specific formatting.
|
||||
if (filteredParams.containsKey("tx_description")) {
|
||||
final description = filteredParams.remove("tx_description")!;
|
||||
if (filteredParams.isNotEmpty) {
|
||||
uriString += Uri(queryParameters: filteredParams).toString();
|
||||
}
|
||||
uriString += "#${Uri.encodeComponent(description)}";
|
||||
} else if (filteredParams.isNotEmpty) {
|
||||
uriString += Uri(queryParameters: filteredParams).toString();
|
||||
}
|
||||
} else {
|
||||
// General case for other cryptocurrencies.
|
||||
if (filteredParams.isNotEmpty) {
|
||||
uriString += Uri(queryParameters: filteredParams).toString();
|
||||
}
|
||||
}
|
||||
String uriString = "${coin.uriScheme}:$sanitizedAddress";
|
||||
if (params.isNotEmpty) {
|
||||
uriString += Uri(queryParameters: params).toString();
|
||||
}
|
||||
|
||||
return uriString;
|
||||
}
|
||||
|
||||
|
@ -215,10 +334,7 @@ class AddressUtils {
|
|||
try {
|
||||
result = Map<String, dynamic>.from(jsonDecode(data) as Map);
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"Exception caught in parseQRSeedData($data): $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
print("Exception caught in parseQRSeedData($data): $e");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -227,4 +343,53 @@ class AddressUtils {
|
|||
static String encodeQRSeedData(List<String> words) {
|
||||
return jsonEncode({"mnemonic": words});
|
||||
}
|
||||
|
||||
/// Method to get CryptoCurrency based on URI scheme.
|
||||
static CryptoCurrency _getCryptoCurrencyByScheme(String scheme) {
|
||||
if (AppConfig.coins.map((e) => e.uriScheme).toSet().contains(scheme)) {
|
||||
return AppConfig.coins.firstWhere((e) => e.uriScheme == scheme);
|
||||
} else {
|
||||
throw UnsupportedError('Unsupported URI scheme: $scheme');
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an address string to remove any unnecessary prefixes or suffixes.
|
||||
String formatAddress(String epicAddress) {
|
||||
// strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address)
|
||||
if ((epicAddress.startsWith("http://") ||
|
||||
epicAddress.startsWith("https://")) &&
|
||||
epicAddress.contains("@")) {
|
||||
epicAddress = epicAddress.replaceAll("http://", "");
|
||||
epicAddress = epicAddress.replaceAll("https://", "");
|
||||
}
|
||||
// strip mailto: prefix
|
||||
if (epicAddress.startsWith("mailto:")) {
|
||||
epicAddress = epicAddress.replaceAll("mailto:", "");
|
||||
}
|
||||
// strip / suffix if the address contains an @ symbol (and is thus an epicbox address)
|
||||
if (epicAddress.endsWith("/") && epicAddress.contains("@")) {
|
||||
epicAddress = epicAddress.substring(0, epicAddress.length - 1);
|
||||
}
|
||||
return epicAddress;
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentUriData {
|
||||
final CryptoCurrency coin;
|
||||
final String address;
|
||||
final String? amount;
|
||||
final String? label;
|
||||
final String? message;
|
||||
final String? paymentId; // Specific to Monero.
|
||||
final Map<String, String> additionalParams;
|
||||
|
||||
PaymentUriData({
|
||||
required this.coin,
|
||||
required this.address,
|
||||
this.amount,
|
||||
this.label,
|
||||
this.message,
|
||||
this.paymentId,
|
||||
required this.additionalParams,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -150,7 +150,7 @@ Future<MoneroNodeConnectionResponse> testMoneroNodeConnection(
|
|||
|
||||
final response = await request.close();
|
||||
final result = await response.transform(utf8.decoder).join();
|
||||
print("HTTP Response: $result");
|
||||
// print("HTTP Response: $result");
|
||||
|
||||
final success =
|
||||
result.contains('"result":') && !result.contains('"error"');
|
||||
|
|
|
@ -119,7 +119,7 @@ Future<bool> testNodeConnection({
|
|||
final url = formData.host!;
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri != null) {
|
||||
if (!uri.hasScheme) {
|
||||
if (!uri.hasScheme && !uri.host.endsWith(".onion")) {
|
||||
// try https first
|
||||
testPassed = await _xmrHelper(
|
||||
formData
|
||||
|
@ -141,16 +141,6 @@ Future<bool> testNodeConnection({
|
|||
proxyInfo,
|
||||
);
|
||||
}
|
||||
} else if (!uri.hasScheme && uri.host.endsWith(".onion")) {
|
||||
// We can just test http for onion addresses.
|
||||
testPassed = await _xmrHelper(
|
||||
formData
|
||||
..host = url
|
||||
..useSSL = false,
|
||||
context,
|
||||
onSuccess,
|
||||
proxyInfo,
|
||||
);
|
||||
} else {
|
||||
testPassed = await _xmrHelper(
|
||||
formData
|
||||
|
|
|
@ -84,6 +84,66 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
|
|||
_credentials = web3.EthPrivateKey.fromHex(privateKey);
|
||||
}
|
||||
|
||||
TxData _prepareTempTx(TxData txData, String myAddress) {
|
||||
// hack eth tx data into inputs and outputs
|
||||
final List<OutputV2> outputs = [];
|
||||
final List<InputV2> inputs = [];
|
||||
|
||||
final amount = txData.recipients!.first.amount;
|
||||
final addressTo = txData.recipients!.first.address;
|
||||
|
||||
final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptPubKeyHex: "00",
|
||||
valueStringSats: amount.raw.toString(),
|
||||
addresses: [
|
||||
addressTo,
|
||||
],
|
||||
walletOwns: addressTo == myAddress,
|
||||
);
|
||||
final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptSigHex: null,
|
||||
scriptSigAsm: null,
|
||||
sequence: null,
|
||||
outpoint: null,
|
||||
addresses: [myAddress],
|
||||
valueStringSats: amount.raw.toString(),
|
||||
witness: null,
|
||||
innerRedeemScriptAsm: null,
|
||||
coinbase: null,
|
||||
walletOwns: true,
|
||||
);
|
||||
|
||||
outputs.add(output);
|
||||
inputs.add(input);
|
||||
|
||||
final otherData = {
|
||||
"nonce": txData.nonce,
|
||||
"isCancelled": false,
|
||||
"overrideFee": txData.fee!.toJsonString(),
|
||||
};
|
||||
|
||||
final txn = TransactionV2(
|
||||
walletId: walletId,
|
||||
blockHash: null,
|
||||
hash: txData.txHash!,
|
||||
txid: txData.txid!,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
height: null,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
version: -1,
|
||||
type: addressTo == myAddress
|
||||
? TransactionType.sentToSelf
|
||||
: TransactionType.outgoing,
|
||||
subType: TransactionSubType.none,
|
||||
otherData: jsonEncode(otherData),
|
||||
);
|
||||
|
||||
return txData.copyWith(
|
||||
tempTx: txn,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Overrides ============================================
|
||||
|
||||
@override
|
||||
|
@ -447,7 +507,10 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<TxData> confirmSend({required TxData txData}) async {
|
||||
Future<TxData> confirmSend({
|
||||
required TxData txData,
|
||||
TxData Function(TxData txData, String myAddress)? prepareTempTx,
|
||||
}) async {
|
||||
final client = getEthClient();
|
||||
if (_credentials == null) {
|
||||
await _initCredentials();
|
||||
|
@ -459,10 +522,15 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
|
|||
chainId: txData.chainId!.toInt(),
|
||||
);
|
||||
|
||||
return txData.copyWith(
|
||||
final data = (prepareTempTx ?? _prepareTempTx)(
|
||||
txData.copyWith(
|
||||
txid: txid,
|
||||
txHash: txid,
|
||||
),
|
||||
(await getCurrentReceivingAddress())!.value,
|
||||
);
|
||||
|
||||
return await updateSentCachedTxData(txData: data);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -39,7 +39,7 @@ class EthTokenWallet extends Wallet {
|
|||
late web3dart.DeployedContract _deployedContract;
|
||||
late web3dart.ContractFunction _sendFunction;
|
||||
|
||||
static const _gasLimit = 200000;
|
||||
static const _gasLimit = 65000;
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
|
@ -67,6 +67,67 @@ class EthTokenWallet extends Wallet {
|
|||
String _addressFromTopic(String topic) =>
|
||||
checksumEthereumAddress("0x${topic.substring(topic.length - 40)}");
|
||||
|
||||
TxData _prepareTempTx(TxData txData, String myAddress) {
|
||||
final otherData = {
|
||||
"nonce": txData.nonce!,
|
||||
"isCancelled": false,
|
||||
"overrideFee": txData.fee!.toJsonString(),
|
||||
"contractAddress": tokenContract.address,
|
||||
};
|
||||
|
||||
final amount = txData.recipients!.first.amount;
|
||||
final addressTo = txData.recipients!.first.address;
|
||||
|
||||
// hack eth tx data into inputs and outputs
|
||||
final List<OutputV2> outputs = [];
|
||||
final List<InputV2> inputs = [];
|
||||
|
||||
final output = OutputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptPubKeyHex: "00",
|
||||
valueStringSats: amount.raw.toString(),
|
||||
addresses: [
|
||||
addressTo,
|
||||
],
|
||||
walletOwns: addressTo == myAddress,
|
||||
);
|
||||
final input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptSigHex: null,
|
||||
scriptSigAsm: null,
|
||||
sequence: null,
|
||||
outpoint: null,
|
||||
addresses: [myAddress],
|
||||
valueStringSats: amount.raw.toString(),
|
||||
witness: null,
|
||||
innerRedeemScriptAsm: null,
|
||||
coinbase: null,
|
||||
walletOwns: true,
|
||||
);
|
||||
|
||||
outputs.add(output);
|
||||
inputs.add(input);
|
||||
|
||||
final tempTx = TransactionV2(
|
||||
walletId: walletId,
|
||||
blockHash: null,
|
||||
hash: txData.txHash!,
|
||||
txid: txData.txid!,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
height: null,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
version: -1,
|
||||
type: addressTo == myAddress
|
||||
? TransactionType.sentToSelf
|
||||
: TransactionType.outgoing,
|
||||
subType: TransactionSubType.ethToken,
|
||||
otherData: jsonEncode(otherData),
|
||||
);
|
||||
|
||||
return txData.copyWith(
|
||||
tempTx: tempTx,
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@override
|
||||
|
@ -205,7 +266,10 @@ class EthTokenWallet extends Wallet {
|
|||
@override
|
||||
Future<TxData> confirmSend({required TxData txData}) async {
|
||||
try {
|
||||
return await ethWallet.confirmSend(txData: txData);
|
||||
return await ethWallet.confirmSend(
|
||||
txData: txData,
|
||||
prepareTempTx: _prepareTempTx,
|
||||
);
|
||||
} catch (e) {
|
||||
// rethrow to pass error in alert
|
||||
rethrow;
|
||||
|
|
401
lib/widgets/desktop/qr_code_scanner_dialog.dart
Normal file
401
lib/widgets/desktop/qr_code_scanner_dialog.dart
Normal file
|
@ -0,0 +1,401 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera_linux/camera_linux.dart';
|
||||
import 'package:camera_macos/camera_macos_arguments.dart';
|
||||
import 'package:camera_macos/camera_macos_device.dart';
|
||||
import 'package:camera_macos/camera_macos_platform_interface.dart';
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
import 'package:camera_windows/camera_windows.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../utilities/assets.dart';
|
||||
import '../../utilities/logger.dart';
|
||||
import '../../utilities/text_styles.dart';
|
||||
import 'desktop_dialog.dart';
|
||||
import 'desktop_dialog_close_button.dart';
|
||||
|
||||
class QrCodeScannerDialog extends StatefulWidget {
|
||||
final Function(String) onQrCodeDetected;
|
||||
|
||||
QrCodeScannerDialog({
|
||||
required this.onQrCodeDetected,
|
||||
});
|
||||
|
||||
@override
|
||||
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
|
||||
}
|
||||
|
||||
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||
final CameraLinux? _cameraLinuxPlugin =
|
||||
Platform.isLinux ? CameraLinux() : null;
|
||||
final CameraWindows? _cameraWindowsPlugin =
|
||||
Platform.isWindows ? CameraWindows() : null;
|
||||
bool _isCameraOpen = false;
|
||||
Image? _image;
|
||||
bool _isScanning = false;
|
||||
int _cameraId = -1;
|
||||
String? _macOSDeviceId;
|
||||
final int _imageDelayInMs = 250;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isCameraOpen = false;
|
||||
_isScanning = false;
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopCamera();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
try {
|
||||
setState(() {
|
||||
_isScanning = true; // Show the progress indicator
|
||||
});
|
||||
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
await _cameraLinuxPlugin!.initializeCamera();
|
||||
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
|
||||
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
|
||||
final List<CameraDescription> cameras =
|
||||
await _cameraWindowsPlugin!.availableCameras();
|
||||
if (cameras.isEmpty) {
|
||||
throw CameraException('No cameras available', 'No cameras found.');
|
||||
}
|
||||
final CameraDescription camera = cameras[0]; // Could be user-selected.
|
||||
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings(
|
||||
camera,
|
||||
const MediaSettings(
|
||||
resolutionPreset: ResolutionPreset.low,
|
||||
fps: 4,
|
||||
videoBitrate: 200000,
|
||||
enableAudio: false,
|
||||
),
|
||||
);
|
||||
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
|
||||
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
|
||||
// TODO [prio=low]: Make this work. ^^^
|
||||
Logging.instance.log("Windows Camera initialized with ID: $_cameraId",
|
||||
level: LogLevel.Info);
|
||||
} else if (Platform.isMacOS) {
|
||||
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
|
||||
.listDevices(deviceType: CameraMacOSDeviceType.video);
|
||||
if (videoDevices.isEmpty) {
|
||||
throw Exception('No cameras available');
|
||||
}
|
||||
_macOSDeviceId = videoDevices.first.deviceId;
|
||||
await CameraMacOS.instance
|
||||
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
|
||||
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
});
|
||||
|
||||
Logging.instance.log(
|
||||
"macOS Camera initialized with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
_isScanning = true;
|
||||
});
|
||||
}
|
||||
unawaited(_captureAndScanImage()); // Could be awaited.
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
|
||||
if (mounted) {
|
||||
// widget.onSnackbar("Failed to initialize camera. Please try again.");
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopCamera() async {
|
||||
try {
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
_cameraLinuxPlugin!.stopCamera();
|
||||
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
|
||||
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
|
||||
// if (_cameraId >= 0) {
|
||||
await _cameraWindowsPlugin!.dispose(_cameraId);
|
||||
Logging.instance.log("Windows Camera stopped with ID: $_cameraId",
|
||||
level: LogLevel.Info);
|
||||
// } else {
|
||||
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
|
||||
// level: LogLevel.Error);
|
||||
// }
|
||||
} else if (Platform.isMacOS) {
|
||||
// if (_macOSDeviceId != null) {
|
||||
await CameraMacOS.instance.stopImageStream();
|
||||
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
// } else {
|
||||
// Logging.instance.log("macOS Camera ID is null. Cannot stop.",
|
||||
// level: LogLevel.Error);
|
||||
// }
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
_isCameraOpen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _captureAndScanImage() async {
|
||||
while (_isCameraOpen && _isScanning) {
|
||||
try {
|
||||
String? base64Image;
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
base64Image = await _cameraLinuxPlugin!.captureImage();
|
||||
} else if (Platform.isWindows) {
|
||||
final XFile xfile =
|
||||
await _cameraWindowsPlugin!.takePicture(_cameraId);
|
||||
final bytes = await xfile.readAsBytes();
|
||||
base64Image = base64Encode(bytes);
|
||||
// We could use a Uint8List to optimize for Windows and macOS.
|
||||
} else if (Platform.isMacOS) {
|
||||
final macOSimg = await CameraMacOS.instance.takePicture();
|
||||
if (macOSimg == null) {
|
||||
Logging.instance
|
||||
.log("Failed to capture image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
final img.Image? image = img.decodeImage(macOSimg.bytes!);
|
||||
if (image == null) {
|
||||
Logging.instance
|
||||
.log("Failed to capture image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
base64Image = base64Encode(img.encodePng(image));
|
||||
}
|
||||
if (base64Image == null || base64Image.isEmpty) {
|
||||
// Logging.instance
|
||||
// .log("Failed to capture image", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
final img.Image? image = img.decodeImage(base64Decode(base64Image));
|
||||
// TODO [prio=low]: Optimize this process. Docs say:
|
||||
// > WARNING Since this will check the image data against all known
|
||||
// > decoders, it is much slower than using an explicit decoder
|
||||
if (image == null) {
|
||||
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_image = Image.memory(
|
||||
base64Decode(base64Image!),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final String? scanResult = await _scanImage(image);
|
||||
if (scanResult != null && scanResult.isNotEmpty) {
|
||||
widget.onQrCodeDetected(scanResult);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// Logging.instance.log("No QR code found in the image", level: LogLevel.Info);
|
||||
// if (mounted) {
|
||||
// widget.onSnackbar("No QR code found in the image.");
|
||||
// }
|
||||
// Spammy.
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
} catch (e, s) {
|
||||
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
|
||||
// if (mounted) {
|
||||
// widget.onSnackbar(
|
||||
// "Error capturing or scanning the image. Please try again.");
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _scanImage(img.Image image) async {
|
||||
try {
|
||||
final LuminanceSource source = RGBLuminanceSource(
|
||||
image.width,
|
||||
image.height,
|
||||
image
|
||||
.convert(numChannels: 4)
|
||||
.getBytes(order: img.ChannelOrder.abgr)
|
||||
.buffer
|
||||
.asInt32List(),
|
||||
);
|
||||
final BinaryBitmap bitmap =
|
||||
BinaryBitmap(GlobalHistogramBinarizer(source));
|
||||
|
||||
final QRCodeReader reader = QRCodeReader();
|
||||
final qrDecode = reader.decode(bitmap);
|
||||
if (qrDecode.text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return qrDecode.text;
|
||||
} catch (e, s) {
|
||||
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 696,
|
||||
maxHeight: 600,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Text(
|
||||
"Scan QR code",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _isCameraOpen
|
||||
? _image != null
|
||||
? _image!
|
||||
: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Center(
|
||||
child:
|
||||
CircularProgressIndicator(), // Show progress indicator immediately
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Container()),
|
||||
// "Select file" button.
|
||||
SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Select file",
|
||||
width: 200,
|
||||
onPressed: () async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["png", "jpg", "jpeg"],
|
||||
);
|
||||
|
||||
if (result == null || result.files.single.path == null) {
|
||||
await showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "No file selected",
|
||||
iconAsset: Assets.svg.file,
|
||||
context: context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final filePath = result?.files.single.path!;
|
||||
if (filePath == null) {
|
||||
await showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Error selecting file.",
|
||||
iconAsset: Assets.svg.file,
|
||||
context: context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final img.Image? image =
|
||||
img.decodeImage(File(filePath!).readAsBytesSync());
|
||||
if (image == null) {
|
||||
await showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Failed to decode image.",
|
||||
iconAsset: Assets.svg.file,
|
||||
context: context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final String? scanResult = await _scanImage(image);
|
||||
if (scanResult != null && scanResult.isNotEmpty) {
|
||||
widget.onQrCodeDetected(scanResult);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
await showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "No QR code found in the image.",
|
||||
iconAsset: Assets.svg.file,
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Failed to decode image: $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
await showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message:
|
||||
"Error processing the image. Please try again.",
|
||||
iconAsset: Assets.svg.file,
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Close button.
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Close",
|
||||
width: 272.5,
|
||||
onPressed: () {
|
||||
_stopCamera();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -8,6 +10,7 @@ import '../../utilities/logger.dart';
|
|||
import '../../utilities/text_styles.dart';
|
||||
import '../../utilities/util.dart';
|
||||
import '../conditional_parent.dart';
|
||||
import '../desktop/qr_code_scanner_dialog.dart';
|
||||
import '../icon_widgets/clipboard_icon.dart';
|
||||
import '../icon_widgets/qrcode_icon.dart';
|
||||
import '../icon_widgets/x_icon.dart';
|
||||
|
@ -71,6 +74,50 @@ class _FrostStepFieldState extends State<FrostStepField> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> scanQr() 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();
|
||||
|
||||
widget.controller.text = qrResult.rawContent;
|
||||
|
||||
_changed(widget.controller.text);
|
||||
} else {
|
||||
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return QrCodeScannerDialog(
|
||||
onQrCodeDetected: (qrCodeData) {
|
||||
try {
|
||||
// TODO [prio=low]: Validate QR code data.
|
||||
widget.controller.text = qrCodeData;
|
||||
|
||||
_changed(widget.controller.text);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Error processing QR code data: $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
|
@ -150,27 +197,7 @@ class _FrostStepFieldState extends State<FrostStepField> {
|
|||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: _qrKey,
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75),
|
||||
);
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
widget.controller.text = qrResult.rawContent;
|
||||
|
||||
_changed(widget.controller.text);
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: scanQr,
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -92,8 +92,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/s
|
|||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwallet2_api_c.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.dll"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.dll"
|
||||
COMPONENT Runtime)
|
||||
#install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.dll"
|
||||
# COMPONENT Runtime)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libpolyseed.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libpolyseed.dll"
|
||||
COMPONENT Runtime)
|
||||
|
@ -101,8 +101,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/s
|
|||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libssp-0.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libssp-0.dll"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll"
|
||||
COMPONENT Runtime)
|
||||
#install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll"
|
||||
# COMPONENT Runtime)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwinpthread-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwinpthread-1.dll"
|
||||
COMPONENT Runtime)
|
||||
|
|
Loading…
Reference in a new issue