Merge pull request #1052 from cypherstack/julian

Various
This commit is contained in:
Diego Salazar 2024-12-13 10:52:03 -07:00 committed by GitHub
commit 34ad1d9022
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1054 additions and 395 deletions

View file

@ -228,26 +228,24 @@ class _RestoreFrostMsWalletViewState
}); });
} else { } else {
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS. // Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
await showDialog( final qrResult = await showDialog<String>(
context: context, context: context,
builder: (context) { builder: (context) => const QrCodeScannerDialog(),
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);
}
},
);
},
); );
if (qrResult == null) {
Logging.instance.log(
"Qr scanning cancelled",
level: LogLevel.Info,
);
} else {
// TODO [prio=low]: Validate QR code data.
configFieldController.text = qrResult;
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
}
} }
} on PlatformException catch (e, s) { } on PlatformException catch (e, s) {
Logging.instance.log( Logging.instance.log(

View file

@ -10,6 +10,7 @@
import 'dart:async'; import 'dart:async';
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -26,6 +27,8 @@ import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart'; import '../../../../utilities/constants.dart';
import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/enums/sync_type_enum.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/node_uri_util.dart';
import '../../../../utilities/test_node_connection.dart'; import '../../../../utilities/test_node_connection.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
import '../../../../utilities/tor_plain_net_option_enum.dart'; import '../../../../utilities/tor_plain_net_option_enum.dart';
@ -38,7 +41,9 @@ import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart'; import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_dialog.dart';
import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/stack_text_field.dart';
@ -73,6 +78,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
late final String? nodeId; late final String? nodeId;
late final bool isDesktop; late final bool isDesktop;
(NodeModel, String)? _scannedResult;
late bool saveEnabled; late bool saveEnabled;
late bool testConnectionEnabled; late bool testConnectionEnabled;
@ -330,6 +337,88 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
} }
} }
bool _scanLock = false;
void _scanQr() async {
if (_scanLock) return;
_scanLock = true;
try {
if (Util.isDesktop) {
try {
final qrResult = await showDialog<String>(
context: context,
builder: (context) => const QrCodeScannerDialog(),
);
if (qrResult == null) {
Logging.instance.log(
"Qr scanning cancelled",
level: LogLevel.Info,
);
} else {
try {
await _processQrData(qrResult);
} catch (e, s) {
Logging.instance.log(
"Error processing QR code data: $e\n$s",
level: LogLevel.Error,
);
}
}
} catch (e, s) {
Logging.instance.log(
"Error opening QR code scanner dialog: $e\n$s",
level: LogLevel.Error,
);
}
} else {
try {
final result = await BarcodeScanner.scan();
await _processQrData(result.rawContent);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Warning,
);
}
}
} finally {
_scanLock = false;
}
}
Future<void> _processQrData(String data) async {
try {
final nodeQrData = NodeQrUtil.decodeUri(data);
if (mounted) {
setState(() {
_scannedResult = (
NodeModel(
host: nodeQrData.host,
port: nodeQrData.port,
name: nodeQrData.label ?? "",
id: const Uuid().v1(),
useSSL: nodeQrData.scheme == "https",
enabled: true,
coinName: coin.identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: !nodeQrData.host.endsWith(".onion"),
loginName: (nodeQrData as LibMoneroNodeQrData?)?.user,
),
(nodeQrData as LibMoneroNodeQrData?)?.password ?? ""
);
});
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Warning,
);
}
}
@override @override
void initState() { void initState() {
isDesktop = Util.isDesktop; isDesktop = Util.isDesktop;
@ -390,6 +479,35 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
), ),
actions: [ actions: [
if (viewType == AddEditNodeViewType.add &&
coin
is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("qrNodeAppBarButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(context)
.extension<StackColors>()!
.background,
icon: QrCodeIcon(
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
onPressed: _scanQr,
),
),
),
if (viewType == AddEditNodeViewType.edit && if (viewType == AddEditNodeViewType.edit &&
ref ref
.watch( .watch(
@ -473,19 +591,47 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(
height: 8,
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const SizedBox( Row(
width: 8, children: [
), const SizedBox(
const AppBarBackButton( width: 8,
iconSize: 24, ),
size: 40, const AppBarBackButton(
), iconSize: 24,
Text( size: 40,
"Add new node", ),
style: STextStyles.desktopH3(context), Text(
"Add new node",
style: STextStyles.desktopH3(context),
),
],
), ),
if (coin
is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future
Padding(
padding: const EdgeInsets.only(right: 32),
child: AppBarIconButton(
size: 40,
color: isDesktop
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG
: Theme.of(context)
.extension<StackColors>()!
.background,
icon: const QrCodeIcon(
width: 21,
height: 21,
),
onPressed: _scanQr,
),
),
], ],
), ),
Padding( Padding(
@ -504,7 +650,9 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
NodeForm( NodeForm(
node: node, key: Key((node ?? _scannedResult?.$1)?.id ?? "none"),
node: node ?? _scannedResult?.$1,
scannedPw: _scannedResult?.$2,
secureStore: ref.read(secureStoreProvider), secureStore: ref.read(secureStoreProvider),
readOnly: false, readOnly: false,
coin: widget.coin, coin: widget.coin,
@ -629,6 +777,7 @@ class NodeForm extends ConsumerStatefulWidget {
const NodeForm({ const NodeForm({
super.key, super.key,
this.node, this.node,
this.scannedPw,
required this.secureStore, required this.secureStore,
required this.readOnly, required this.readOnly,
required this.coin, required this.coin,
@ -636,6 +785,7 @@ class NodeForm extends ConsumerStatefulWidget {
}); });
final NodeModel? node; final NodeModel? node;
final String? scannedPw;
final SecureStorageInterface secureStore; final SecureStorageInterface secureStore;
final bool readOnly; final bool readOnly;
final CryptoCurrency coin; final CryptoCurrency coin;
@ -738,13 +888,15 @@ class _NodeFormState extends ConsumerState<NodeForm> {
if (widget.node != null) { if (widget.node != null) {
final node = widget.node!; final node = widget.node!;
if (enableAuthFields) { if (enableAuthFields) {
node.getPassword(widget.secureStore).then((value) { if (widget.scannedPw == null) {
if (value is String) { node.getPassword(widget.secureStore).then((value) {
_passwordController.text = value; if (value is String) {
} _passwordController.text = value;
}); }
});
_usernameController.text = node.loginName ?? ""; } else {
_passwordController.text = widget.scannedPw!;
}
} }
_nameController.text = node.name; _nameController.text = node.name;

View file

@ -156,11 +156,13 @@ class _CNWalletKeysState extends State<CNWalletKeys> {
SizedBox( SizedBox(
height: Util.isDesktop ? 12 : 16, height: Util.isDesktop ? 12 : 16,
), ),
QR( if (_current(_currentDropDownValue) != "ERROR")
data: _current(_currentDropDownValue), QR(
size: data: _current(_currentDropDownValue),
Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5, size: Util.isDesktop
), ? 256
: MediaQuery.of(context).size.width / 1.5,
),
SizedBox( SizedBox(
height: Util.isDesktop ? 12 : 16, height: Util.isDesktop ? 12 : 16,
), ),

View file

@ -0,0 +1,238 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/db/main_db_provider.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
class EditRefreshHeightView extends ConsumerStatefulWidget {
const EditRefreshHeightView({
super.key,
required this.walletId,
});
static const String routeName = "/editRefreshHeightView";
final String walletId;
@override
ConsumerState<EditRefreshHeightView> createState() =>
_EditRefreshHeightViewState();
}
class _EditRefreshHeightViewState extends ConsumerState<EditRefreshHeightView> {
late final LibMoneroWallet _wallet;
late final TextEditingController _controller;
final _focusNode = FocusNode();
bool _saveLock = false;
void _save() async {
if (_saveLock) return;
_saveLock = true;
try {
String? errMessage;
try {
final newHeight = int.tryParse(_controller.text);
if (newHeight != null && newHeight >= 0) {
await _wallet.info.updateRestoreHeight(
newRestoreHeight: newHeight,
isar: ref.read(mainDBProvider).isar,
);
_wallet.libMoneroWallet!.setRefreshFromBlockHeight(newHeight);
} else {
errMessage = "Invalid height: ${_controller.text}";
}
} catch (e) {
errMessage = e.toString();
}
if (mounted) {
if (errMessage == null) {
Navigator.of(context).pop();
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Refresh height updated",
context: context,
),
);
} else {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: errMessage,
context: context,
),
);
}
}
} finally {
_saveLock = false;
}
}
@override
void initState() {
super.initState();
_wallet = ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet;
_controller = TextEditingController()
..text = _wallet.libMoneroWallet!.getRefreshFromBlockHeight().toString();
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) {
return DesktopDialog(
maxWidth: 500,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: child,
),
const SizedBox(
height: 32,
),
],
),
);
},
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) {
return Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Restore height",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("restoreHeightFieldKey"),
controller: _controller,
focusNode: _focusNode,
style: Util.isDesktop
? STextStyles.desktopTextMedium(context).copyWith(
height: 2,
)
: STextStyles.field(context),
enableSuggestions: false,
autocorrect: false,
autofocus: true,
onSubmitted: (_) => _save(),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Restore height",
_focusNode,
context,
).copyWith(
suffixIcon: _controller.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => SizedBox(
height: 70,
child: child,
),
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_controller.text = "";
});
},
),
],
),
),
),
)
: Util.isDesktop
? const SizedBox(
height: 70,
)
: null,
),
),
),
Util.isDesktop
? const SizedBox(
height: 32,
)
: const Spacer(),
PrimaryButton(
label: "Save",
onPressed: _save,
),
],
),
),
);
}
}

