mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-24 03:15:50 +00:00
commit
34ad1d9022
18 changed files with 1054 additions and 395 deletions
|
@ -228,26 +228,24 @@ class _RestoreFrostMsWalletViewState
|
|||
});
|
||||
} else {
|
||||
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
|
||||
await showDialog(
|
||||
final qrResult = await showDialog<String>(
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context) => const QrCodeScannerDialog(),
|
||||
);
|
||||
|
||||
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) {
|
||||
Logging.instance.log(
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
@ -26,6 +27,8 @@ import '../../../../utilities/assets.dart';
|
|||
import '../../../../utilities/constants.dart';
|
||||
import '../../../../utilities/enums/sync_type_enum.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/text_styles.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/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/desktop/qr_code_scanner_dialog.dart';
|
||||
import '../../../../widgets/desktop/secondary_button.dart';
|
||||
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
|
||||
import '../../../../widgets/icon_widgets/x_icon.dart';
|
||||
import '../../../../widgets/stack_dialog.dart';
|
||||
import '../../../../widgets/stack_text_field.dart';
|
||||
|
@ -73,6 +78,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
late final String? nodeId;
|
||||
late final bool isDesktop;
|
||||
|
||||
(NodeModel, String)? _scannedResult;
|
||||
|
||||
late bool saveEnabled;
|
||||
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
|
||||
void initState() {
|
||||
isDesktop = Util.isDesktop;
|
||||
|
@ -390,6 +479,35 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
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 &&
|
||||
ref
|
||||
.watch(
|
||||
|
@ -473,19 +591,47 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
const AppBarBackButton(
|
||||
iconSize: 24,
|
||||
size: 40,
|
||||
),
|
||||
Text(
|
||||
"Add new node",
|
||||
style: STextStyles.desktopH3(context),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
const AppBarBackButton(
|
||||
iconSize: 24,
|
||||
size: 40,
|
||||
),
|
||||
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(
|
||||
|
@ -504,7 +650,9 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
NodeForm(
|
||||
node: node,
|
||||
key: Key((node ?? _scannedResult?.$1)?.id ?? "none"),
|
||||
node: node ?? _scannedResult?.$1,
|
||||
scannedPw: _scannedResult?.$2,
|
||||
secureStore: ref.read(secureStoreProvider),
|
||||
readOnly: false,
|
||||
coin: widget.coin,
|
||||
|
@ -629,6 +777,7 @@ class NodeForm extends ConsumerStatefulWidget {
|
|||
const NodeForm({
|
||||
super.key,
|
||||
this.node,
|
||||
this.scannedPw,
|
||||
required this.secureStore,
|
||||
required this.readOnly,
|
||||
required this.coin,
|
||||
|
@ -636,6 +785,7 @@ class NodeForm extends ConsumerStatefulWidget {
|
|||
});
|
||||
|
||||
final NodeModel? node;
|
||||
final String? scannedPw;
|
||||
final SecureStorageInterface secureStore;
|
||||
final bool readOnly;
|
||||
final CryptoCurrency coin;
|
||||
|
@ -738,13 +888,15 @@ class _NodeFormState extends ConsumerState<NodeForm> {
|
|||
if (widget.node != null) {
|
||||
final node = widget.node!;
|
||||
if (enableAuthFields) {
|
||||
node.getPassword(widget.secureStore).then((value) {
|
||||
if (value is String) {
|
||||
_passwordController.text = value;
|
||||
}
|
||||
});
|
||||
|
||||
_usernameController.text = node.loginName ?? "";
|
||||
if (widget.scannedPw == null) {
|
||||
node.getPassword(widget.secureStore).then((value) {
|
||||
if (value is String) {
|
||||
_passwordController.text = value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_passwordController.text = widget.scannedPw!;
|
||||
}
|
||||
}
|
||||
|
||||
_nameController.text = node.name;
|
||||
|
|
|
@ -156,11 +156,13 @@ class _CNWalletKeysState extends State<CNWalletKeys> {
|
|||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
QR(
|
||||
data: _current(_currentDropDownValue),
|
||||
size:
|
||||
Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
if (_current(_currentDropDownValue) != "ERROR")
|
||||
QR(
|
||||
data: _current(_currentDropDownValue),
|
||||
size: Util.isDesktop
|
||||
? 256
|
||||
: MediaQuery.of(context).size.width / 1.5,
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 12 : 16,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import '../../../../utilities/constants.dart';
|
|||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../wallets/isar/models/wallet_info.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/multi_address_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 '../../../pinpad_views/lock_screen_view.dart';
|
||||
import 'delete_wallet_warning_view.dart';
|
||||
import 'edit_refresh_height_view.dart';
|
||||
import 'lelantus_settings_view.dart';
|
||||
import 'rbf_settings_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(
|
||||
height: 8,
|
||||
),
|
||||
|
|
|
@ -145,23 +145,25 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
|
||||
Future<void> scanWebcam() async {
|
||||
try {
|
||||
await showDialog<void>(
|
||||
final qrResult = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return QrCodeScannerDialog(
|
||||
onQrCodeDetected: (qrCodeData) {
|
||||
try {
|
||||
_processQrCodeData(qrCodeData);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error processing QR code data: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context) => const QrCodeScannerDialog(),
|
||||
);
|
||||
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) {
|
||||
Logging.instance.log(
|
||||
"Error opening QR code scanner dialog: $e\n$s",
|
||||
|
|
|
@ -313,108 +313,7 @@ class _UnlockWalletKeysDesktopState
|
|||
child: PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: continueEnabled,
|
||||
onPressed: continueEnabled
|
||||
? () 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,
|
||||
onPressed: continueEnabled ? enterPassphrase : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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/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 '../../../../providers/global/wallets_provider.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/nano_currency.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/view_only_option_interface.dart';
|
||||
import '../../../addresses/desktop_wallet_addresses_view.dart';
|
||||
|
@ -44,7 +46,8 @@ enum _WalletOptions {
|
|||
showXpub,
|
||||
lelantusCoins,
|
||||
sparkCoins,
|
||||
frostOptions;
|
||||
frostOptions,
|
||||
refreshFromHeight;
|
||||
|
||||
String get prettyName {
|
||||
switch (this) {
|
||||
|
@ -62,6 +65,8 @@ enum _WalletOptions {
|
|||
return "Spark Coins";
|
||||
case _WalletOptions.frostOptions:
|
||||
return "FROST settings";
|
||||
case _WalletOptions.refreshFromHeight:
|
||||
return "Refresh height";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +116,9 @@ class WalletOptionsButton extends ConsumerWidget {
|
|||
onFrostMSWalletOptionsPressed: () async {
|
||||
Navigator.of(context).pop(_WalletOptions.frostOptions);
|
||||
},
|
||||
onRefreshHeightPressed: () async {
|
||||
Navigator.of(context).pop(_WalletOptions.refreshFromHeight);
|
||||
},
|
||||
walletId: walletId,
|
||||
);
|
||||
},
|
||||
|
@ -243,6 +251,26 @@ class WalletOptionsButton extends ConsumerWidget {
|
|||
),
|
||||
);
|
||||
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.onFiroShowSparkCoins,
|
||||
required this.onFrostMSWalletOptionsPressed,
|
||||
required this.onRefreshHeightPressed,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
|
@ -288,6 +317,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
final VoidCallback onFiroShowLelantusCoins;
|
||||
final VoidCallback onFiroShowSparkCoins;
|
||||
final VoidCallback onFrostMSWalletOptionsPressed;
|
||||
final VoidCallback onRefreshHeightPressed;
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
|
@ -307,6 +337,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
final bool canChangeRep = coin is NanoCurrency;
|
||||
|
||||
final bool isFrost = coin is FrostCurrency;
|
||||
final bool isMoneroWow = wallet is LibMoneroWallet;
|
||||
|
||||
return Stack(
|
||||
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)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
|
|
|
@ -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_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/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/rbf_settings_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()}");
|
||||
|
||||
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 ============================================
|
||||
case CreatePasswordView.routeName:
|
||||
if (args is bool) {
|
||||
|
|
|
@ -61,7 +61,8 @@ Future<bool> checkElectrumServer({
|
|||
.timeout(Duration(seconds: (proxyInfo == null ? 5 : 30)));
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e\n$s", level: LogLevel.Debug);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
132
lib/utilities/node_uri_util.dart
Normal file
132
lib/utilities/node_uri_util.dart
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -298,14 +298,24 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
if (base == null || (oldInfo != null && oldInfo.name != walletId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CWKeyData(
|
||||
walletId: walletId,
|
||||
publicViewKey: base.getPublicViewKey(),
|
||||
privateViewKey: base.getPrivateViewKey(),
|
||||
publicSpendKey: base.getPublicSpendKey(),
|
||||
privateSpendKey: base.getPrivateSpendKey(),
|
||||
);
|
||||
try {
|
||||
return CWKeyData(
|
||||
walletId: walletId,
|
||||
publicViewKey: base.getPublicViewKey(),
|
||||
privateViewKey: base.getPrivateViewKey(),
|
||||
publicSpendKey: base.getPublicSpendKey(),
|
||||
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)>
|
||||
|
|
|
@ -23,14 +23,10 @@ import 'primary_button.dart';
|
|||
import 'secondary_button.dart';
|
||||
|
||||
class QrCodeScannerDialog extends StatefulWidget {
|
||||
final Function(String) onQrCodeDetected;
|
||||
|
||||
QrCodeScannerDialog({
|
||||
required this.onQrCodeDetected,
|
||||
});
|
||||
const QrCodeScannerDialog({super.key});
|
||||
|
||||
@override
|
||||
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
|
||||
State<QrCodeScannerDialog> createState() => _QrCodeScannerDialogState();
|
||||
}
|
||||
|
||||
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
||||
|
@ -43,39 +39,37 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
bool _isScanning = false;
|
||||
int _cameraId = -1;
|
||||
String? _macOSDeviceId;
|
||||
final int _imageDelayInMs = 250;
|
||||
final int _imageDelayInMs = Platform.isLinux ? 500 : 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
|
||||
_initializeCamera().then((camOpen) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && camOpen) {
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
});
|
||||
unawaited(_captureAndScanImage());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _initializeCamera() async {
|
||||
try {
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
await _cameraLinuxPlugin!.initializeCamera();
|
||||
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();
|
||||
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(
|
||||
_cameraId = await _cameraWindowsPlugin.createCameraWithSettings(
|
||||
camera,
|
||||
const MediaSettings(
|
||||
resolutionPreset: ResolutionPreset.low,
|
||||
|
@ -84,11 +78,13 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
enableAudio: false,
|
||||
),
|
||||
);
|
||||
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
|
||||
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);
|
||||
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);
|
||||
|
@ -99,43 +95,34 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
await CameraMacOS.instance
|
||||
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
|
||||
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
});
|
||||
|
||||
Logging.instance.log(
|
||||
"macOS Camera initialized with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
"macOS Camera initialized with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCameraOpen = true;
|
||||
_isScanning = true;
|
||||
});
|
||||
}
|
||||
unawaited(_captureAndScanImage()); // Could be awaited.
|
||||
|
||||
return true;
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopCamera() async {
|
||||
_isScanning = false;
|
||||
|
||||
try {
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
_cameraLinuxPlugin!.stopCamera();
|
||||
_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);
|
||||
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);
|
||||
|
@ -143,8 +130,10 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
} else if (Platform.isMacOS) {
|
||||
// if (_macOSDeviceId != null) {
|
||||
await CameraMacOS.instance.stopImageStream();
|
||||
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
|
||||
level: LogLevel.Info);
|
||||
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);
|
||||
|
@ -153,22 +142,16 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
} 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) {
|
||||
_isScanning = true;
|
||||
while (_isScanning) {
|
||||
try {
|
||||
String? base64Image;
|
||||
if (Platform.isLinux && _cameraLinuxPlugin != null) {
|
||||
base64Image = await _cameraLinuxPlugin!.captureImage();
|
||||
base64Image = await _cameraLinuxPlugin.captureImage();
|
||||
} else if (Platform.isWindows) {
|
||||
final XFile xfile =
|
||||
await _cameraWindowsPlugin!.takePicture(_cameraId);
|
||||
|
@ -180,14 +163,14 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
if (macOSimg == null) {
|
||||
Logging.instance
|
||||
.log("Failed to capture image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
await Future<void>.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));
|
||||
await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
base64Image = base64Encode(img.encodePng(image));
|
||||
|
@ -196,7 +179,7 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
// Logging.instance
|
||||
// .log("Failed to capture image", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
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
|
||||
if (image == null) {
|
||||
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -220,9 +203,10 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
|
||||
final String? scanResult = await _scanImage(image);
|
||||
if (scanResult != null && scanResult.isNotEmpty) {
|
||||
widget.onQrCodeDetected(scanResult);
|
||||
await _stopCamera();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop(scanResult);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
|
@ -233,8 +217,8 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
// Spammy.
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
} catch (e, s) {
|
||||
await Future<void>.delayed(Duration(milliseconds: _imageDelayInMs));
|
||||
} catch (e) {
|
||||
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
|
||||
|
@ -266,7 +250,7 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
return null;
|
||||
}
|
||||
return qrDecode.text;
|
||||
} catch (e, s) {
|
||||
} catch (e) {
|
||||
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
|
||||
// Spammy.
|
||||
return null;
|
||||
|
@ -275,126 +259,139 @@ class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
|
|||
|
||||
@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(
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (_, __) {
|
||||
_stopCamera();
|
||||
},
|
||||
child: DesktopDialog(
|
||||
maxWidth: 696,
|
||||
maxHeight: 600,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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();
|
||||
},
|
||||
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 (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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -91,24 +91,22 @@ class _FrostStepFieldState extends State<FrostStepField> {
|
|||
_changed(widget.controller.text);
|
||||
} else {
|
||||
// Platform.isLinux, Platform.isWindows, or Platform.isMacOS.
|
||||
await showDialog(
|
||||
final qrResult = await showDialog<String>(
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context) => const QrCodeScannerDialog(),
|
||||
);
|
||||
|
||||
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) {
|
||||
Logging.instance.log(
|
||||
|
|
30
pubspec.lock
30
pubspec.lock
|
@ -254,10 +254,11 @@ packages:
|
|||
camera_linux:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_linux
|
||||
sha256: "6ea08c23f643364e650e8fad73653747c049cbd00803a7c317132379ee3653ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: ecb412474c5d240347b04ac1eb9f019802ff7034
|
||||
resolved-ref: ecb412474c5d240347b04ac1eb9f019802ff7034
|
||||
url: "https://github.com/cypherstack/camera-linux"
|
||||
source: git
|
||||
version: "0.0.8"
|
||||
camera_macos:
|
||||
dependency: "direct main"
|
||||
|
@ -626,11 +627,12 @@ packages:
|
|||
devicelocale:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: devicelocale
|
||||
sha256: "0812b66f9eac57bc55c6ed4c178e0779440aa4e4e7c7e32fe1db02a758501d0e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
path: "."
|
||||
ref: ba7d7d87a3772e972adb1358a5ec9a111b514fce
|
||||
resolved-ref: ba7d7d87a3772e972adb1358a5ec9a111b514fce
|
||||
url: "https://github.com/cypherstack/flutter-devicelocale"
|
||||
source: git
|
||||
version: "0.8.1"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -683,8 +685,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "6bf385b2e1e18c8aa23783cb8afeabace299cf68"
|
||||
resolved-ref: "6bf385b2e1e18c8aa23783cb8afeabace299cf68"
|
||||
ref: f0b1300140d45c13e7722f8f8d20308efeba8449
|
||||
resolved-ref: f0b1300140d45c13e7722f8f8d20308efeba8449
|
||||
url: "https://github.com/cypherstack/electrum_adapter.git"
|
||||
source: git
|
||||
version: "3.0.0"
|
||||
|
@ -1979,8 +1981,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "534ec251b339199446b723c01a25d324ae7bb974"
|
||||
resolved-ref: "534ec251b339199446b723c01a25d324ae7bb974"
|
||||
ref: "752f054b65c500adb9cad578bf183a978e012502"
|
||||
resolved-ref: "752f054b65c500adb9cad578bf183a978e012502"
|
||||
url: "https://github.com/cypherstack/tor.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
@ -2186,7 +2188,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
|
|
|
@ -63,7 +63,7 @@ dependencies:
|
|||
tor_ffi_plugin:
|
||||
git:
|
||||
url: https://github.com/cypherstack/tor.git
|
||||
ref: 647cadc3c82c276dc07915b02d24538fd610f220
|
||||
ref: 752f054b65c500adb9cad578bf183a978e012502
|
||||
|
||||
fusiondart:
|
||||
git:
|
||||
|
@ -123,7 +123,10 @@ dependencies:
|
|||
barcode_scan2: ^4.3.3
|
||||
wakelock_plus: ^1.2.8
|
||||
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
|
||||
keyboard_dismisser: ^3.0.0
|
||||
another_flushbar: ^1.10.28
|
||||
|
@ -174,7 +177,7 @@ dependencies:
|
|||
electrum_adapter:
|
||||
git:
|
||||
url: https://github.com/cypherstack/electrum_adapter.git
|
||||
ref: 6bf385b2e1e18c8aa23783cb8afeabace299cf68
|
||||
ref: f0b1300140d45c13e7722f8f8d20308efeba8449
|
||||
stream_channel: ^2.1.0
|
||||
solana:
|
||||
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
|
||||
sqlite3: 2.4.3
|
||||
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
|
||||
camera_windows:
|
||||
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
|
||||
|
||||
dependency_overrides:
|
||||
# required to make devicelocale work
|
||||
web: ^0.5.0
|
||||
|
||||
# needed for dart 3.5+ (at least for now)
|
||||
win32: ^5.5.4
|
||||
|
@ -241,12 +250,6 @@ dependency_overrides:
|
|||
ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739
|
||||
path: coinlib_flutter
|
||||
|
||||
# adding here due to pure laziness
|
||||
tor_ffi_plugin:
|
||||
git:
|
||||
url: https://github.com/cypherstack/tor.git
|
||||
ref: 534ec251b339199446b723c01a25d324ae7bb974
|
||||
|
||||
bip47:
|
||||
git:
|
||||
url: https://github.com/cypherstack/bip47.git
|
||||
|
|
|
@ -25,7 +25,7 @@ cd "$LINUX_DIRECTORY" || exit 1
|
|||
#pip3 install --user meson markdown tomli --upgrade
|
||||
# pip3 install --user gi-docgen
|
||||
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
|
||||
git checkout $LIBSECRET_TAG
|
||||
if ! [ -x "$(command -v meson)" ]; then
|
||||
|
|
104
test/utilities/node_uri_util_test.dart
Normal file
104
test/utilities/node_uri_util_test.dart
Normal 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"
|
||||
"}",
|
||||
);
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue