diff --git a/asset_sources/default_themes/stack_duo/dark.zip b/asset_sources/default_themes/stack_duo/dark.zip index 8b31f4278..a44c89cf8 100644 Binary files a/asset_sources/default_themes/stack_duo/dark.zip and b/asset_sources/default_themes/stack_duo/dark.zip differ diff --git a/asset_sources/default_themes/stack_duo/light.zip b/asset_sources/default_themes/stack_duo/light.zip index 120573ccf..82dc05870 100644 Binary files a/asset_sources/default_themes/stack_duo/light.zip and b/asset_sources/default_themes/stack_duo/light.zip differ diff --git a/asset_sources/default_themes/stack_wallet/dark.zip b/asset_sources/default_themes/stack_wallet/dark.zip index 8b31f4278..a44c89cf8 100644 Binary files a/asset_sources/default_themes/stack_wallet/dark.zip and b/asset_sources/default_themes/stack_wallet/dark.zip differ diff --git a/asset_sources/default_themes/stack_wallet/light.zip b/asset_sources/default_themes/stack_wallet/light.zip index 120573ccf..82dc05870 100644 Binary files a/asset_sources/default_themes/stack_wallet/light.zip and b/asset_sources/default_themes/stack_wallet/light.zip differ diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 8947d6c46..cb239bc5e 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -101,7 +101,8 @@ class Address extends CryptoCurrencyAddress { } @override - String toString() => "{ " + String toString() => + "{ " "id: $id, " "walletId: $walletId, " "value: $value, " @@ -130,10 +131,7 @@ class Address extends CryptoCurrencyAddress { return jsonEncode(result); } - static Address fromJsonString( - String jsonString, { - String? overrideWalletId, - }) { + static Address fromJsonString(String jsonString, {String? overrideWalletId}) { final json = jsonDecode(jsonString); final derivationPathString = json["derivationPath"] as String?; @@ -176,7 +174,8 @@ enum AddressType { p2tr, solana, cardanoShelley, - xelis; + xelis, + fact0rn; String get readableName { switch (this) { @@ -216,6 +215,8 @@ enum AddressType { return "Cardano Shelley"; case AddressType.xelis: return "Xelis"; + case AddressType.fact0rn: + return "FACT0RN"; } } } diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 64deee9be..9b388a5a3 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -14,6 +13,7 @@ import '../../../../providers/providers.dart'; import '../../../../services/frost.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; @@ -212,7 +212,7 @@ class _RestoreFrostMsWalletViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await BarcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); configFieldController.text = qrResult.rawContent; @@ -238,11 +238,26 @@ class _RestoreFrostMsWalletViewState } } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code: ", + error: e, + stackTrace: s, + ); + } } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index 75bda4577..2ad57cf02 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -15,7 +15,6 @@ import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; @@ -50,7 +49,7 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { required this.coin, required this.restoreBlockHeight, this.enableLelantusScanning = false, - this.barcodeScanner = const BarcodeScannerWrapper(), + this.clipboard = const ClipboardWrapper(), }); @@ -60,7 +59,6 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { final CryptoCurrency coin; final int restoreBlockHeight; final bool enableLelantusScanning; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index a8e0f8218..d56a69a8c 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -78,7 +78,7 @@ class RestoreWalletView extends ConsumerStatefulWidget { required this.mnemonicPassphrase, required this.restoreBlockHeight, this.enableLelantusScanning = false, - this.barcodeScanner = const BarcodeScannerWrapper(), + this.clipboard = const ClipboardWrapper(), }); @@ -91,7 +91,6 @@ class RestoreWalletView extends ConsumerStatefulWidget { final int restoreBlockHeight; final bool enableLelantusScanning; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -111,8 +110,6 @@ class _RestoreWalletViewState extends ConsumerState { final List _inputStatuses = []; // final List _focusNodes = []; - late final BarcodeScannerInterface scanner; - late final TextSelectionControls textSelectionControls; bool _hideSeedWords = false; @@ -162,7 +159,6 @@ class _RestoreWalletViewState extends ConsumerState { ? CustomCupertinoTextSelectionControls(onPaste: onControlsPaste) : CustomMaterialTextSelectionControls(onPaste: onControlsPaste); - scanner = widget.barcodeScanner; for (int i = 0; i < _seedWordCount; i++) { _controllers.add(TextEditingController()); _inputStatuses.add(FormInputStatus.empty); @@ -608,7 +604,7 @@ class _RestoreWalletViewState extends ConsumerState { Future scanMnemonicQr() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); final results = AddressUtils.decodeQRSeedData(qrResult.rawContent); @@ -625,11 +621,26 @@ class _RestoreWalletViewState extends ConsumerState { } } on PlatformException catch (e, s) { // likely failed to get camera permissions - Logging.instance.e( - "Restore wallet qr scan failed: $e", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Restore wallet qr scan failed: $e", + error: e, + stackTrace: s, + ); + } } } diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 22ecb4407..84e9be9fa 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -21,7 +21,6 @@ import '../../../providers/ui/address_book_providers/contact_name_is_not_empty_s import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; @@ -43,13 +42,11 @@ import 'new_contact_address_entry_form.dart'; class AddAddressBookEntryView extends ConsumerStatefulWidget { const AddAddressBookEntryView({ super.key, - this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), }); static const String routeName = "/addAddressBookEntry"; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -63,7 +60,6 @@ class _AddAddressBookEntryViewState late final FocusNode nameFocusNode; late final ScrollController scrollController; - late final BarcodeScannerInterface scanner; late final ClipboardInterface clipboard; Emoji? _selectedEmoji; @@ -72,7 +68,7 @@ class _AddAddressBookEntryViewState @override initState() { ref.refresh(addressEntryDataProviderFamilyRefresher); - scanner = widget.barcodeScanner; + clipboard = widget.clipboard; nameController = TextEditingController(); @@ -114,7 +110,6 @@ class _AddAddressBookEntryViewState key: Key("contactAddressEntryForm_$id"), id: ref.read(addressEntryDataProvider(id)).id, clipboard: clipboard, - barcodeScanner: scanner, ), ); setState(() {}); diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index 2954c4d3f..374752024 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -18,7 +18,6 @@ import '../../../providers/ui/address_book_providers/address_entry_data_provider import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -33,7 +32,6 @@ class AddNewContactAddressView extends ConsumerStatefulWidget { const AddNewContactAddressView({ super.key, required this.contactId, - this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), }); @@ -41,7 +39,6 @@ class AddNewContactAddressView extends ConsumerStatefulWidget { final String contactId; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -53,13 +50,12 @@ class _AddNewContactAddressViewState extends ConsumerState { late final String contactId; - late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; @override void initState() { contactId = widget.contactId; - barcodeScanner = widget.barcodeScanner; + clipboard = widget.clipboard; super.initState(); @@ -173,11 +169,7 @@ class _AddNewContactAddressViewState ], ), const SizedBox(height: 16), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), + NewContactAddressEntryForm(id: 0, clipboard: clipboard), const SizedBox(height: 16), const Spacer(), Row( diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 774fac6be..652ff94e3 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -18,7 +18,6 @@ import '../../../providers/ui/address_book_providers/address_entry_data_provider import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -34,7 +33,7 @@ class EditContactAddressView extends ConsumerStatefulWidget { super.key, required this.contactId, required this.addressEntry, - this.barcodeScanner = const BarcodeScannerWrapper(), + this.clipboard = const ClipboardWrapper(), }); @@ -43,7 +42,6 @@ class EditContactAddressView extends ConsumerStatefulWidget { final String contactId; final ContactAddressEntry addressEntry; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -56,7 +54,6 @@ class _EditContactAddressViewState late final String contactId; late final ContactAddressEntry addressEntry; - late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; Future save(ContactEntry contact) async { @@ -97,7 +94,6 @@ class _EditContactAddressViewState void initState() { contactId = widget.contactId; addressEntry = widget.addressEntry; - barcodeScanner = widget.barcodeScanner; clipboard = widget.clipboard; super.initState(); @@ -211,11 +207,7 @@ class _EditContactAddressViewState ], ), const SizedBox(height: 16), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), + NewContactAddressEntryForm(id: 0, clipboard: clipboard), const SizedBox(height: 24), ConditionalParent( condition: isDesktop, diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index f1f399f13..818393aec 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -42,13 +42,11 @@ class NewContactAddressEntryForm extends ConsumerStatefulWidget { const NewContactAddressEntryForm({ super.key, required this.id, - required this.barcodeScanner, required this.clipboard, }); final int id; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -72,7 +70,7 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = false; - final qrResult = await widget.barcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); // Future.delayed( // const Duration(seconds: 2), @@ -102,9 +100,10 @@ class _NewContactAddressEntryFormState // now check for non standard encoded basic address } else if (ref.read(addressEntryDataProvider(widget.id)).coin != null) { - if (ref.read(addressEntryDataProvider(widget.id)).coin!.validateAddress( - qrResult.rawContent, - )) { + if (ref + .read(addressEntryDataProvider(widget.id)) + .coin! + .validateAddress(qrResult.rawContent)) { addressController.text = qrResult.rawContent; ref.read(addressEntryDataProvider(widget.id)).address = qrResult.rawContent; @@ -115,16 +114,39 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - Logging.instance.w("Failed to get camera permissions to scan address qr code: ", error: e, stackTrace: s); + + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions to scan address qr code: ", + error: e, + stackTrace: s, + ); + } } } @override void initState() { - addressLabelController = TextEditingController() - ..text = ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; - addressController = TextEditingController() - ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; + addressLabelController = + TextEditingController() + ..text = + ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; + addressController = + TextEditingController() + ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); coins = [...AppConfig.coins]; @@ -153,18 +175,17 @@ class _NewContactAddressEntryFormState final isDesktop = Util.isDesktop; if (isDesktop) { coins = [...AppConfig.coins]; - coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + coins.removeWhere((e) => e is Firo && e.network.isTestNet); final showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { coins = coins.toList(); } else { - coins = coins - .where((e) => e.network != CryptoCurrencyNetwork.test) - .toList(); + coins = + coins + .where((e) => e.network != CryptoCurrencyNetwork.test) + .toList(); } } @@ -181,24 +202,23 @@ class _NewContactAddressEntryFormState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), ), isExpanded: true, value: ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), ), onChanged: (value) { if (value is CryptoCurrency) { @@ -222,23 +242,20 @@ class _NewContactAddressEntryFormState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), height: 24, width: 24, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( coin.prettyName, - style: - STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -285,54 +302,54 @@ class _NewContactAddressEntryFormState mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), ) == null ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) : Row( - children: [ - SvgPicture.file( - File( - ref.watch( - coinIconProvider( - ref.watch( - addressEntryDataProvider(widget.id) - .select( - (value) => value.coin, - ), - )!, - ), + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider( + ref.watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), + )!, ), ), - height: 20, - width: 20, - ), - const SizedBox( - width: 12, ), - Text( - ref - .watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), - )! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + height: 20, + width: 20, + ), + const SizedBox(width: 12), + Text( + ref + .watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), + )! + .prettyName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), if (!isDesktop) SvgPicture.asset( Assets.svg.chevronDown, width: 8, height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), ], ), @@ -341,10 +358,7 @@ class _NewContactAddressEntryFormState ), ), ), - if (!AppConfig.isSingleCoinApp) - const SizedBox( - height: 8, - ), + if (!AppConfig.isSingleCoinApp) const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -361,25 +375,26 @@ class _NewContactAddressEntryFormState context, ).copyWith( labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: addressLabelController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - addressLabelController.text = ""; - }); - }, - ), - ], + suffixIcon: + addressLabelController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + addressLabelController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { ref.read(addressEntryDataProvider(widget.id)).addressLabel = @@ -388,9 +403,7 @@ class _NewContactAddressEntryFormState }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -410,8 +423,9 @@ class _NewContactAddressEntryFormState child: Row( children: [ if (ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) != null) TextFieldIconButton( @@ -425,8 +439,9 @@ class _NewContactAddressEntryFormState child: const XIcon(), ), if (ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) == null) TextFieldIconButton( @@ -438,8 +453,10 @@ class _NewContactAddressEntryFormState if (data?.text != null && data!.text!.isNotEmpty) { String content = data.text!.trim(); if (content.contains("\n")) { - content = - content.substring(0, content.indexOf("\n")); + content = content.substring( + 0, + content.indexOf("\n"), + ); } addressController.text = content; ref @@ -451,8 +468,9 @@ class _NewContactAddressEntryFormState ), if (!Util.isDesktop && ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) == null) TextFieldIconButton( @@ -460,9 +478,7 @@ class _NewContactAddressEntryFormState onTap: _onQrTapped, child: const QrCodeIcon(), ), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), ], ), ), @@ -485,21 +501,18 @@ class _NewContactAddressEntryFormState ), ), if (!ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.isValidAddress), + addressEntryDataProvider( + widget.id, + ).select((value) => value.isValidAddress), ) && addressController.text.isNotEmpty) Row( children: [ - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( "Invalid address", textAlign: TextAlign.left, diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index d68a02e4f..d62a2bdc4 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -64,13 +64,11 @@ class BuyForm extends ConsumerStatefulWidget { this.coin, this.tokenContract, this.clipboard = const ClipboardWrapper(), - this.scanner = const BarcodeScannerWrapper(), }); final CryptoCurrency? coin; final ClipboardInterface clipboard; - final BarcodeScannerInterface scanner; final EthContract? tokenContract; @override @@ -81,7 +79,6 @@ class _BuyFormState extends ConsumerState { late final CryptoCurrency? coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late final TextEditingController _receiveAddressController; late final TextEditingController _buyAmountController; @@ -162,13 +159,14 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexCryptos(); @@ -202,66 +200,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a crypto to buy", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a crypto to buy", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: CryptoSelectionView( - coins: coins, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: CryptoSelectionView(coins: coins), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => CryptoSelectionView( - coins: coins, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CryptoSelectionView(coins: coins), ), - ), - ); + ); if (mounted && result is Crypto) { onSelected(result); @@ -274,13 +268,14 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexFiats(); @@ -334,66 +329,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a fiat with which to pay", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a fiat with which to pay", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: FiatSelectionView( - fiats: fiats, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: FiatSelectionView(fiats: fiats), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => FiatSelectionView( - fiats: fiats, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FiatSelectionView(fiats: fiats), ), - ), - ); + ); if (mounted && result is Fiat) { onSelected(result); @@ -411,25 +402,28 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading quote data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading quote data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); quote = SimplexQuote( crypto: selectedCrypto!, fiat: selectedFiat!, - youPayFiatPrice: buyWithFiat - ? Decimal.parse(_buyAmountController.text) - : Decimal.parse("100"), // dummy value - youReceiveCryptoAmount: buyWithFiat - ? Decimal.parse("0.000420282") // dummy value - : Decimal.parse(_buyAmountController.text), // Ternary for this + youPayFiatPrice: + buyWithFiat + ? Decimal.parse(_buyAmountController.text) + : Decimal.parse("100"), // dummy value + youReceiveCryptoAmount: + buyWithFiat + ? Decimal.parse("0.000420282") // dummy value + : Decimal.parse(_buyAmountController.text), // Ternary for this id: "id", // anything; we get an ID back receivingAddress: _receiveAddressController.text, buyWithFiat: buyWithFiat, @@ -469,16 +463,12 @@ class _BuyFormState extends ConsumerState { "Simplex API unresponsive", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Text( "Simplex API unresponsive, please try again later", style: STextStyles.smallMed14(context), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -506,9 +496,10 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -558,16 +549,12 @@ class _BuyFormState extends ConsumerState { "Simplex API error", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Text( quoteResponse.exception!.errorMessage, style: STextStyles.smallMed14(context), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -596,9 +583,10 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -622,11 +610,9 @@ class _BuyFormState extends ConsumerState { } else { Logging.instance.d("_loadQuote: $response"); return BuyResponse( - exception: response.exception ?? - BuyException( - response.toString(), - BuyExceptionType.generic, - ), + exception: + response.exception ?? + BuyException(response.toString(), BuyExceptionType.generic), ); } } @@ -638,66 +624,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Preview quote", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Preview quote", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: BuyQuotePreviewView( - quote: quote, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: BuyQuotePreviewView(quote: quote), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BuyQuotePreviewView( - quote: quote, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BuyQuotePreviewView(quote: quote), ), - ), - ); + ); if (mounted && result is SimplexQuote) { onSelected(result); @@ -708,12 +690,10 @@ class _BuyFormState extends ConsumerState { try { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -743,13 +723,26 @@ class _BuyFormState extends ConsumerState { }); } } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } @@ -759,7 +752,6 @@ class _BuyFormState extends ConsumerState { _buyAmountController = TextEditingController(); clipboard = widget.clipboard; - scanner = widget.scanner; coins = ref.read(simplexProvider).supportedCryptos; fiats = ref.read(simplexProvider).supportedFiats; @@ -776,8 +768,10 @@ class _BuyFormState extends ConsumerState { ); // TODO enum this or something // TODO set defaults better; should probably explicitly enumerate the coins & fiats used and pull the specific ones we need rather than generating them as defaults here - selectedFiat = - Fiat.fromJson({'ticker': 'USD', 'name': 'United States Dollar'}); + selectedFiat = Fiat.fromJson({ + 'ticker': 'USD', + 'name': 'United States Dollar', + }); selectedCrypto = Crypto.fromJson({ 'ticker': widget.coin?.ticker ?? 'BTC', 'name': widget.coin?.prettyName ?? 'Bitcoin', @@ -815,24 +809,21 @@ class _BuyFormState extends ConsumerState { return ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 458, - child: child, - ), + builder: (child) => SizedBox(width: 458, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, - ), + builder: + (child) => LayoutBuilder( + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ), ), - ), - ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -843,9 +834,7 @@ class _BuyFormState extends ConsumerState { color: Theme.of(context).extension()!.textDark3, ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovering1 = true), @@ -855,16 +844,19 @@ class _BuyFormState extends ConsumerState { selectCrypto(); }, child: RoundedContainer( - padding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 2), - color: _hovering1 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering1 ? 0.3 : 0) - : Theme.of(context) - .extension()! - .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 2, + ), + color: + _hovering1 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering1 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.all(12), child: Row( @@ -873,9 +865,7 @@ class _BuyFormState extends ConsumerState { ticker: selectedCrypto?.ticker ?? "BTC", size: 20, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Text( selectedCrypto?.ticker ?? "ERR", @@ -884,9 +874,10 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: + Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -896,9 +887,7 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -912,9 +901,7 @@ class _BuyFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovering2 = true), @@ -924,16 +911,19 @@ class _BuyFormState extends ConsumerState { selectFiat(); }, child: RoundedContainer( - padding: - const EdgeInsets.symmetric(vertical: 3, horizontal: 2), - color: _hovering2 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering2 ? 0.3 : 0) - : Theme.of(context) - .extension()! - .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + vertical: 3, + horizontal: 2, + ), + color: + _hovering2 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering2 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.only( left: 12.0, @@ -949,9 +939,10 @@ class _BuyFormState extends ConsumerState { horizontal: 6, ), decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, + color: + Theme.of( + context, + ).extension()!.currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -960,22 +951,19 @@ class _BuyFormState extends ConsumerState { ), textAlign: TextAlign.center, style: STextStyles.smallMed12(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), Text( selectedFiat?.ticker ?? 'ERR', style: STextStyles.largeMedium14(context), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Text( selectedFiat?.name ?? 'Error', @@ -984,9 +972,10 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: + Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -996,9 +985,7 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1021,9 +1008,7 @@ class _BuyFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -1037,12 +1022,13 @@ class _BuyFormState extends ConsumerState { // ? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00' // : _BuyFormState.minCrypto.toStringAsFixed(8), focusNode: _buyAmountFocusNode, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.left, // inputFormatters: [NumericalRangeFormatter()], onChanged: (_) { @@ -1060,9 +1046,10 @@ class _BuyFormState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1073,33 +1060,34 @@ class _BuyFormState extends ConsumerState { const SizedBox(width: 2), buyWithFiat ? Container( - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 6, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - format.simpleCurrencySymbol( - selectedFiat?.ticker.toUpperCase() ?? "ERR", - ), - textAlign: TextAlign.center, - style: - STextStyles.smallMed12(context).copyWith( - color: Theme.of(context) + padding: const EdgeInsets.symmetric( + vertical: 3, + horizontal: 6, + ), + decoration: BoxDecoration( + color: + Theme.of(context) .extension()! - .accentColorDark, - ), + .currencyListItemBG, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + selectedFiat?.ticker.toUpperCase() ?? "ERR", + ), + textAlign: TextAlign.center, + style: STextStyles.smallMed12(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), - ) - : CoinIconForTicker( - ticker: selectedCrypto?.ticker ?? "BTC", - size: 20, ), + ) + : CoinIconForTicker( + ticker: selectedCrypto?.ticker ?? "BTC", + size: 20, + ), SizedBox( width: buyWithFiat ? 8 : 10, ), // maybe make isDesktop-aware? @@ -1108,9 +1096,10 @@ class _BuyFormState extends ConsumerState { ? selectedFiat?.ticker ?? "ERR" : selectedCrypto?.ticker ?? "ERR", style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ], @@ -1125,65 +1114,63 @@ class _BuyFormState extends ConsumerState { children: [ _buyAmountController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "buyViewClearAmountFieldButtonKey", - ), - onTap: () { - // if (_BuyFormState.buyWithFiat) { - // _buyAmountController.text = _BuyFormState - // .minFiat - // .toStringAsFixed(2); - // } else { - // if (selectedCrypto?.ticker == - // _BuyFormState.boundedCryptoTicker) { - // _buyAmountController.text = _BuyFormState - // .minCrypto - // .toStringAsFixed(8); - // } - // } - _buyAmountController.text = ""; - validateAmount(); - }, - child: const XIcon(), - ) + key: const Key( + "buyViewClearAmountFieldButtonKey", + ), + onTap: () { + // if (_BuyFormState.buyWithFiat) { + // _buyAmountController.text = _BuyFormState + // .minFiat + // .toStringAsFixed(2); + // } else { + // if (selectedCrypto?.ticker == + // _BuyFormState.boundedCryptoTicker) { + // _buyAmountController.text = _BuyFormState + // .minCrypto + // .toStringAsFixed(8); + // } + // } + _buyAmountController.text = ""; + validateAmount(); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); + key: const Key( + "buyViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); - final amountString = - Decimal.tryParse(data?.text ?? ""); - if (amountString != null) { - _buyAmountController.text = - amountString.toString(); + final amountString = Decimal.tryParse( + data?.text ?? "", + ); + if (amountString != null) { + _buyAmountController.text = + amountString.toString(); - validateAmount(); - } - }, - child: _buyAmountController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + validateAmount(); + } + }, + child: + _buyAmountController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), ], ), ), ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), if (_amountOutOfRangeErrorString.isNotEmpty) Text( _amountOutOfRangeErrorString, style: STextStyles.errorSmall(context), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1205,41 +1192,37 @@ class _BuyFormState extends ConsumerState { ); Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) + ChooseFromStackView.routeName, + arguments: coin, + ) .then((value) async { - if (value is String) { - final wallet = ref.read(pWallets).getWallet(value); - - // _toController.text = manager.walletName; - // model.recipientAddress = - // await manager.currentReceivingAddress; - _receiveAddressController.text = - (await wallet.getCurrentReceivingAddress())! - .value; - - setState(() { - _addressToggleFlag = - _receiveAddressController.text.isNotEmpty; + if (value is String) { + final wallet = ref + .read(pWallets) + .getWallet(value); + + // _toController.text = manager.walletName; + // model.recipientAddress = + // await manager.currentReceivingAddress; + _receiveAddressController.text = + (await wallet.getCurrentReceivingAddress())! + .value; + + setState(() { + _addressToggleFlag = + _receiveAddressController.text.isNotEmpty; + }); + validateAmount(); + } }); - validateAmount(); - } - }); } catch (e, s) { - Logging.instance.w( - "", - error: e, - stackTrace: s, - ); + Logging.instance.w("", error: e, stackTrace: s); } }, ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1289,105 +1272,115 @@ class _BuyFormState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _receiveAddressController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _receiveAddressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "buyViewClearAddressFieldButtonKey", - ), - onTap: () { - _receiveAddressController.text = ""; - _address = ""; - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "buyViewClearAddressFieldButtonKey", + ), + onTap: () { + _receiveAddressController.text = ""; + _address = ""; + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - - _receiveAddressController.text = content; - _address = content; - - setState(() { - _addressToggleFlag = - _receiveAddressController - .text.isNotEmpty; - }); - } - }, - child: _receiveAddressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "buyViewPasteAddressFieldButtonKey", ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + + _receiveAddressController.text = content; + _address = content; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text + .isNotEmpty; + }); + } + }, + child: + _receiveAddressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_receiveAddressController.text.isEmpty && AppConfig.isStackCoin(selectedCrypto?.ticker) && isDesktop) TextFieldIconButton( key: const Key("buyViewAddressBookButtonKey"), onTap: () async { - final entry = - await showDialog( + final entry = await showDialog< + ContactAddressEntry? + >( context: context, - builder: (context) => DesktopDialog( - maxWidth: 696, - maxHeight: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3( - context, + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3( + context, + ), + ), ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: AppConfig.coins + .firstWhere( + (e) => + e.ticker + .toLowerCase() == + selectedCrypto!.ticker + .toString() + .toLowerCase(), + ), ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: AppConfig.coins.firstWhere( - (e) => - e.ticker.toLowerCase() == - selectedCrypto!.ticker - .toString() - .toLowerCase(), - ), - ), - ), - ], - ), - ), + ), ); if (entry != null) { @@ -1408,10 +1401,10 @@ class _BuyFormState extends ConsumerState { TextFieldIconButton( key: const Key("buyViewAddressBookButtonKey"), onTap: () { - Navigator.of(context, rootNavigator: isDesktop) - .pushNamed( - AddressBookView.routeName, - ); + Navigator.of( + context, + rootNavigator: isDesktop, + ).pushNamed(AddressBookView.routeName); }, child: const AddressBookIcon(), ), @@ -1429,20 +1422,17 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), if (_receivingAddressValidationErrorString.isNotEmpty) Text( _receivingAddressValidationErrorString, style: STextStyles.errorSmall(context), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, - enabled: _addressToggleFlag && + enabled: + _addressToggleFlag && _amountOutOfRangeErrorString.isEmpty && _buyAmountController.text.isNotEmpty, onPressed: () { diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 618bafb05..621b981ec 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -44,14 +44,12 @@ class Step2View extends ConsumerStatefulWidget { super.key, required this.model, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); static const String routeName = "/exchangeStep2"; final IncompleteExchangeModel model; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _Step2ViewState(); @@ -60,7 +58,6 @@ class Step2View extends ConsumerStatefulWidget { class _Step2ViewState extends ConsumerState { late final IncompleteExchangeModel model; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late final TextEditingController _toController; late final TextEditingController _refundController; @@ -72,7 +69,7 @@ class _Step2ViewState extends ConsumerState { void _onRefundQrTapped() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -100,17 +97,32 @@ class _Step2ViewState extends ConsumerState { }); } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } void _onToQrTapped() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -140,11 +152,26 @@ class _Step2ViewState extends ConsumerState { }); } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } @@ -152,7 +179,6 @@ class _Step2ViewState extends ConsumerState { void initState() { model = widget.model; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; _toController = TextEditingController(); _refundController = TextEditingController(); diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index dcd8e1282..1061eb128 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -41,14 +41,12 @@ class TransferOptionWidget extends ConsumerStatefulWidget { required this.walletId, required this.utxo, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); final String walletId; final UTXO utxo; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => @@ -58,7 +56,7 @@ class TransferOptionWidget extends ConsumerStatefulWidget { class _TransferOptionWidgetState extends ConsumerState { late final String walletId; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; + late final TextEditingController _addressController; late final FocusNode _addressFocusNode; @@ -71,9 +69,7 @@ class _TransferOptionWidgetState extends ConsumerState { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); try { final wallet = ref.read(pWallets).getWallet(walletId) as NamecoinWallet; @@ -129,11 +125,7 @@ class _TransferOptionWidgetState extends ConsumerState { final opName = wallet.getOpNameDataFrom(widget.utxo)!; - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final nameScriptHex = scriptNameUpdate(opName.fullname, opName.value); @@ -164,10 +156,7 @@ class _TransferOptionWidgetState extends ConsumerState { ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -179,15 +168,16 @@ class _TransferOptionWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -212,12 +202,13 @@ class _TransferOptionWidgetState extends ConsumerState { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -245,7 +236,7 @@ class _TransferOptionWidgetState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); final coin = ref.read(pWalletCoin(walletId)); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -271,14 +262,27 @@ class _TransferOptionWidgetState extends ConsumerState { _setValidAddressProviders(_address); } } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in" - " $runtimeType", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in" + " $runtimeType", + error: e, + stackTrace: s, + ); + } } } @@ -287,7 +291,7 @@ class _TransferOptionWidgetState extends ConsumerState { super.initState(); walletId = widget.walletId; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; + _addressController = TextEditingController(); _addressFocusNode = FocusNode(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -345,72 +349,64 @@ class _TransferOptionWidgetState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _addressController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _addressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressController.text.isNotEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "nameTransferClearAddressFieldButtonKey", - ), - onTap: () { - _addressController.text = ""; - _address = ""; - _setValidAddressProviders( - _address, - ); - setState(() {}); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "nameTransferClearAddressFieldButtonKey", + ), + onTap: () { + _addressController.text = ""; + _address = ""; + _setValidAddressProviders(_address); + setState(() {}); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "nameTransferPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf( - "\n", - ), - ); - } - - _addressController.text = content.trim(); - _address = content.trim(); - - _setValidAddressProviders( - _address, + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "nameTransferPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), ); } - }, - child: _addressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + + _addressController.text = content.trim(); + _address = content.trim(); + + _setValidAddressProviders(_address); + } + }, + child: + _addressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_addressController.text.isEmpty) TextFieldIconButton( semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", - key: const Key( - "nameTransferAddressBookButtonKey", - ), + key: const Key("nameTransferAddressBookButtonKey"), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -423,9 +419,7 @@ class _TransferOptionWidgetState extends ConsumerState { TextFieldIconButton( semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key( - "nameTransferScanQrButtonKey", - ), + key: const Key("nameTransferScanQrButtonKey"), onTap: _scanQr, child: const QrCodeIcon(), ), @@ -436,32 +430,28 @@ class _TransferOptionWidgetState extends ConsumerState { ), ), ), - SizedBox( - height: Util.isDesktop ? 42 : 16, - ), + SizedBox(height: Util.isDesktop ? 42 : 16), if (!Util.isDesktop) const Spacer(), ConditionalParent( condition: Util.isDesktop, - builder: (child) => Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: child, + builder: + (child) => Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox(width: 16), + Expanded(child: child), + ], ), - ], - ), child: PrimaryButton( label: "Transfer", enabled: _enableButton, @@ -470,10 +460,7 @@ class _TransferOptionWidgetState extends ConsumerState { onPressed: _preview, ), ), - if (!Util.isDesktop) - const SizedBox( - height: 16, - ), + if (!Util.isDesktop) const SizedBox(height: 16), ], ); } diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index 80c016245..e438e420c 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -9,7 +9,6 @@ import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../providers/global/wallets_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; @@ -33,14 +32,12 @@ class UpdateOptionWidget extends ConsumerStatefulWidget { required this.walletId, required this.utxo, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); final String walletId; final UTXO utxo; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _BuyDomainWidgetState(); @@ -84,9 +81,7 @@ class _BuyDomainWidgetState extends ConsumerState { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); final wallet = ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; @@ -144,11 +139,7 @@ class _BuyDomainWidgetState extends ConsumerState { final opName = wallet.getOpNameDataFrom(widget.utxo)!; - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final nameScriptHex = scriptNameUpdate(opName.fullname, newValue); @@ -179,10 +170,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -194,15 +182,16 @@ class _BuyDomainWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -227,12 +216,13 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Update failed", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Update failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -269,17 +259,13 @@ class _BuyDomainWidgetState extends ConsumerState { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ - Text( - "Edit value", - style: STextStyles.label(context), - ), - const SizedBox( - height: 6, - ), + Text("Edit value", style: STextStyles.label(context)), + const SizedBox(height: 6), TextField( controller: _controller, maxLines: null, @@ -296,9 +282,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -308,18 +292,17 @@ class _BuyDomainWidgetState extends ConsumerState { return Text( "$length/$valueMaxLength", style: STextStyles.w500_10(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), ); }, ), ], ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), if (!Util.isDesktop) const Spacer(), Row( children: [ @@ -327,15 +310,11 @@ class _BuyDomainWidgetState extends ConsumerState { child: SecondaryButton( label: "Cancel", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, + onPressed: + Navigator.of(context, rootNavigator: Util.isDesktop).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Update", @@ -346,10 +325,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ], ), - if (!Util.isDesktop) - const SizedBox( - height: 16, - ), + if (!Util.isDesktop) const SizedBox(height: 16), ], ); } diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index 5f9e7bb71..d74a17429 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -16,9 +16,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/paynym/paynym_account.dart'; import '../../providers/global/paynym_api_provider.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../widgets/conditional_parent.dart'; @@ -40,10 +42,7 @@ import 'subwidgets/featured_paynyms_widget.dart'; import 'subwidgets/paynym_card.dart'; class AddNewPaynymFollowView extends ConsumerStatefulWidget { - const AddNewPaynymFollowView({ - super.key, - required this.walletId, - }); + const AddNewPaynymFollowView({super.key, required this.walletId}); final String walletId; @@ -73,9 +72,7 @@ class _AddNewPaynymFollowViewState showDialog( barrierDismissible: false, context: context, - builder: (context) => const LoadingIndicator( - width: 200, - ), + builder: (context) => const LoadingIndicator(width: 200), ).then((_) => didPopLoading = true), ); @@ -104,12 +101,7 @@ class _AddNewPaynymFollowViewState if (data?.text != null && data!.text!.isNotEmpty) { String content = data.text!.trim(); if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf( - "\n", - ), - ); + content = content.substring(0, content.indexOf("\n")); } _searchString = content; @@ -129,7 +121,7 @@ class _AddNewPaynymFollowViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await const BarcodeScannerWrapper().scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); final pCodeString = qrResult.rawContent; @@ -141,6 +133,21 @@ class _AddNewPaynymFollowViewState offset: pCodeString.length, ); }); + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } } catch (_) { // scan failed } @@ -166,100 +173,95 @@ class _AddNewPaynymFollowViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => MasterScaffold( - isDesktop: isDesktop, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "New follow", - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), + builder: + (child) => MasterScaffold( + isDesktop: isDesktop, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "New follow", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ), ), ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "New follow", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "New follow", - style: STextStyles.desktopH3(context), + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), + child: child, ), - const DesktopDialogCloseButton(), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Text( "Featured PayNyms", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), - ), - const SizedBox( - height: 12, - ), - FeaturedPaynymsWidget( - walletId: widget.walletId, - ), - const SizedBox( - height: 24, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), + const SizedBox(height: 12), + FeaturedPaynymsWidget(walletId: widget.walletId), + const SizedBox(height: 24), Text( "Add new", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), - ), - const SizedBox( - height: 12, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), + const SizedBox(height: 12), if (isDesktop) Row( children: [ @@ -268,9 +270,10 @@ class _AddNewPaynymFollowViewState children: [ RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, height: 56, child: Center( child: TextField( @@ -286,9 +289,10 @@ class _AddNewPaynymFollowViewState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of(context) + .extension()! + .textFieldActiveText, // height: 1.8, ), decoration: InputDecoration( @@ -296,11 +300,9 @@ class _AddNewPaynymFollowViewState hoverColor: Colors.transparent, fillColor: Colors.transparent, contentPadding: const EdgeInsets.all(16), - hintStyle: - STextStyles.desktopTextFieldLabel(context) - .copyWith( - fontSize: 14, - ), + hintStyle: STextStyles.desktopTextFieldLabel( + context, + ).copyWith(fontSize: 14), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, errorBorder: InputBorder.none, @@ -313,30 +315,38 @@ class _AddNewPaynymFollowViewState children: [ _searchController.text.isNotEmpty ? TextFieldIconButton( - onTap: _clear, - child: RoundedContainer( - padding: - const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: const XIcon(), + onTap: _clear, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, ), - ) + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonBackSecondary, + child: const XIcon(), + ), + ) : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: RoundedContainer( - padding: - const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: const ClipboardIcon(), + key: const Key( + "paynymPasteAddressFieldButtonKey", + ), + onTap: _paste, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, ), + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonBackSecondary, + child: const ClipboardIcon(), ), + ), TextFieldIconButton( key: const Key( "paynymScanQrButtonKey", @@ -344,9 +354,10 @@ class _AddNewPaynymFollowViewState onTap: _scanQr, child: RoundedContainer( padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: const QrCodeIcon(), ), ), @@ -361,9 +372,7 @@ class _AddNewPaynymFollowViewState ], ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), PaynymSearchButton(onPressed: _search), ], ), @@ -396,16 +405,16 @@ class _AddNewPaynymFollowViewState children: [ _searchController.text.isNotEmpty ? TextFieldIconButton( - onTap: _clear, - child: const XIcon(), - ) + onTap: _clear, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: const ClipboardIcon(), + key: const Key( + "paynymPasteAddressFieldButtonKey", ), + onTap: _paste, + child: const ClipboardIcon(), + ), TextFieldIconButton( key: const Key("paynymScanQrButtonKey"), onTap: _scanQr, @@ -418,34 +427,27 @@ class _AddNewPaynymFollowViewState ), ), ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) - const SizedBox( - height: 12, - ), - if (!isDesktop) - SecondaryButton( - label: "Search", - onPressed: _search, - ), - if (_didSearch) - const SizedBox( - height: 20, - ), + SecondaryButton(label: "Search", onPressed: _search), + if (_didSearch) const SizedBox(height: 20), if (_didSearch && _searchResult == null) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Nothing found. Please check the payment code.", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.label(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), ), ], ), @@ -453,11 +455,12 @@ class _AddNewPaynymFollowViewState if (_didSearch && _searchResult != null) RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: PaynymCard( key: UniqueKey(), label: _searchResult!.nymName, diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index ba8b9dbd7..36122962c 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../providers/global/locale_provider.dart'; +import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/amount/amount.dart'; @@ -11,7 +11,6 @@ import '../../../utilities/amount/amount_formatter.dart'; import '../../../utilities/amount/amount_input_formatter.dart'; import '../../../utilities/amount/amount_unit.dart'; import '../../../utilities/barcode_scanner_interface.dart'; -import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/text_styles.dart'; @@ -25,12 +24,6 @@ import '../../../widgets/rounded_container.dart'; import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; -//TODO: move the following two providers elsewhere -final pClipboard = - Provider((ref) => const ClipboardWrapper()); -final pBarcodeScanner = - Provider((ref) => const BarcodeScannerWrapper()); - // final _pPrice = Provider.family((ref, coin) { // return ref.watch( // priceAnd24hChangeNotifierProvider @@ -40,8 +33,8 @@ final pBarcodeScanner = final pRecipient = StateProvider.family<({String address, Amount? amount})?, int>( - (ref, index) => null, -); + (ref, index) => null, + ); class Recipient extends ConsumerStatefulWidget { const Recipient({ @@ -79,8 +72,9 @@ class _RecipientState extends ConsumerState { void _updateRecipientData() { final address = addressController.text; - final amount = - ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text); + final amount = ref + .read(pAmountFormatter(widget.coin)) + .tryParse(amountController.text); ref.read(pRecipient(widget.index).notifier).state = ( address: address, @@ -91,9 +85,9 @@ class _RecipientState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse( - amountController.text, - ); + Amount? cryptoAmount = ref + .read(pAmountFormatter(widget.coin)) + .tryParse(amountController.text); if (cryptoAmount != null) { if (ref.read(pRecipient(widget.index))?.amount != null && ref.read(pRecipient(widget.index))?.amount == cryptoAmount) { @@ -124,11 +118,7 @@ class _RecipientState extends ConsumerState { try { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75, - ), - ); + await Future.delayed(const Duration(milliseconds: 75)); } final qrResult = await ref.read(pBarcodeScanner).scan(); @@ -150,14 +140,12 @@ class _RecipientState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: widget.coin.fractionDigits, - ); - amountController.text = - ref.read(pAmountFormatter(widget.coin)).format( - amount, - withUnitName: false, - ); + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: widget.coin.fractionDigits); + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format(amount, withUnitName: false); } } else { addressController.text = qrResult.rawContent.trim(); @@ -169,12 +157,27 @@ class _RecipientState extends ConsumerState { _updateRecipientData(); } on PlatformException catch (e, s) { - Logging.instance.e( - "Failed to get camera permissions while " - "trying to scan qr code in SendView: $e\n$s", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while " + "trying to scan qr code in SendView: $e\n$s", + error: e, + stackTrace: s, + ); + } } } @@ -221,9 +224,7 @@ class _RecipientState extends ConsumerState { @override Widget build(BuildContext context) { final String locale = ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), + localeServiceChangeNotifierProvider.select((value) => value.locale), ); return RoundedContainer( @@ -248,9 +249,7 @@ class _RecipientState extends ConsumerState { ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -281,72 +280,73 @@ class _RecipientState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _addressIsEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _addressIsEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_addressIsEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - addressController.text = ""; + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + addressController.text = ""; + + setState(() { + _addressIsEmpty = true; + }); + + _updateRecipientData(); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await ref + .read(pClipboard) + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + + addressController.text = content.trim(); setState(() { - _addressIsEmpty = true; + _addressIsEmpty = + addressController.text.isEmpty; }); _updateRecipientData(); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await ref - .read(pClipboard) - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - - addressController.text = content.trim(); - - setState(() { - _addressIsEmpty = - addressController.text.isEmpty; - }); - - _updateRecipientData(); - } - }, - child: _addressIsEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + } + }, + child: + _addressIsEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_addressIsEmpty) TextFieldIconButton( - semanticsLabel: "Scan QR Button. " + semanticsLabel: + "Scan QR Button. " "Opens Camera For Scanning QR Code.", - key: const Key( - "sendViewScanQrButtonKey", - ), + key: const Key("sendViewScanQrButtonKey"), onTap: _onQrTapped, child: const QrCodeIcon(), ), @@ -357,9 +357,7 @@ class _RecipientState extends ConsumerState { ), ), ), - SizedBox( - height: isSingle ? 12 : 8, - ), + SizedBox(height: isSingle ? 12 : 8), if (isSingle) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -380,10 +378,7 @@ class _RecipientState extends ConsumerState { // ), ], ), - if (isSingle) - const SizedBox( - height: 8, - ), + if (isSingle) const SizedBox(height: 8), TextField( autocorrect: false, enableSuggestions: false, @@ -396,12 +391,13 @@ class _RecipientState extends ConsumerState { onChanged: (_) { _updateRecipientData(); }, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -411,14 +407,9 @@ class _RecipientState extends ConsumerState { ), ], decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), + contentPadding: const EdgeInsets.only(top: 12, right: 12), hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + hintStyle: STextStyles.fieldLabel(context).copyWith(fontSize: 14), prefixIcon: FittedBox( fit: BoxFit.scaleDown, child: Padding( @@ -428,9 +419,10 @@ class _RecipientState extends ConsumerState { .watch(pAmountUnit(widget.coin)) .unitForCoin(widget.coin), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 4ea893ea7..d2806d513 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -83,7 +83,6 @@ class SendView extends ConsumerStatefulWidget { required this.coin, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), this.accountLite, }); @@ -93,7 +92,6 @@ class SendView extends ConsumerStatefulWidget { final CryptoCurrency coin; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -111,7 +109,6 @@ class _SendViewState extends ConsumerState { late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -271,7 +268,7 @@ class _SendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); // Future.delayed( // const Duration(seconds: 2), @@ -307,13 +304,27 @@ class _SendViewState extends ConsumerState { // shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } } } @@ -1055,7 +1066,6 @@ class _SendViewState extends ConsumerState { _data = widget.autoFillData; walletId = widget.walletId; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; isStellar = coin is Stellar; isFiro = coin is Firo; isEth = coin is Ethereum; diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 8d9f41327..635df7e89 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -69,7 +69,6 @@ class TokenSendView extends ConsumerStatefulWidget { required this.tokenContract, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); static const String routeName = "/tokenSendView"; @@ -79,7 +78,6 @@ class TokenSendView extends ConsumerStatefulWidget { final EthContract tokenContract; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _TokenSendViewState(); @@ -90,7 +88,6 @@ class _TokenSendViewState extends ConsumerState { late final CryptoCurrency coin; late final EthContract tokenContract; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -154,7 +151,7 @@ class _TokenSendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); // Future.delayed( // const Duration(seconds: 2), @@ -223,13 +220,26 @@ class _TokenSendViewState extends ConsumerState { // shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } } } @@ -581,7 +591,6 @@ class _TokenSendViewState extends ConsumerState { coin = widget.coin; tokenContract = widget.tokenContract; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index effd1bc90..98b80995d 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -10,7 +10,6 @@ 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'; @@ -24,6 +23,7 @@ import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; @@ -375,8 +375,23 @@ class _AddEditNodeViewState extends ConsumerState { } } else { try { - final result = await BarcodeScanner.scan(); + final result = await ref.read(pBarcodeScanner).scan(); await _processQrData(result.rawContent); + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } } catch (e, s) { Logging.instance.e("$runtimeType._scanQr()", error: e, stackTrace: s); } diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index 476a3eb24..1ff4405df 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -24,7 +24,6 @@ import '../../../themes/coin_icon_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; @@ -39,14 +38,13 @@ class DesktopPaynymSendDialog extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 7dd007a2e..609f1d757 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -37,7 +37,6 @@ import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/amount/amount_input_formatter.dart'; import '../../../../utilities/amount/amount_unit.dart'; import '../../../../utilities/assets.dart'; -import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; @@ -78,14 +77,13 @@ class DesktopSend extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -96,7 +94,6 @@ class _DesktopSendState extends ConsumerState { late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -894,7 +891,7 @@ class _DesktopSendState extends ConsumerState { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; + isStellar = coin is Stellar; sendToController = TextEditingController(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 48e8cb230..34071ea8e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -30,7 +30,6 @@ import '../../../../utilities/amount/amount.dart'; import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/amount/amount_input_formatter.dart'; import '../../../../utilities/amount/amount_unit.dart'; -import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; @@ -61,14 +60,13 @@ class DesktopTokenSend extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -79,7 +77,6 @@ class _DesktopTokenSendState extends ConsumerState { late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -423,7 +420,7 @@ class _DesktopTokenSendState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -588,7 +585,6 @@ class _DesktopTokenSendState extends ConsumerState { walletId = widget.walletId; coin = ref.read(pWallets).getWallet(walletId).info.coin; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); diff --git a/lib/providers/global/barcode_scanner_provider.dart b/lib/providers/global/barcode_scanner_provider.dart new file mode 100644 index 000000000..3623b20a1 --- /dev/null +++ b/lib/providers/global/barcode_scanner_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../utilities/barcode_scanner_interface.dart'; + +final pBarcodeScanner = Provider( + (ref) => const BarcodeScannerWrapper(), +); diff --git a/lib/providers/global/clipboard_provider.dart b/lib/providers/global/clipboard_provider.dart new file mode 100644 index 000000000..956d03c02 --- /dev/null +++ b/lib/providers/global/clipboard_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../utilities/clipboard_interface.dart'; + +final pClipboard = Provider( + (ref) => const ClipboardWrapper(), +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 90e0ced3a..3db18af2f 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -19,6 +19,8 @@ export './exchange/exchange_form_state_provider.dart'; export './exchange/exchange_send_from_wallet_id_provider.dart'; export './exchange/trade_note_service_provider.dart'; export './exchange/trade_sent_from_stack_lookup_provider.dart'; +export './global/barcode_scanner_provider.dart'; +export './global/clipboard_provider.dart'; export './global/duress_provider.dart'; export './global/locale_provider.dart'; export './global/node_service_provider.dart'; diff --git a/lib/services/price.dart b/lib/services/price.dart index 75a4ad664..8dc686fa1 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -36,6 +36,7 @@ class PriceAPI { Epiccash: "epic-cash", Ecash: "ecash", Ethereum: "ethereum", + Fact0rn: "fact0rn", Firo: "zcoin", Monero: "monero", Particl: "particl", diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index 542c6375e..3479f7ecf 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -31,7 +31,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 17; + static const _currentDefaultThemeVersion = 18; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); diff --git a/lib/utilities/barcode_scanner_interface.dart b/lib/utilities/barcode_scanner_interface.dart index 77df4904f..7579eedcf 100644 --- a/lib/utilities/barcode_scanner_interface.dart +++ b/lib/utilities/barcode_scanner_interface.dart @@ -8,7 +8,16 @@ * */ +import 'dart:io'; + import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../widgets/desktop/primary_button.dart'; +import '../widgets/desktop/secondary_button.dart'; +import '../widgets/stack_dialog.dart'; +import 'logger.dart'; abstract class BarcodeScannerInterface { Future scan({ScanOptions options = const ScanOptions()}); @@ -27,3 +36,52 @@ class BarcodeScannerWrapper implements BarcodeScannerInterface { } } } + +/// Check if cam perms permanently denied on mobile and open app settings +Future checkCamPermDeniedMobileAndOpenAppSettings( + BuildContext context, { + required Logging logging, +}) async { + if (Platform.isAndroid || Platform.isIOS) { + final status = await Permission.camera.status; + if (status == PermissionStatus.permanentlyDenied && context.mounted) { + final trySettings = await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Camera permissions required", + message: "Open settings?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + rightButton: PrimaryButton( + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ); + + if (trySettings == true) { + final success = await openAppSettings(); + if (!success) { + logging.e("Failed to open app settings"); + if (context.mounted) { + await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Could not open app settings", + message: "You will need manually go find your app settings", + rightButton: PrimaryButton( + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ); + } + } + } + } + } +} diff --git a/lib/wallets/crypto_currency/coins/fact0rn.dart b/lib/wallets/crypto_currency/coins/fact0rn.dart new file mode 100644 index 000000000..84fc902b5 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/fact0rn.dart @@ -0,0 +1,241 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_currency.dart'; + +class Fact0rn extends Bip39HDCurrency with ElectrumXCurrencyInterface { + Fact0rn(super.network) { + _idMain = "fact0rn"; + _uriScheme = "fact0rn"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "FACT0RN"; + _ticker = "FACT"; + case CryptoCurrencyNetwork.test: + _id = "fact0rnTestNet"; + _name = "tFACT0RN"; + _ticker = "tFACT"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + bool get torSupport => false; + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip84, + ]; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + + switch (networkParams.wifPrefix) { + case 0x80: + coinType = "42069"; // fact0rn mainnet + break; + case 0xef: + coinType = "1"; // fact0rn testnet + break; + default: + throw Exception("Invalid Fact0rn network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip84: + purpose = 84; + break; + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000), fractionDigits: fractionDigits); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "79cb40f8075b0e3dc2bc468c5ce2a7acbe0afd36c6c3d3a134ea692edac7de49"; + case CryptoCurrencyNetwork.test: + return "550bbf0a444d9f92189f067dd225f5b8a5d92587ebc2e8398d143236072580af"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ + required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType, + }) { + switch (derivePathType) { + case DerivePathType.bip84: + final addr = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2wpkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + int get minConfirms => 1; + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return coinlib.Network( + wifPrefix: 0x80, + p2pkhPrefix: 0x00, + p2shPrefix: 0x05, + privHDPrefix: 0x0488ade4, + pubHDPrefix: 0x0488b21e, + bech32Hrp: "fact", + messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + case CryptoCurrencyNetwork.test: + return coinlib.Network( + wifPrefix: 0xef, + p2pkhPrefix: 0x6f, + p2shPrefix: 0xc4, + privHDPrefix: 0x04358394, + pubHDPrefix: 0x043587cf, + bech32Hrp: "tfact", + messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "electrumx.fact0rn.io", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: false, + clearnetEnabled: true, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 12; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => true; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength, 24]; + + @override + AddressType get defaultAddressType => defaultDerivePathType.getAddressType(); + + @override + BigInt get satsPerCoin => BigInt.from(100000000); + + @override + int get targetBlockTimeSeconds => 1800; + + @override + DerivePathType get defaultDerivePathType => DerivePathType.bip84; + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + // "https://explorer.fact0rn.io/tx/$txid" doesn't show mempool transactions + return Uri.parse("https://factexplorer.io/tx/$txid"); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } + + @override + int get transactionVersion => 2; + + @override + BigInt get defaultFeeRate => BigInt.from(1000); +} diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 773128bbb..363022e63 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -12,6 +12,7 @@ export 'coins/dogecoin.dart'; export 'coins/ecash.dart'; export 'coins/epiccash.dart'; export 'coins/ethereum.dart'; +export 'coins/fact0rn.dart'; export 'coins/firo.dart'; export 'coins/litecoin.dart'; export 'coins/monero.dart'; diff --git a/lib/wallets/wallet/impl/fact0rn_wallet.dart b/lib/wallets/wallet/impl/fact0rn_wallet.dart new file mode 100644 index 000000000..14a9d4be8 --- /dev/null +++ b/lib/wallets/wallet/impl/fact0rn_wallet.dart @@ -0,0 +1,326 @@ +import 'package:isar/isar.dart'; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_wallet.dart'; +import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; + +class Fact0rnWallet + extends Bip39HDWallet + with ElectrumXInterface, ExtendedKeysInterface, CoinControlInterface { + Fact0rnWallet(CryptoCurrencyNetwork network) : super(Fact0rn(network) as T); + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Future updateTransactions() async { + // Get all addresses. + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); + + // Only parse new txs (not in db yet). + final List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + // Only tx to list once. + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + cryptoCurrency: cryptoCurrency, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + final TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Fact0rn has special outputs like deadpool bounties + announcements, but they're unsupported. + // This is where we would check for them. + // TODO: [prio=none] Check for special Fact0rn outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.e("Unexpected tx found (ignoring it)"); + Logging.instance.d("Unexpected tx found (ignoring it): $txData"); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: + txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + @override + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + bool blocked = false; + String? blockedReason; + + // check for bip47 notification + final outputs = jsonTX["vout"] as List; + for (final output in outputs) { + final List? scriptChunks = + (output['scriptPubKey']?['asm'] as String?)?.split(" "); + if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { + final blindedPaymentCode = scriptChunks![1]; + final bytes = blindedPaymentCode.toUint8ListFromHex; + + // https://en.bitcoin.it/wiki/BIP_0047#Sending + if (bytes.length == 80 && bytes.first == 1) { + blocked = true; + blockedReason = + "Paynym notification output. Incautious " + "handling of outputs from notification transactions " + "may cause unintended loss of privacy."; + break; + } + } + } + + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); + } + + // Typical SegWit estimation + @override + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB.toInt() / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 595c1088e..19923d45a 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -35,6 +35,7 @@ import 'impl/dogecoin_wallet.dart'; import 'impl/ecash_wallet.dart'; import 'impl/epiccash_wallet.dart'; import 'impl/ethereum_wallet.dart'; +import 'impl/fact0rn_wallet.dart'; import 'impl/firo_wallet.dart'; import 'impl/litecoin_wallet.dart'; import 'impl/monero_wallet.dart'; @@ -358,6 +359,9 @@ abstract class Wallet { case const (Ethereum): return EthereumWallet(net); + case const (Fact0rn): + return Fact0rnWallet(net); + case const (Firo): return FiroWallet(net); diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index 3c86f1fed..31364555b 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -1,10 +1,12 @@ import 'dart:io'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; +import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; @@ -16,7 +18,7 @@ import '../icon_widgets/qrcode_icon.dart'; import '../icon_widgets/x_icon.dart'; import '../textfield_icon_button.dart'; -class FrostStepField extends StatefulWidget { +class FrostStepField extends ConsumerStatefulWidget { const FrostStepField({ super.key, required this.controller, @@ -35,10 +37,10 @@ class FrostStepField extends StatefulWidget { final bool showQrScanOption; @override - State createState() => _FrostStepFieldState(); + ConsumerState createState() => _FrostStepFieldState(); } -class _FrostStepFieldState extends State { +class _FrostStepFieldState extends ConsumerState { final _xKey = UniqueKey(); final _pasteKey = UniqueKey(); late final Key? _qrKey; @@ -46,13 +48,8 @@ class _FrostStepFieldState extends State { bool _isEmpty = true; final _inputBorder = OutlineInputBorder( - borderSide: const BorderSide( - width: 0, - color: Colors.transparent, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + borderSide: const BorderSide(width: 0, color: Colors.transparent), + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); late final void Function(String) _changed; @@ -79,12 +76,10 @@ class _FrostStepFieldState extends State { if (Platform.isAndroid || Platform.isIOS) { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await BarcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); widget.controller.text = qrResult.rawContent; @@ -106,11 +101,26 @@ class _FrostStepFieldState extends State { } } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code: ", + error: e, + stackTrace: s, + ); + } } } @@ -118,19 +128,15 @@ class _FrostStepFieldState extends State { Widget build(BuildContext context) { return ConditionalParent( condition: widget.label != null, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - widget.label!, - style: STextStyles.w500_14(context), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(widget.label!, style: STextStyles.w500_14(context)), + const SizedBox(height: 4), + child, + ], ), - const SizedBox( - height: 4, - ), - child, - ], - ), child: TextField( controller: widget.controller, focusNode: widget.focusNode, @@ -141,53 +147,60 @@ class _FrostStepFieldState extends State { onChanged: _changed, decoration: InputDecoration( hintText: widget.hint, - fillColor: widget.focusNode.hasFocus - ? Theme.of(context).extension()!.textFieldActiveBG - : Theme.of(context).extension()!.textFieldDefaultBG, - hintStyle: Util.isDesktop - ? STextStyles.desktopTextFieldLabel(context) - : STextStyles.fieldLabel(context), + fillColor: + widget.focusNode.hasFocus + ? Theme.of( + context, + ).extension()!.textFieldActiveBG + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, + hintStyle: + Util.isDesktop + ? STextStyles.desktopTextFieldLabel(context) + : STextStyles.fieldLabel(context), enabledBorder: _inputBorder, focusedBorder: _inputBorder, errorBorder: _inputBorder, disabledBorder: _inputBorder, focusedErrorBorder: _inputBorder, suffixIcon: Padding( - padding: _isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_isEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Frost Step Field Input.", - key: _xKey, - onTap: () { - widget.controller.text = ""; - - _changed(widget.controller.text); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + _changed(widget.controller.text); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Frost Step Field Input.", - key: _pasteKey, - onTap: () async { - final ClipboardData? data = - await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text != null && data!.text!.isNotEmpty) { - widget.controller.text = data.text!.trim(); - } - - _changed(widget.controller.text); - }, - child: - _isEmpty ? const ClipboardIcon() : const XIcon(), - ), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + _changed(widget.controller.text); + }, + child: _isEmpty ? const ClipboardIcon() : const XIcon(), + ), if (_isEmpty && widget.showQrScanOption) TextFieldIconButton( semanticsLabel: diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 1968ed686..5aa170a98 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -63,6 +63,7 @@ final List _supportedCoins = List.unmodifiable([ Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main), Ethereum(CryptoCurrencyNetwork.main), + Fact0rn(CryptoCurrencyNetwork.main), Firo(CryptoCurrencyNetwork.main), Litecoin(CryptoCurrencyNetwork.main), Nano(CryptoCurrencyNetwork.main),