View file

@ -20,6 +20,7 @@ import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
import '../../../../wallets/isar/models/wallet_info.dart'; import '../../../../wallets/isar/models/wallet_info.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
@ -32,6 +33,7 @@ import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_dialog.dart';
import '../../../pinpad_views/lock_screen_view.dart'; import '../../../pinpad_views/lock_screen_view.dart';
import 'delete_wallet_warning_view.dart'; import 'delete_wallet_warning_view.dart';
import 'edit_refresh_height_view.dart';
import 'lelantus_settings_view.dart'; import 'lelantus_settings_view.dart';
import 'rbf_settings_view.dart'; import 'rbf_settings_view.dart';
import 'rename_wallet_view.dart'; import 'rename_wallet_view.dart';
@ -354,6 +356,42 @@ class _WalletSettingsWalletSettingsViewState
), ),
), ),
), ),
if (wallet is LibMoneroWallet)
const SizedBox(
height: 8,
),
if (wallet is LibMoneroWallet)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
EditRefreshHeightView.routeName,
arguments: widget.walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Restore height",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),

View file

@ -145,23 +145,25 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
Future<void> scanWebcam() async { Future<void> scanWebcam() async {
try { try {
await showDialog<void>( final qrResult = await showDialog<String>(
context: context, context: context,
builder: (context) { builder: (context) => const QrCodeScannerDialog(),
return QrCodeScannerDialog(
onQrCodeDetected: (qrCodeData) {
try {
_processQrCodeData(qrCodeData);
} catch (e, s) {
Logging.instance.log(
"Error processing QR code data: $e\n$s",
level: LogLevel.Error,
);
}
},
);
},
); );
if (qrResult == null) {
Logging.instance.log(
"Qr scanning cancelled",
level: LogLevel.Info,
);
} else {
try {
_processQrCodeData(qrResult);
} catch (e, s) {
Logging.instance.log(
"Error processing QR code data: $e\n$s",
level: LogLevel.Error,
);
}
}
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
"Error opening QR code scanner dialog: $e\n$s", "Error opening QR code scanner dialog: $e\n$s",

View file

@ -313,108 +313,7 @@ class _UnlockWalletKeysDesktopState
child: PrimaryButton( child: PrimaryButton(
label: "Continue", label: "Continue",
enabled: continueEnabled, enabled: continueEnabled,
onPressed: continueEnabled onPressed: continueEnabled ? enterPassphrase : null,
? () async {
unawaited(
showDialog(
context: context,
builder: (context) => const Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LoadingIndicator(
width: 200,
height: 200,
),
],
),
),
);
await Future<void>.delayed(
const Duration(seconds: 1),
);
final verified = await ref
.read(storageCryptoHandlerProvider)
.verifyPassphrase(passwordController.text);
if (verified) {
if (context.mounted) {
Navigator.of(context, rootNavigator: true)
.pop();
}
({String keys, String config})? frostData;
List<String>? words;
final wallet =
ref.read(pWallets).getWallet(widget.walletId);
// TODO: [prio=low] handle wallets that don't have a mnemonic
// All wallets currently are mnemonic based
if (wallet is! MnemonicInterface) {
if (wallet is BitcoinFrostWallet) {
frostData = (
keys: (await wallet.getSerializedKeys())!,
config: (await wallet.getMultisigConfig())!,
);
} else {
throw Exception("FIXME ~= see todo in code");
}
} else {
if (wallet is ViewOnlyOptionInterface &&
(wallet as ViewOnlyOptionInterface)
.isViewOnly) {
// TODO: is something needed here?
} else {
words = await wallet.getMnemonicAsWords();
}
}
KeyDataInterface? keyData;
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly) {
keyData = await wallet.getViewOnlyWalletData();
} else if (wallet is ExtendedKeysInterface) {
keyData = await wallet.getXPrivs();
} else if (wallet is LibMoneroWallet) {
keyData = await wallet.getKeys();
}
if (context.mounted) {
await Navigator.of(context)
.pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: (
mnemonic: words ?? [],
walletId: widget.walletId,
frostData: frostData,
keyData: keyData,
),
);
}
} else {
if (context.mounted) {
Navigator.of(context, rootNavigator: true)
.pop();
}
await Future<void>.delayed(
const Duration(milliseconds: 300),
);
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid passphrase!",
context: context,
),
);
}
}
}
: null,
), ),
), ),
], ],

View file

@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart';
import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import '../../../../providers/global/wallets_provider.dart'; import '../../../../providers/global/wallets_provider.dart';
import '../../../../route_generator.dart'; import '../../../../route_generator.dart';
@ -30,6 +31,7 @@ import '../../../../wallets/crypto_currency/coins/firo.dart';
import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../addresses/desktop_wallet_addresses_view.dart'; import '../../../addresses/desktop_wallet_addresses_view.dart';
@ -44,7 +46,8 @@ enum _WalletOptions {
showXpub, showXpub,
lelantusCoins, lelantusCoins,
sparkCoins, sparkCoins,
frostOptions; frostOptions,
refreshFromHeight;
String get prettyName { String get prettyName {
switch (this) { switch (this) {
@ -62,6 +65,8 @@ enum _WalletOptions {
return "Spark Coins"; return "Spark Coins";
case _WalletOptions.frostOptions: case _WalletOptions.frostOptions:
return "FROST settings"; return "FROST settings";
case _WalletOptions.refreshFromHeight:
return "Refresh height";
} }
} }
} }
@ -111,6 +116,9 @@ class WalletOptionsButton extends ConsumerWidget {
onFrostMSWalletOptionsPressed: () async { onFrostMSWalletOptionsPressed: () async {
Navigator.of(context).pop(_WalletOptions.frostOptions); Navigator.of(context).pop(_WalletOptions.frostOptions);
}, },
onRefreshHeightPressed: () async {
Navigator.of(context).pop(_WalletOptions.refreshFromHeight);
},
walletId: walletId, walletId: walletId,
); );
}, },
@ -243,6 +251,26 @@ class WalletOptionsButton extends ConsumerWidget {
), ),
); );
break; break;
case _WalletOptions.refreshFromHeight:
if (Util.isDesktop) {
unawaited(
showDialog(
context: context,
builder: (context) => EditRefreshHeightView(
walletId: walletId,
),
),
);
} else {
unawaited(
Navigator.of(context).pushNamed(
EditRefreshHeightView.routeName,
arguments: walletId,
),
);
}
break;
} }
} }
}, },
@ -278,6 +306,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
required this.onFiroShowLelantusCoins, required this.onFiroShowLelantusCoins,
required this.onFiroShowSparkCoins, required this.onFiroShowSparkCoins,
required this.onFrostMSWalletOptionsPressed, required this.onFrostMSWalletOptionsPressed,
required this.onRefreshHeightPressed,
required this.walletId, required this.walletId,
}); });
@ -288,6 +317,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final VoidCallback onFiroShowLelantusCoins; final VoidCallback onFiroShowLelantusCoins;
final VoidCallback onFiroShowSparkCoins; final VoidCallback onFiroShowSparkCoins;
final VoidCallback onFrostMSWalletOptionsPressed; final VoidCallback onFrostMSWalletOptionsPressed;
final VoidCallback onRefreshHeightPressed;
final String walletId; final String walletId;
@override @override
@ -307,6 +337,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final bool canChangeRep = coin is NanoCurrency; final bool canChangeRep = coin is NanoCurrency;
final bool isFrost = coin is FrostCurrency; final bool isFrost = coin is FrostCurrency;
final bool isMoneroWow = wallet is LibMoneroWallet;
return Stack( return Stack(
children: [ children: [
@ -509,6 +540,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
), ),
), ),
), ),
if (isMoneroWow)
const SizedBox(
height: 8,
),
if (isMoneroWow)
TransparentButton(
onPressed: onRefreshHeightPressed,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.addressBookDesktop,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
const SizedBox(width: 14),
Expanded(
child: Text(
_WalletOptions.refreshFromHeight.prettyName,
style: STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
),
),
if (xpubEnabled) if (xpubEnabled)
const SizedBox( const SizedBox(
height: 8, height: 8,

View file

@ -135,6 +135,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
@ -2140,6 +2141,20 @@ class RouteGenerator {
} }
return _routeError("${settings.name} invalid args: ${args.toString()}"); return _routeError("${settings.name} invalid args: ${args.toString()}");
case EditRefreshHeightView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => EditRefreshHeightView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
// == Desktop specific routes ============================================ // == Desktop specific routes ============================================
case CreatePasswordView.routeName: case CreatePasswordView.routeName:
if (args is bool) { if (args is bool) {

View file

@ -61,7 +61,8 @@ Future<bool> checkElectrumServer({
.timeout(Duration(seconds: (proxyInfo == null ? 5 : 30))); .timeout(Duration(seconds: (proxyInfo == null ? 5 : 30)));
return true; return true;
} catch (_) { } catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Debug);
return false; return false;
} }
} }

View file

@ -0,0 +1,132 @@
abstract interface class NodeQrData {
final String host;
final int port;
final String? label;
NodeQrData({required this.host, required this.port, this.label});
String encode();
String get scheme;
}
abstract class LibMoneroNodeQrData extends NodeQrData {
final String user;
final String password;
LibMoneroNodeQrData({
required super.host,
required super.port,
super.label,
required this.user,
required this.password,
});
@override
String encode() {
String? userInfo;
if (user.isNotEmpty) {
userInfo = user;
if (password.isNotEmpty) {
userInfo += ":$password";
}
}
final uri = Uri(
scheme: scheme,
userInfo: userInfo,
port: port,
host: host,
queryParameters: {"label": label},
);
return uri.toString();
}
@override
String toString() {
return "$runtimeType {"
"scheme: $scheme, "
"host: $host, "
"port: $port, "
"user: $user, "
"password: $password, "
"label: $label"
"}";
}
}
class MoneroNodeQrData extends LibMoneroNodeQrData {
MoneroNodeQrData({
required super.host,
required super.port,
required super.user,
required super.password,
super.label,
});
@override
String get scheme => "xmrrpc";
}
class WowneroNodeQrData extends LibMoneroNodeQrData {
WowneroNodeQrData({
required super.host,
required super.port,
required super.user,
required super.password,
super.label,
});
@override
String get scheme => "wowrpc";
}
abstract final class NodeQrUtil {
static ({String? user, String? password}) _parseUserInfo(String? userInfo) {
if (userInfo == null || userInfo.isEmpty) {
return (user: null, password: null);
}
final splitIndex = userInfo.indexOf(":");
if (splitIndex == -1) {
return (user: userInfo, password: null);
}
return (
user: userInfo.substring(0, splitIndex),
password: userInfo.substring(splitIndex + 1),
);
}
static NodeQrData decodeUri(String uriString) {
final uri = Uri.tryParse(uriString);
if (uri == null) throw Exception("Invalid uri string.");
if (!uri.hasAuthority) throw Exception("Uri has no authority.");
final userInfo = _parseUserInfo(uri.userInfo);
final query = uri.queryParameters;
switch (uri.scheme) {
case "xmrrpc":
return MoneroNodeQrData(
host: uri.host,
port: uri.port,
user: userInfo.user ?? "",
password: userInfo.password ?? "",
label: query["label"],
);
case "wowrpc":
return WowneroNodeQrData(
host: uri.host,
port: uri.port,
user: userInfo.user ?? "",
password: userInfo.password ?? "",
label: query["label"],
);
default:
throw Exception("Unknown node uri scheme \"${uri.scheme}\" found.");
}
}
}

View file

@ -298,14 +298,24 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
if (base == null || (oldInfo != null && oldInfo.name != walletId)) { if (base == null || (oldInfo != null && oldInfo.name != walletId)) {
return null; return null;
} }
try {
return CWKeyData( return CWKeyData(
walletId: walletId, walletId: walletId,
publicViewKey: base.getPublicViewKey(), publicViewKey: base.getPublicViewKey(),
privateViewKey: base.getPrivateViewKey(), privateViewKey: base.getPrivateViewKey(),
publicSpendKey: base.getPublicSpendKey(), publicSpendKey: base.getPublicSpendKey(),
privateSpendKey: base.getPrivateSpendKey(), privateSpendKey: base.getPrivateSpendKey(),
); );
} catch (e, s) {
Logging.instance.log("getKeys failed: $e\n$s", level: LogLevel.Fatal);
return CWKeyData(
walletId: walletId,
publicViewKey: "ERROR",
privateViewKey: "ERROR",
publicSpendKey: "ERROR",
privateSpendKey: "ERROR",
);
}
} }
Future<(String, String)> Future<(String, String)>

View file

@ -23,14 +23,10 @@ import 'primary_button.dart';
import 'secondary_button.dart'; import 'secondary_button.dart';
class QrCodeScannerDialog extends StatefulWidget { class QrCodeScannerDialog extends StatefulWidget {
final Function(String) onQrCodeDetected; const QrCodeScannerDialog({super.key});
QrCodeScannerDialog({
required this.onQrCodeDetected,
});
@override @override
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState(); State<QrCodeScannerDialog> createState() => _QrCodeScannerDialogState();
} }
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> { class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
@ -43,39 +39,37 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
bool _isScanning = false; bool _isScanning = false;
int _cameraId = -1; int _cameraId = -1;
String? _macOSDeviceId; String? _macOSDeviceId;
final int _imageDelayInMs = 250; final int _imageDelayInMs = Platform.isLinux ? 500 : 250;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isCameraOpen = false;
_isScanning = false;
_initializeCamera();
}
@override _initializeCamera().then((camOpen) {
void dispose() { WidgetsBinding.instance.addPostFrameCallback((_) {
_stopCamera(); if (mounted && camOpen) {
super.dispose(); setState(() {
} _isCameraOpen = true;
});
Future<void> _initializeCamera() async { unawaited(_captureAndScanImage());
try { }
setState(() {
_isScanning = true; // Show the progress indicator
}); });
});
}
Future<bool> _initializeCamera() async {
try {
if (Platform.isLinux && _cameraLinuxPlugin != null) { if (Platform.isLinux && _cameraLinuxPlugin != null) {
await _cameraLinuxPlugin!.initializeCamera(); await _cameraLinuxPlugin.initializeCamera();
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info); Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) { } else if (Platform.isWindows && _cameraWindowsPlugin != null) {
final List<CameraDescription> cameras = final List<CameraDescription> cameras =
await _cameraWindowsPlugin!.availableCameras(); await _cameraWindowsPlugin.availableCameras();
if (cameras.isEmpty) { if (cameras.isEmpty) {
throw CameraException('No cameras available', 'No cameras found.'); throw CameraException('No cameras available', 'No cameras found.');
} }
final CameraDescription camera = cameras[0]; // Could be user-selected. final CameraDescription camera = cameras[0]; // Could be user-selected.
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings( _cameraId = await _cameraWindowsPlugin.createCameraWithSettings(
camera, camera,
const MediaSettings( const MediaSettings(
resolutionPreset: ResolutionPreset.low, resolutionPreset: ResolutionPreset.low,
@ -84,11 +78,13 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
enableAudio: false, enableAudio: false,
), ),
); );
await _cameraWindowsPlugin!.initializeCamera(_cameraId); await _cameraWindowsPlugin.initializeCamera(_cameraId);
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first; // await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
// TODO [prio=low]: Make this work. ^^^ // TODO [prio=low]: Make this work. ^^^
Logging.instance.log("Windows Camera initialized with ID: $_cameraId", Logging.instance.log(
level: LogLevel.Info); "Windows Camera initialized with ID: $_cameraId",
level: LogLevel.Info,
);
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
.listDevices(deviceType: CameraMacOSDeviceType.video); .listDevices(deviceType: CameraMacOSDeviceType.video);
@ -99,43 +95,34 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
await CameraMacOS.instance await CameraMacOS.instance
.initialize(cameraMacOSMode: CameraMacOSMode.photo); .initialize(cameraMacOSMode: CameraMacOSMode.photo);
setState(() {
_isCameraOpen = true;
});
Logging.instance.log( Logging.instance.log(
"macOS Camera initialized with ID: $_macOSDeviceId", "macOS Camera initialized with ID: $_macOSDeviceId",
level: LogLevel.Info); level: LogLevel.Info,
);
} }
if (mounted) {
setState(() { return true;
_isCameraOpen = true;
_isScanning = true;
});
}
unawaited(_captureAndScanImage()); // Could be awaited.
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error); .log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
if (mounted) { return false;
// widget.onSnackbar("Failed to initialize camera. Please try again.");
setState(() {
_isScanning = false;
});
}
} }
} }
Future<void> _stopCamera() async { Future<void> _stopCamera() async {
_isScanning = false;
try { try {
if (Platform.isLinux && _cameraLinuxPlugin != null) { if (Platform.isLinux && _cameraLinuxPlugin != null) {
_cameraLinuxPlugin!.stopCamera(); _cameraLinuxPlugin.stopCamera();
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info); Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) { } else if (Platform.isWindows && _cameraWindowsPlugin != null) {
// if (_cameraId >= 0) { // if (_cameraId >= 0) {
await _cameraWindowsPlugin!.dispose(_cameraId); await _cameraWindowsPlugin.dispose(_cameraId);
Logging.instance.log("Windows Camera stopped with ID: $_cameraId", Logging.instance.log(
level: LogLevel.Info); "Windows Camera stopped with ID: $_cameraId",
level: LogLevel.Info,
);
// } else { // } else {
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.", // Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
// level: LogLevel.Error); // level: LogLevel.Error);
@ -143,8 +130,10 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
// if (_macOSDeviceId != null) { // if (_macOSDeviceId != null) {
await CameraMacOS.instance.stopImageStream(); await CameraMacOS.instance.stopImageStream();
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId", Logging.instance.log(
level: LogLevel.Info); "macOS Camera stopped with ID: $_macOSDeviceId",
level: LogLevel.Info,
);
// } else { // } else {
// Logging.instance.log("macOS Camera ID is null. Cannot stop.", // Logging.instance.log("macOS Camera ID is null. Cannot stop.",
// level: LogLevel.Error); // level: LogLevel.Error);
@ -153,22 +142,16 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error); .log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
} finally {
if (mounted) {
setState(() {
_isScanning = false;
_isCameraOpen = false;
});
}
} }
} }
Future<void> _captureAndScanImage() async { Future<void> _captureAndScanImage() async {
while (_isCameraOpen && _isScanning) { _isScanning = true;
while (_isScanning) {
try { try {
String? base64Image; String? base64Image;
if (Platform.isLinux && _cameraLinuxPlugin != null) { if (Platform.isLinux && _cameraLinuxPlugin != null) {
base64Image = await _cameraLinuxPlugin!.captureImage(); base64Image = await _cameraLinuxPlugin.captureImage();
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
final XFile xfile = final XFile xfile =
await _cameraWindowsPlugin!.takePicture(_cameraId); await _cameraWindowsPlugin!.takePicture(_cameraId);
@ -180,14 +163,14 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
if (macOSimg == null) { if (macOSimg == null) {
Logging.instance Logging.instance
.log("Failed to capture image", level: LogLevel.Error); .log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs)); await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
continue; continue;
} }
final img.Image? image = img.decodeImage(macOSimg.bytes!); final img.Image? image = img.decodeImage(macOSimg.bytes!);
if (image == null) { if (image == null) {
Logging.instance Logging.instance
.log("Failed to capture image", level: LogLevel.Error); .log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs)); await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
continue; continue;
} }
base64Image = base64Encode(img.encodePng(image)); base64Image = base64Encode(img.encodePng(image));
@ -196,7 +179,7 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
// Logging.instance // Logging.instance
// .log("Failed to capture image", level: LogLevel.Error); // .log("Failed to capture image", level: LogLevel.Error);
// Spammy. // Spammy.
await Future.delayed(Duration(milliseconds: _imageDelayInMs)); await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
continue; continue;
} }
final img.Image? image = img.decodeImage(base64Decode(base64Image)); final img.Image? image = img.decodeImage(base64Decode(base64Image));
@ -205,7 +188,7 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
// > decoders, it is much slower than using an explicit decoder // > decoders, it is much slower than using an explicit decoder
if (image == null) { if (image == null) {
Logging.instance.log("Failed to decode image", level: LogLevel.Error); Logging.instance.log("Failed to decode image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs)); await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
continue; continue;
} }
@ -220,9 +203,10 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
final String? scanResult = await _scanImage(image); final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) { if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult); await _stopCamera();
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop(scanResult);
} }
break; break;
} else { } else {
@ -233,8 +217,8 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
// Spammy. // Spammy.
} }
await Future.delayed(Duration(milliseconds: _imageDelayInMs)); await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
} catch (e, s) { } catch (e) {
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error); // Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
// Spammy. // Spammy.
@ -266,7 +250,7 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
return null; return null;
} }
return qrDecode.text; return qrDecode.text;
} catch (e, s) { } catch (e) {
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error); // Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
// Spammy. // Spammy.
return null; return null;
@ -275,126 +259,139 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DesktopDialog( return PopScope(
maxWidth: 696, onPopInvokedWithResult: (_, __) {
maxHeight: 600, _stopCamera();
child: Column( },
mainAxisSize: MainAxisSize.min, child: DesktopDialog(
children: [ maxWidth: 696,
Row( maxHeight: 600,
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Padding( children: [
padding: const EdgeInsets.only(left: 32), Row(
child: Text( mainAxisAlignment: MainAxisAlignment.spaceBetween,
"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: [ children: [
Expanded(child: Container()), Padding(
// "Select file" button. padding: const EdgeInsets.only(left: 32),
SecondaryButton( child: Text(
buttonHeight: ButtonHeight.l, "Scan QR code",
label: "Select file", style: STextStyles.desktopH3(context),
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();
},
), ),
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 (context.mounted) {
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 (context.mounted) {
if (scanResult != null && scanResult.isNotEmpty) {
Navigator.of(context).pop(scanResult);
} 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,
);
if (context.mounted) {
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: () {
Navigator.of(context).pop();
},
),
],
),
),
],
),
), ),
); );
} }

View file

@ -91,24 +91,22 @@ class _FrostStepFieldState extends State<FrostStepField> {
_changed(widget.controller.text); _changed(widget.controller.text);
} else { } else {
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS. // Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
await showDialog( final qrResult = await showDialog<String>(
context: context, context: context,
builder: (context) { builder: (context) => const QrCodeScannerDialog(),
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);
}
},
);
},
); );
if (qrResult == null) {
Logging.instance.log(
"Qr scanning cancelled",
level: LogLevel.Info,
);
} else {
// TODO [prio=low]: Validate QR code data.
widget.controller.text = qrResult;
_changed(widget.controller.text);
}
} }
} on PlatformException catch (e, s) { } on PlatformException catch (e, s) {
Logging.instance.log( Logging.instance.log(

View file

@ -254,10 +254,11 @@ packages:
camera_linux: camera_linux:
dependency: "direct main" dependency: "direct main"
description: description:
name: camera_linux path: "."
sha256: "6ea08c23f643364e650e8fad73653747c049cbd00803a7c317132379ee3653ac" ref: ecb412474c5d240347b04ac1eb9f019802ff7034
url: "https://pub.dev" resolved-ref: ecb412474c5d240347b04ac1eb9f019802ff7034
source: hosted url: "https://github.com/cypherstack/camera-linux"
source: git
version: "0.0.8" version: "0.0.8"
camera_macos: camera_macos:
dependency: "direct main" dependency: "direct main"
@ -626,11 +627,12 @@ packages:
devicelocale: devicelocale:
dependency: "direct main" dependency: "direct main"
description: description:
name: devicelocale path: "."
sha256: "0812b66f9eac57bc55c6ed4c178e0779440aa4e4e7c7e32fe1db02a758501d0e" ref: ba7d7d87a3772e972adb1358a5ec9a111b514fce
url: "https://pub.dev" resolved-ref: ba7d7d87a3772e972adb1358a5ec9a111b514fce
source: hosted url: "https://github.com/cypherstack/flutter-devicelocale"
version: "0.7.1" source: git
version: "0.8.1"
dio: dio:
dependency: transitive dependency: transitive
description: description:
@ -683,8 +685,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "6bf385b2e1e18c8aa23783cb8afeabace299cf68" ref: f0b1300140d45c13e7722f8f8d20308efeba8449
resolved-ref: "6bf385b2e1e18c8aa23783cb8afeabace299cf68" resolved-ref: f0b1300140d45c13e7722f8f8d20308efeba8449
url: "https://github.com/cypherstack/electrum_adapter.git" url: "https://github.com/cypherstack/electrum_adapter.git"
source: git source: git
version: "3.0.0" version: "3.0.0"
@ -1979,8 +1981,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "534ec251b339199446b723c01a25d324ae7bb974" ref: "752f054b65c500adb9cad578bf183a978e012502"
resolved-ref: "534ec251b339199446b723c01a25d324ae7bb974" resolved-ref: "752f054b65c500adb9cad578bf183a978e012502"
url: "https://github.com/cypherstack/tor.git" url: "https://github.com/cypherstack/tor.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -2186,7 +2188,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
web: web:
dependency: transitive dependency: "direct overridden"
description: description:
name: web name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"

View file

@ -63,7 +63,7 @@ dependencies:
tor_ffi_plugin: tor_ffi_plugin:
git: git:
url: https://github.com/cypherstack/tor.git url: https://github.com/cypherstack/tor.git
ref: 647cadc3c82c276dc07915b02d24538fd610f220 ref: 752f054b65c500adb9cad578bf183a978e012502
fusiondart: fusiondart:
git: git:
@ -123,7 +123,10 @@ dependencies:
barcode_scan2: ^4.3.3 barcode_scan2: ^4.3.3
wakelock_plus: ^1.2.8 wakelock_plus: ^1.2.8
intl: ^0.17.0 intl: ^0.17.0
devicelocale: ^0.7.1 devicelocale:
git:
url: https://github.com/cypherstack/flutter-devicelocale
ref: ba7d7d87a3772e972adb1358a5ec9a111b514fce
device_info_plus: ^10.1.2 device_info_plus: ^10.1.2
keyboard_dismisser: ^3.0.0 keyboard_dismisser: ^3.0.0
another_flushbar: ^1.10.28 another_flushbar: ^1.10.28
@ -174,7 +177,7 @@ dependencies:
electrum_adapter: electrum_adapter:
git: git:
url: https://github.com/cypherstack/electrum_adapter.git url: https://github.com/cypherstack/electrum_adapter.git
ref: 6bf385b2e1e18c8aa23783cb8afeabace299cf68 ref: f0b1300140d45c13e7722f8f8d20308efeba8449
stream_channel: ^2.1.0 stream_channel: ^2.1.0
solana: solana:
git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream.
@ -184,7 +187,11 @@ dependencies:
calendar_date_picker2: ^1.0.2 calendar_date_picker2: ^1.0.2
sqlite3: 2.4.3 sqlite3: 2.4.3
sqlite3_flutter_libs: 0.5.22 sqlite3_flutter_libs: 0.5.22
camera_linux: ^0.0.8 # camera_linux: ^0.0.8
camera_linux:
git:
url: https://github.com/cypherstack/camera-linux
ref: ecb412474c5d240347b04ac1eb9f019802ff7034
zxing2: ^0.2.3 zxing2: ^0.2.3
camera_windows: camera_windows:
git: # TODO [prio=low]: Revert to official after https://github.com/flutter/packages/pull/7067. git: # TODO [prio=low]: Revert to official after https://github.com/flutter/packages/pull/7067.
@ -224,6 +231,8 @@ flutter_native_splash:
android_disable_fullscreen: true android_disable_fullscreen: true
dependency_overrides: dependency_overrides:
# required to make devicelocale work
web: ^0.5.0
# needed for dart 3.5+ (at least for now) # needed for dart 3.5+ (at least for now)
win32: ^5.5.4 win32: ^5.5.4
@ -241,12 +250,6 @@ dependency_overrides:
ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739 ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739
path: coinlib_flutter path: coinlib_flutter
# adding here due to pure laziness
tor_ffi_plugin:
git:
url: https://github.com/cypherstack/tor.git
ref: 534ec251b339199446b723c01a25d324ae7bb974
bip47: bip47:
git: git:
url: https://github.com/cypherstack/bip47.git url: https://github.com/cypherstack/bip47.git

View file

@ -25,7 +25,7 @@ cd "$LINUX_DIRECTORY" || exit 1
#pip3 install --user meson markdown tomli --upgrade #pip3 install --user meson markdown tomli --upgrade
# pip3 install --user gi-docgen # pip3 install --user gi-docgen
cd build || exit 1 cd build || exit 1
git -C libsecret pull origin $LIBSECRET_TAG || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret git -C libsecret pull origin $LIBSECRET_TAG || git clone https://git.cypherstack.com/Cypher_Stack/libsecret.git libsecret
cd libsecret || exit 1 cd libsecret || exit 1
git checkout $LIBSECRET_TAG git checkout $LIBSECRET_TAG
if ! [ -x "$(command -v meson)" ]; then if ! [ -x "$(command -v meson)" ]; then

View file

@ -0,0 +1,104 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stackwallet/utilities/node_uri_util.dart';
void main() {
test("Valid xmrrpc scheme node uri", () {
expect(
NodeQrUtil.decodeUri(
"xmrrpc://nodo:password@bob.onion:18083?label=Nodo Tor Node",
),
isA<MoneroNodeQrData>(),
);
});
test("Valid wowrpc scheme node uri", () {
expect(
NodeQrUtil.decodeUri(
"wowrpc://nodo:password@10.0.0.10:18083",
),
isA<WowneroNodeQrData>(),
);
});
test("Invalid authority node uri", () {
String? message;
try {
NodeQrUtil.decodeUri(
"nodo:password@bob.onion:18083?label=Nodo Tor Node",
);
} catch (e) {
message = e.toString();
}
expect(message, "Exception: Uri has no authority.");
});
test("Empty uri string", () {
String? message;
try {
NodeQrUtil.decodeUri("");
} catch (e) {
message = e.toString();
}
expect(message, "Exception: Uri has no authority.");
});
test("Invalid uri string", () {
String? message;
try {
NodeQrUtil.decodeUri("::invalid@@@.ok");
} catch (e) {
message = e.toString();
}
expect(message, "Exception: Invalid uri string.");
});
test("Unknown uri string", () {
String? message;
try {
NodeQrUtil.decodeUri("http://u:p@host.com:80/lol?hmm=42");
} catch (e) {
message = e.toString();
}
expect(message, "Exception: Unknown node uri scheme \"http\" found.");
});
test("decoding to model", () {
final data = NodeQrUtil.decodeUri(
"xmrrpc://nodo:password@bob.onion:18083?label=Nodo+Tor+Node",
);
expect(data.scheme, "xmrrpc");
expect(data.host, "bob.onion");
expect(data.port, 18083);
expect(data.label, "Nodo Tor Node");
expect((data as MoneroNodeQrData?)?.user, "nodo");
expect((data as MoneroNodeQrData?)?.password, "password");
});
test("encoding to string", () {
const validString =
"xmrrpc://nodo:password@bob.onion:18083?label=Nodo+Tor+Node";
final data = NodeQrUtil.decodeUri(
validString,
);
expect(data.encode(), validString);
});
test("normal to string", () {
const validString =
"xmrrpc://nodo:password@bob.onion:18083?label=Nodo+Tor+Node";
final data = NodeQrUtil.decodeUri(
validString,
);
expect(
data.toString(),
"MoneroNodeQrData {"
"scheme: xmrrpc, "
"host: bob.onion, "
"port: 18083, "
"user: nodo, "
"password: password, "
"label: Nodo Tor Node"
"}",
);
});
}