diff --git a/asset_sources/svg/campfire/socials/twitter-brands.svg b/asset_sources/svg/campfire/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/campfire/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/campfire/socials/x.svg b/asset_sources/svg/campfire/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/campfire/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_duo/socials/twitter-brands.svg b/asset_sources/svg/stack_duo/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/stack_duo/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/stack_duo/socials/x.svg b/asset_sources/svg/stack_duo/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/stack_duo/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_wallet/socials/twitter-brands.svg b/asset_sources/svg/stack_wallet/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/stack_wallet/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/stack_wallet/socials/x.svg b/asset_sources/svg/stack_wallet/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/stack_wallet/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 378e93d0f..3a2026785 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -329,6 +329,10 @@ class TransactionV2 { bool isCoinbase() => type == TransactionType.incoming && inputs.any((e) => e.coinbase != null); + @ignore + bool get isInstantLock => + _getFromOtherData(key: TxV2OdKeys.isInstantLock) == true; + @override String toString() { return 'TransactionV2(\n' @@ -362,4 +366,5 @@ abstract final class TxV2OdKeys { static const moneroAmount = "moneroAmount"; static const moneroAccountIndex = "moneroAccountIndex"; static const isMoneroTransaction = "isMoneroTransaction"; + static const isInstantLock = "isInstantLock"; } diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index 23035dc8e..9137e21f4 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -46,21 +46,40 @@ class AddTokenListElement extends ConsumerStatefulWidget { class _AddTokenListElementState extends ConsumerState { final bool isDesktop = Util.isDesktop; + Currency? currency; + @override - Widget build(BuildContext context) { - final currency = - ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - widget.data.token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); + void initState() { + super.initState(); + + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.data.token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + this.currency = currency; + }); + } + }); + } + }); + } + + @override + Widget build(BuildContext context) { final String mainLabel = widget.data.token.name; final double iconSize = isDesktop ? 32 : 24; @@ -77,7 +96,7 @@ class _AddTokenListElementState extends ConsumerState { children: [ currency != null ? SvgPicture.network( - currency.image, + currency!.image, width: iconSize, height: iconSize, placeholderBuilder: diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 80acac3ed..2506b4195 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -28,39 +28,61 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; -class CoinSelectItem extends ConsumerWidget { +class CoinSelectItem extends ConsumerStatefulWidget { const CoinSelectItem({super.key, required this.entity}); final AddWalletListEntity entity; @override - Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: CoinSelectItem for ${entity.name}"); - final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); + ConsumerState createState() => _CoinSelectItemState(); +} - final isDesktop = Util.isDesktop; +class _CoinSelectItemState extends ConsumerState { + String? tokenImageUri; - String? tokenImageUri; - if (entity is EthTokenEntity) { - final currency = - ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - (entity as EthTokenEntity).token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); - tokenImageUri = currency?.image; + @override + void initState() { + super.initState(); + + if (widget.entity is EthTokenEntity) { + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + (widget.entity as EthTokenEntity).token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + tokenImageUri = currency?.image; + }); + } + }); + } + }); } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: CoinSelectItem for ${widget.entity.name}"); + final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); + + final isDesktop = Util.isDesktop; return Container( decoration: BoxDecoration( color: - selectedEntity == entity + selectedEntity == widget.entity ? Theme.of(context).extension()!.textFieldActiveBG : Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular( @@ -68,7 +90,9 @@ class CoinSelectItem extends ConsumerWidget { ), ), child: MaterialButton( - key: Key("coinSelectItemButtonKey_${entity.name}${entity.ticker}"), + key: Key( + "coinSelectItemButtonKey_${widget.entity.name}${widget.entity.ticker}", + ), padding: isDesktop ? const EdgeInsets.only(left: 24) @@ -84,15 +108,17 @@ class CoinSelectItem extends ConsumerWidget { child: Row( children: [ tokenImageUri != null - ? SvgPicture.network(tokenImageUri, width: 26, height: 26) + ? SvgPicture.network(tokenImageUri!, width: 26, height: 26) : SvgPicture.file( - File(ref.watch(coinIconProvider(entity.cryptoCurrency))), + File( + ref.watch(coinIconProvider(widget.entity.cryptoCurrency)), + ), width: 26, height: 26, ), SizedBox(width: isDesktop ? 12 : 10), Text( - "${entity.name} (${entity.ticker})", + "${widget.entity.name} (${widget.entity.ticker})", style: isDesktop ? STextStyles.desktopTextMedium(context) @@ -100,8 +126,8 @@ class CoinSelectItem extends ConsumerWidget { context, ).copyWith(fontSize: 14), ), - if (isDesktop && selectedEntity == entity) const Spacer(), - if (isDesktop && selectedEntity == entity) + if (isDesktop && selectedEntity == widget.entity) const Spacer(), + if (isDesktop && selectedEntity == widget.entity) Padding( padding: const EdgeInsets.only(right: 18), child: SizedBox( @@ -120,7 +146,8 @@ class CoinSelectItem extends ConsumerWidget { ), ), onPressed: () { - ref.read(addWalletSelectedEntityStateProvider.state).state = entity; + ref.read(addWalletSelectedEntityStateProvider.state).state = + widget.entity; }, ), ); diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index d934526b8..db55898d2 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -30,7 +30,7 @@ class ExpandingSubListItem extends StatefulWidget { double? animationDurationMultiplier, this.curve = Curves.easeInOutCubicEmphasized, }) : animationDurationMultiplier = - animationDurationMultiplier ?? entities.length * 0.11; + animationDurationMultiplier ?? entities.length * 0.11; final String title; final List entities; @@ -85,23 +85,21 @@ class _ExpandingSubListItemState extends State { header: Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 8.0, - right: 10, - ), + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.title, - style: isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextMedium(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), RotateIcon( @@ -109,9 +107,10 @@ class _ExpandingSubListItemState extends State { Assets.svg.chevronDown, width: isDesktop ? 20 : 12, height: isDesktop ? 10 : 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), curve: widget.curve, animationDurationMultiplier: widget.animationDurationMultiplier, diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index c194ec9ee..64a7ba40f 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -16,12 +16,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; +import 'package:isar/isar.dart'; import '../../app_config.dart'; import '../../models/buy/response_objects/crypto.dart'; import '../../models/buy/response_objects/fiat.dart'; import '../../models/buy/response_objects/quote.dart'; import '../../models/contact_address_entry.dart'; +import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import '../../providers/providers.dart'; @@ -33,10 +35,12 @@ import '../../utilities/assets.dart'; import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; +import '../../utilities/enums/derive_path_type_enum.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_loading_overlay.dart'; @@ -1204,9 +1208,34 @@ class _BuyFormState extends ConsumerState { // _toController.text = manager.walletName; // model.recipientAddress = // await manager.currentReceivingAddress; - _receiveAddressController.text = - (await wallet.getCurrentReceivingAddress())! - .value; + + final address = + await wallet.getCurrentReceivingAddress(); + + if (address!.type == AddressType.p2tr && + wallet is Bip39HDWallet) { + // lets assume any wallet that has taproot also has segwit. WCGW + final address = + await ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(wallet.walletId) + .filter() + .typeEqualTo(AddressType.p2wpkh) + .sortByDerivationIndexDesc() + .findFirst() ?? + await wallet.generateNextReceivingAddress( + derivePathType: DerivePathType.bip84, + ); + + _receiveAddressController.text = + address.value; + } else { + _receiveAddressController.text = + address.value; + } setState(() { _addressToggleFlag = diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index ce861d560..edfe6ba19 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -100,8 +100,9 @@ class _ExchangeCurrencySelectionViewState Future> _loadCurrencies() async { await ExchangeDataLoadingService.instance.initDB(); + final isar = await ExchangeDataLoadingService.instance.isar; final currencies = - await ExchangeDataLoadingService.instance.isar.currencies + await isar.currencies .where() .filter() .isFiatEqualTo(false) diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index d937ac4a7..126a16bfb 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -20,11 +20,14 @@ import '../../providers/global/notifications_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../providers/ui/home_view_index_provider.dart'; import '../../providers/ui/unread_notifications_provider.dart'; +import '../../route_generator.dart'; import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; +import '../../utilities/idle_monitor.dart'; +import '../../utilities/prefs.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/animated_widgets/rotate_icon.dart'; import '../../widgets/app_icon.dart'; @@ -35,6 +38,7 @@ import '../../widgets/stack_dialog.dart'; import '../buy_view/buy_view.dart'; import '../exchange_view/exchange_view.dart'; import '../notification_views/notifications_view.dart'; +import '../pinpad_views/lock_screen_view.dart'; import '../settings_views/global_settings_view/global_settings_view.dart'; import '../settings_views/global_settings_view/hidden_settings.dart'; import '../wallets_view/wallets_view.dart'; @@ -63,6 +67,51 @@ class _HomeViewState extends ConsumerState { late TorConnectionStatus _currentSyncStatus; + IdleMonitor? _idleMonitor; + + void _onIdle() async { + final context = _key.currentContext; + if (context != null) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: false, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to unlock ${AppConfig.appName}", + biometricsAuthenticationTitle: "Unlock ${AppConfig.appName}", + ), + settings: const RouteSettings(name: "/unlockTimedOutAppScreen"), + ), + ); + } + } + + late AutoLockInfo _autoLockInfo; + void _prefsTimeoutListener() { + final prefs = ref.read(prefsChangeNotifierProvider); + if (mounted && prefs.autoLockInfo != _autoLockInfo) { + _autoLockInfo = prefs.autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor?.detach(); + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + _idleMonitor!.attach(); + } else { + _idleMonitor?.detach(); + _idleMonitor = null; + } + } + } + // final _buyDataLoadingService = BuyDataLoadingService(); Future _onWillPop() async { @@ -124,6 +173,14 @@ class _HomeViewState extends ConsumerState { @override void initState() { + _autoLockInfo = ref.read(prefsChangeNotifierProvider).autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + } + _pageController = PageController(); _rotateIconController = RotateIconController(); _children = [ @@ -140,11 +197,17 @@ class _HomeViewState extends ConsumerState { // showOneTimeTorHasBeenAddedDialogIfRequired(context); // }); + _idleMonitor?.attach(); + + ref.read(prefsChangeNotifierProvider).addListener(_prefsTimeoutListener); + super.initState(); } @override dispose() { + ref.read(prefsChangeNotifierProvider).removeListener(_prefsTimeoutListener); + _idleMonitor?.detach(); _pageController.dispose(); _rotateIconController.forward = null; _rotateIconController.reverse = null; diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 79cadf9a0..49329cc98 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -80,7 +80,7 @@ class _MonkeyViewState extends ConsumerState { .read(pWallets) .getWallet(walletId) .getCurrentReceivingAddress(); - String filePath = path.join(dir.path, "monkey_$address"); + String filePath = path.join(dir.path, "monkey_${address?.value}"); filePath += isPNG ? ".png" : ".svg"; diff --git a/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart b/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart new file mode 100644 index 000000000..924b39189 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart @@ -0,0 +1,229 @@ +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/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../widgets/background.dart'; +import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/rounded_white_container.dart'; + +class AutoLockTimeoutSettingsView extends ConsumerStatefulWidget { + const AutoLockTimeoutSettingsView({super.key}); + + static const routeName = "/autoLockTimeoutSettingsView"; + + @override + ConsumerState createState() => + _AutoLockTimeoutSettingsViewState(); +} + +class _AutoLockTimeoutSettingsViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + final TextEditingController _timeController = TextEditingController(); + late bool _enabled; + bool _lock = false; + + Future _save() async { + if (_lock) return; + _lock = true; + + try { + final minutes = int.tryParse(_timeController.text); + + if (minutes == null) { + // this should not hit unless logic in validating text field input is + // wrong + return; + } + + ref.read(prefsChangeNotifierProvider).autoLockInfo = ( + enabled: _enabled, + minutes: minutes, + ); + + Navigator.of(context, rootNavigator: isDesktop).pop(); + } finally { + _lock = false; + } + } + + int _minutesCache = 1; + + int _clampMinutes(int input) { + if (input > 60) return 60; + if (input < 1) return 1; + return input; + } + + @override + void initState() { + super.initState(); + _enabled = ref.read(prefsChangeNotifierProvider).autoLockInfo.enabled; + _minutesCache = _clampMinutes( + ref.read(prefsChangeNotifierProvider).autoLockInfo.minutes, + ); + _timeController.text = _minutesCache.toString(); + } + + @override + void dispose() { + _timeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 70), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: RawMaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle auto lock", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: _enabled, + onValueChanged: (newValue) { + _enabled = newValue; + }, + ), + ), + ], + ), + ), + ), + ), + SizedBox(height: isDesktop ? 24 : 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Text("Minutes", style: STextStyles.titleBold12(context)), + const SizedBox(width: 16), + Flexible( + child: TextField( + controller: _timeController, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction( + (oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue, + ), + ], + onChanged: (value) { + final number = int.tryParse(value); + if (number == null || number < 1 || number > 60) { + _timeController.text = _minutesCache.toString(); + return; + } + + _minutesCache = _clampMinutes(number); + }, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Minutes", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], + ), + ), + SizedBox(height: isDesktop ? 40 : 16), + if (!isDesktop) const Spacer(), + ConditionalParent( + condition: isDesktop, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [child], + ), + child: PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, + width: isDesktop ? 200 : null, + label: "Save", + onPressed: _save, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index 5412189ec..76331b0bb 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -27,6 +27,7 @@ import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; +import 'auto_lock_timeout_settings_view.dart'; import 'change_pin_view/change_pin_view.dart'; import 'create_duress_pin_view.dart'; @@ -519,6 +520,56 @@ class _SecurityViewState extends ConsumerState { }, ), ), + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + AutoLockTimeoutSettingsView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change auto lock settings", + biometricsAuthenticationTitle: + "Auto lock settings", + ), + settings: const RouteSettings( + name: "/autoLockTimeoutSettingsLockScreen", + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Auto lock settings", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + ), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index fcfc6ee18..a88e47099 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -21,6 +21,8 @@ import '../../../utilities/util.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; class SupportView extends StatelessWidget { @@ -90,8 +92,8 @@ class SupportView extends StatelessWidget { ), const SizedBox(height: 8), AboutItem( - linkUrl: "https://twitter.com/stack_wallet", - label: "Twitter", + linkUrl: "https://x.com/stack_wallet", + label: "X", buttonText: "@stack_wallet", iconAsset: Assets.socials.twitter, isDesktop: isDesktop, @@ -140,8 +142,26 @@ class AboutItem extends StatelessWidget { Constants.size.circularBorderRadius, ), ), - onPressed: () { - launchUrl(Uri.parse(linkUrl), mode: LaunchMode.externalApplication); + onPressed: () async { + if (label == "Email") { + await launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + } else { + await showDialog( + context: context, + builder: + (_) => ScamWarningDialog( + channel: label, + onUnderstandPressed: + () => launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ), + ), + ); + } }, child: Padding( padding: @@ -207,3 +227,182 @@ class AboutItem extends StatelessWidget { ); } } + +class ScamWarningDialog extends StatelessWidget { + const ScamWarningDialog({ + super.key, + required this.onUnderstandPressed, + required this.channel, + }); + + final String channel; + final VoidCallback onUnderstandPressed; + + @override + Widget build(BuildContext context) { + return SDialog( + padding: EdgeInsets.all(Util.isDesktop ? 32 : 16), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => IntrinsicWidth(child: child), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: [ + TextSpan( + text: "Important: Protect Yourself from Scammers!\n\n", + style: + Util.isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH2(context), + ), + const TextSpan( + text: "All official support for ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + const TextSpan( + text: AppConfig.appName, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " in ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + TextSpan( + text: channel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " is provided ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + const TextSpan( + text: "ONLY", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " in public channels.\n\n", + style: TextStyle(fontWeight: FontWeight.normal), + ), + ], + ), + ), + const _Bullet( + text: + "Never trust direct messages (DMs) from anyone" + " claiming to be support staff.\n", + ), + const _Bullet( + text: + "Do not share personal information," + " wallet details, or private keys.\n", + ), + const _Bullet( + text: + "If someone asks you to send them money or crypto," + " they are a scammer.\n\n", + ), + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: const [ + TextSpan( + text: "Our support staff will ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + TextSpan( + text: "*never*", + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + " contact you privately first. " + "They will only help you in the public chat.", + style: TextStyle(fontWeight: FontWeight.normal), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 40 : 32), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!Util.isDesktop) const Spacer(), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: PrimaryButton( + width: Util.isDesktop ? 240 : null, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + label: "I UNDERSTAND", + onPressed: onUnderstandPressed, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _Bullet extends StatelessWidget { + const _Bullet({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: const [ + TextSpan( + text: " • ", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: [ + TextSpan( + text: text, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index 9af007aef..3fb0aaaab 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -21,6 +21,8 @@ import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../../widgets/coin_ticker_tag.dart'; +import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../sub_widgets/tx_icon.dart'; import 'transaction_v2_details_view.dart'; @@ -235,9 +237,28 @@ class _TransactionCardStateV2 extends ConsumerState { Flexible( child: FittedBox( fit: BoxFit.scaleDown, - child: Text( - whatIsIt(coin, currentHeight), - style: STextStyles.itemSubtitle12(context), + child: ConditionalParent( + condition: + coin is Firo && + _transaction.isInstantLock && + !_transaction.isConfirmed( + currentHeight, + coin.minConfirms, + coin.minCoinbaseConfirms, + ), + builder: + (child) => Row( + children: [ + child, + + const SizedBox(width: 10), + const CoinTickerTag(ticker: "INSTANT"), + ], + ), + child: Text( + whatIsIt(coin, currentHeight), + style: STextStyles.itemSubtitle12(context), + ), ), ), ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 1bb76d59e..a479ae4eb 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -358,16 +358,17 @@ class _WalletViewState extends ConsumerState { ); } else { Future _future; + final isar = await ExchangeDataLoadingService.instance.isar; try { _future = - ExchangeDataLoadingService.instance.isar.currencies + isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(); } catch (_) { _future = ExchangeDataLoadingService.instance.loadAll().then( (_) => - ExchangeDataLoadingService.instance.isar.currencies + isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index dd30adee2..6e18c5086 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -23,9 +23,7 @@ import '../step_scaffold.dart'; import 'desktop_step_item.dart'; class DesktopStep4 extends ConsumerStatefulWidget { - const DesktopStep4({ - super.key, - }); + const DesktopStep4({super.key}); @override ConsumerState createState() => _DesktopStep4State(); @@ -56,8 +54,9 @@ class _DesktopStep4State extends ConsumerState { return; } - final statusResponse = - await ref.read(efExchangeProvider).updateTrade(trade); + final statusResponse = await ref + .read(efExchangeProvider) + .updateTrade(trade); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; @@ -99,16 +98,12 @@ class _DesktopStep4State extends ConsumerState { "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below", style: STextStyles.desktopTextMedium(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below. Once it is received, ${ref.watch(desktopExchangeModelProvider.select((value) => value!.trade?.exchangeName))} will send the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedContainer( color: Theme.of(context).extension()!.warningBackground, child: RichText( @@ -116,9 +111,10 @@ class _DesktopStep4State extends ConsumerState { text: "You must send at least ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}. ", style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, fontSize: 14, ), children: [ @@ -126,9 +122,10 @@ class _DesktopStep4State extends ConsumerState { text: "If you send less than ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}, your transaction may not be converted and it may not be refunded.", style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, fontSize: 14, ), ), @@ -136,9 +133,7 @@ class _DesktopStep4State extends ConsumerState { ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, padding: const EdgeInsets.all(0), @@ -146,11 +141,14 @@ class _DesktopStep4State extends ConsumerState { children: [ DesktopStepItem( vertical: true, + copyableValue: true, label: "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to this address", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInAddress), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInAddress, + ), ) ?? "Error", ), @@ -159,22 +157,26 @@ class _DesktopStep4State extends ConsumerState { color: Theme.of(context).extension()!.background, ), if (ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) != null) DesktopStepItem( vertical: true, label: "Memo", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) ?? "Error", ), if (ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) != null) Container( @@ -192,9 +194,11 @@ class _DesktopStep4State extends ConsumerState { ), DesktopStepItem( label: "Trade ID", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.tradeId), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.tradeId, + ), ) ?? "Error", ), @@ -213,8 +217,9 @@ class _DesktopStep4State extends ConsumerState { ), Text( _statusString, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context) .extension()! .colorForStatus(_statusString), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart index 352e353fb..6349350d1 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/simple_copy_button.dart'; class DesktopStepItem extends StatelessWidget { const DesktopStepItem({ @@ -21,12 +22,14 @@ class DesktopStepItem extends StatelessWidget { required this.value, this.padding = const EdgeInsets.all(16), this.vertical = false, + this.copyableValue = false, }); final String label; final String value; final EdgeInsets padding; final bool vertical; + final bool copyableValue; @override Widget build(BuildContext context) { @@ -34,35 +37,69 @@ class DesktopStepItem extends StatelessWidget { padding: padding, child: ConditionalParent( condition: vertical, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const SizedBox( - height: 2, - ), - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: copyableValue, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [child, SimpleCopyButton(data: value)], + ), + child: child, + ), + const SizedBox(height: 2), + copyableValue + ? SelectableText( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ) + : Text( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], ), - ], - ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - label, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), + Text(label, style: STextStyles.desktopTextExtraExtraSmall(context)), if (!vertical) - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ), - ), + copyableValue + ? SelectableText( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ) + : Text( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ), ], ), ), diff --git a/lib/pages_desktop_specific/desktop_home_view.dart b/lib/pages_desktop_specific/desktop_home_view.dart index 980b563a0..d29aeb4bc 100644 --- a/lib/pages_desktop_specific/desktop_home_view.dart +++ b/lib/pages_desktop_specific/desktop_home_view.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,6 +24,8 @@ import '../providers/ui/unread_notifications_provider.dart'; import '../route_generator.dart'; import '../themes/stack_colors.dart'; import '../utilities/enums/backup_frequency_type.dart'; +import '../utilities/idle_monitor.dart'; +import '../utilities/prefs.dart'; import '../widgets/background.dart'; import 'address_book_view/desktop_address_book.dart'; import 'desktop_buy/desktop_buy_view.dart'; @@ -29,6 +33,7 @@ import 'desktop_exchange/desktop_exchange_view.dart'; import 'desktop_menu.dart'; import 'my_stack_view/my_stack_view.dart'; import 'notifications/desktop_notifications_view.dart'; +import 'password/desktop_unlock_app_dialog.dart'; import 'settings/desktop_settings_view.dart'; import 'settings/settings_menu/desktop_about_view.dart'; import 'settings/settings_menu/desktop_support_view.dart'; @@ -45,14 +50,60 @@ class DesktopHomeView extends ConsumerStatefulWidget { class _DesktopHomeViewState extends ConsumerState { final GlobalKey myStackViewNavKey = GlobalKey(); late final Navigator myStackViewNav; + IdleMonitor? _idleMonitor; + + void _onIdle() async { + final context = myStackViewNavKey.currentContext; + if (context != null) { + await showDialog( + barrierDismissible: false, + context: context, + useSafeArea: false, + builder: + (context) => const Background( + child: Center(child: DesktopUnlockAppDialog()), + ), + ); + } + } + + late AutoLockInfo _autoLockInfo; + void _prefsTimeoutListener() { + final prefs = ref.read(prefsChangeNotifierProvider); + if (mounted && prefs.autoLockInfo != _autoLockInfo) { + _autoLockInfo = prefs.autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor?.detach(); + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + _idleMonitor!.attach(); + } else { + _idleMonitor?.detach(); + _idleMonitor = null; + } + } + } @override void initState() { + _autoLockInfo = ref.read(prefsChangeNotifierProvider).autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + } + myStackViewNav = Navigator( key: myStackViewNavKey, onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ); + _idleMonitor?.attach(); + + ref.read(prefsChangeNotifierProvider).addListener(_prefsTimeoutListener); // WidgetsBinding.instance.addPostFrameCallback((timeStamp) { // showOneTimeTorHasBeenAddedDialogIfRequired(context); @@ -61,12 +112,19 @@ class _DesktopHomeViewState extends ConsumerState { super.initState(); } + @override + dispose() { + ref.read(prefsChangeNotifierProvider).removeListener(_prefsTimeoutListener); + _idleMonitor?.detach(); + super.dispose(); + } + final Map contentViews = { DesktopMenuItemId.myStack: Container( - // key: Key("desktopStackHomeKey"), - // onGenerateRoute: RouteGenerator.generateRoute, - // initialRoute: MyStackView.routeName, - ), + // key: Key("desktopStackHomeKey"), + // onGenerateRoute: RouteGenerator.generateRoute, + // initialRoute: MyStackView.routeName, + ), DesktopMenuItemId.exchange: const Navigator( key: Key("desktopExchangeHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, @@ -109,11 +167,13 @@ class _DesktopHomeViewState extends ConsumerState { if (ref.read(prevDesktopMenuItemProvider.state).state == DesktopMenuItemId.myStack && ref.read(prevDesktopMenuItemProvider.state).state == newKey) { - Navigator.of(myStackViewNavKey.currentContext!) - .popUntil(ModalRoute.withName(MyStackView.routeName)); + Navigator.of( + myStackViewNavKey.currentContext!, + ).popUntil(ModalRoute.withName(MyStackView.routeName)); if (ref.read(currentWalletIdProvider.state).state != null) { - final wallet = - ref.read(pWallets).getWallet(ref.read(currentWalletIdProvider)!); + final wallet = ref + .read(pWallets) + .getWallet(ref.read(currentWalletIdProvider)!); if (wallet.shouldAutoSync) { wallet.shouldAutoSync = false; @@ -182,17 +242,19 @@ class _DesktopHomeViewState extends ConsumerState { ), Expanded( child: IndexedStack( - index: ref - .watch(currentDesktopMenuItemProvider.state) - .state - .index > - 0 - ? 1 - : 0, + index: + ref + .watch(currentDesktopMenuItemProvider.state) + .state + .index > + 0 + ? 1 + : 0, children: [ myStackViewNav, - contentViews[ - ref.watch(currentDesktopMenuItemProvider.state).state]!, + contentViews[ref + .watch(currentDesktopMenuItemProvider.state) + .state]!, ], ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 71cd7f196..e5b05270f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -45,6 +45,7 @@ import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet.dart' show Wallet; import '../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -481,10 +482,20 @@ class _DesktopWalletFeaturesState extends ConsumerState { final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; - final isViewOnlyNoAddressGen = - wallet is ViewOnlyOptionInterface && - wallet.isViewOnly && - wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; + final bool canGen; + if (isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { + canGen = false; + } else { + final supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; + + canGen = + (wallet is MultiAddressInterface || + wallet is SparkInterface || + supportsMweb); + } final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly; @@ -494,8 +505,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()), - if (!isViewOnlyNoAddressGen) - (WalletFeature.reuseAddress, Assets.svg.key, () => ()), + if (canGen) (WalletFeature.reuseAddress, Assets.svg.key, () => ()), if (showMwebOption) (WalletFeature.enableMweb, Assets.svg.key, () => ()), ]; diff --git a/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart b/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart new file mode 100644 index 000000000..46f3218f5 --- /dev/null +++ b/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart @@ -0,0 +1,206 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/stack_text_field.dart'; +import '../../app_config.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../utilities/show_loading.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; + +class DesktopUnlockAppDialog extends ConsumerStatefulWidget { + const DesktopUnlockAppDialog({super.key}); + + @override + ConsumerState createState() => + _DesktopUnlockAppDialogState(); +} + +class _DesktopUnlockAppDialogState + extends ConsumerState { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + + bool _confirmEnabled = false; + bool _lock = false; + + Future _confirmPressed() async { + if (_lock) { + return; + } + _lock = true; + + try { + final passwordIsValid = await showLoading( + whileFuture: ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text), + context: context, + message: "Verifying password...", + delay: const Duration(seconds: 1), + ); + + if (mounted) { + if (passwordIsValid == true) { + Navigator.of(context, rootNavigator: true).pop(); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid password!", + context: context, + ), + ); + } + } + } finally { + _lock = false; + } + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 579, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.svg.keys, width: 100), + const SizedBox(height: 56), + Text( + "Unlock ${AppConfig.appName}", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 16), + Text( + "Enter your wallet password to unlock ${AppConfig.appName}", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopUnlockAppPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_confirmEnabled) { + _confirmPressed(); + } + }, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox(width: 24), + GestureDetector( + key: const Key( + "desktopUnlockAppPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of( + context, + ).extension()!.textDark3, + width: 24, + height: 24, + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _confirmEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 48), + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + enabled: _confirmEnabled, + label: "Unlock", + buttonHeight: ButtonHeight.l, + onPressed: _confirmPressed, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart new file mode 100644 index 000000000..2ebd3c85b --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../../../../pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; + +class DesktopAutolockTimeoutSettingsDialog extends StatelessWidget { + const DesktopAutolockTimeoutSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 480, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Auto lock timeout", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + + const Padding( + padding: EdgeInsets.only(left: 32, right: 32, bottom: 32, top: 20), + child: AutoLockTimeoutSettingsView(), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart index 3b227f1d3..bb0ed0fda 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart @@ -18,6 +18,7 @@ import 'package:zxcvbn/zxcvbn.dart'; import '../../../app_config.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../providers/global/prefs_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; @@ -27,6 +28,7 @@ import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/progress_bar.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_text_field.dart'; +import 'advanced_settings/desktop_autolock_timeout_settings_dialog.dart'; class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({super.key}); @@ -69,8 +71,9 @@ class _SecuritySettings extends ConsumerState { final String pwNew = passwordController.text; final String pwNewRepeat = passwordRepeatController.text; - final verified = - await ref.read(storageCryptoHandlerProvider).verifyPassphrase(pw); + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(pw); if (verified) { if (pwNew != pwNewRepeat) { @@ -78,11 +81,9 @@ class _SecuritySettings extends ConsumerState { return (false, FlushBarType.warning, "New passphrase does not match!"); } else { - final success = - await ref.read(storageCryptoHandlerProvider).changePassphrase( - pw, - pwNew, - ); + final success = await ref + .read(storageCryptoHandlerProvider) + .changePassphrase(pw, pwNew); if (success) { await Future.delayed(const Duration(seconds: 1)); @@ -90,7 +91,7 @@ class _SecuritySettings extends ConsumerState { return ( true, FlushBarType.success, - "Passphrase successfully changed" + "Passphrase successfully changed", ); } else { await Future.delayed(const Duration(seconds: 1)); @@ -137,9 +138,7 @@ class _SecuritySettings extends ConsumerState { return Column( children: [ Padding( - padding: const EdgeInsets.only( - right: 30, - ), + padding: const EdgeInsets.only(right: 30), child: RoundedWhiteContainer( radiusMultiplier: 2, child: Column( @@ -162,392 +161,450 @@ class _SecuritySettings extends ConsumerState { "Change Password", style: STextStyles.desktopTextSmall(context), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "Protect your ${AppConfig.appName} with a strong password. ${AppConfig.appName} does not store " "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), changePassword ? SizedBox( - width: 512, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey", ), - child: TextField( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldKey", - ), - focusNode: passwordCurrentFocusNode, - controller: passwordCurrentController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter current password", - passwordCurrentFocusNode, + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - setState(() {}); - }, ), + onChanged: (newValue) { + setState(() {}); + }, ), - const SizedBox(height: 16), - Text( - "New password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, + ), + const SizedBox(height: 16), + Text( + "New password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1", ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey1", - ), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter new password", - passwordFocusNode, + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey1", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = - zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (final sug in result - .feedback.suggestions! - .toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += - result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback - .contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", - "phrases\nNo need", - ); - } - - if (feedback.endsWith("\n")) { - feedback = feedback.substring( - 0, - feedback.length - 2, - ); - } - + ), + onChanged: (newValue) { + if (newValue.isEmpty) { setState(() { - passwordFeedback = feedback; + passwordFeedback = ""; }); - }, - ), + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (final sug + in result.feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need", + ); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, + feedback.length - 2, + ); + } + + setState(() { + passwordFeedback = feedback; + }); + }, ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: - passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: + passwordFeedback.isNotEmpty + ? Text( passwordFeedback, style: STextStyles.infoSmall( context, ), ) - : null, + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key( - "desktopSecurityCreateStackBackUpProgressBar", - ), - width: 450, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + child: ProgressBar( + key: const Key( + "desktopSecurityCreateStackBackUpProgressBar", ), + width: 450, + height: 5, + fillColor: + passwordStrength < 0.51 + ? Theme.of(context) + .extension()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: + Theme.of(context) + .extension()! + .buttonBackSecondary, + percent: + passwordStrength < 0.25 + ? 0.03 + : passwordStrength, ), - const SizedBox(height: 16), - Text( - "Confirm new password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 16), + Text( + "Confirm new password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2", ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey2", - ), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm new password", - passwordRepeatFocusNode, + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey2", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - setState(() {}); - }, ), + onChanged: (newValue) { + setState(() {}); + }, ), - const SizedBox(height: 20), - PrimaryButton( - width: 160, - buttonHeight: ButtonHeight.l, - enabled: shouldEnableSave, - label: "Save changes", - onPressed: () async { - if (_changePWLock) { - return; + ), + const SizedBox(height: 20), + PrimaryButton( + width: 160, + buttonHeight: ButtonHeight.l, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () async { + if (_changePWLock) { + return; + } + _changePWLock = true; + + try { + final (didChangePW, type, message) = + (await showLoading( + whileFuture: _attemptChangePW(), + context: context, + message: "Updating...", + rootNavigator: true, + ))!; + + if (mounted) { + unawaited( + showFloatingFlushBar( + type: type, + message: message, + context: context, + ), + ); } - _changePWLock = true; - - try { - final (didChangePW, type, message) = - (await showLoading( - whileFuture: _attemptChangePW(), - context: context, - message: "Updating...", - rootNavigator: true, - ))!; - - if (mounted) { - unawaited( - showFloatingFlushBar( - type: type, - message: message, - context: context, - ), - ); - } - - if (didChangePW == true) { - setState(() { - changePassword = false; - }); - } - } finally { - _changePWLock = false; + + if (didChangePW == true) { + setState(() { + changePassword = false; + }); } - }, - ), - ], - ), - ) + } finally { + _changePWLock = false; + } + }, + ), + ], + ), + ) : PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: true, - label: "Set up new password", - onPressed: () { - setState(() { - changePassword = true; - }); - }, + width: 210, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), + + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider(thickness: 0.5), + ), + + Consumer( + builder: (_, ref, __) { + final autoLockInfo = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.autoLockInfo, ), + ); + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Auto lock timeout", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), + textAlign: TextAlign.left, + ), + Text( + autoLockInfo.enabled + ? "${autoLockInfo.minutes} minutes" + : "Disabled", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + ], + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Edit", + width: 101, + onPressed: () async { + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DesktopAutolockTimeoutSettingsDialog(); + }, + ); + }, + ), + ], + ), + ); + }, + ), ], ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3266d1273..ee09bdba0 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -113,6 +113,7 @@ import 'pages/settings_views/global_settings_view/manage_nodes_views/add_edit_no import 'pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; import 'pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart'; import 'pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; +import 'pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart'; import 'pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart'; import 'pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart'; import 'pages/settings_views/global_settings_view/security_views/security_view.dart'; @@ -997,6 +998,13 @@ class RouteGenerator { settings: RouteSettings(name: settings.name), ); + case AutoLockTimeoutSettingsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AutoLockTimeoutSettingsView(), + settings: RouteSettings(name: settings.name), + ); + case BaseCurrencySettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 78fb0117f..35a167795 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; @@ -33,7 +35,10 @@ class ExchangeDataLoadingService { static ExchangeDataLoadingService get instance => _instance; Isar? _isar; - Isar get isar => _isar!; + Future get isar async { + if (_isar == null) await initDB(); + return _isar!; + } VoidCallback? onLoadingError; VoidCallback? onLoadingComplete; @@ -56,9 +61,16 @@ class ExchangeDataLoadingService { ); } + Completer? _initCompleter; Future initDB() async { if (_isar != null) return; - await _isar?.close(); + + if (_initCompleter != null) { + return await _initCompleter!.future; + } + + _initCompleter = Completer(); + _isar = await Isar.open( [ CurrencySchema, @@ -70,6 +82,8 @@ class ExchangeDataLoadingService { name: "exchange_cache", maxSizeMiB: 64, ); + + _initCompleter!.complete(); } Future setCurrenciesIfEmpty( @@ -77,7 +91,7 @@ class ExchangeDataLoadingService { ExchangeRateType rateType, ) async { if (pair?.send == null && pair?.receive == null) { - if (await isar.currencies.count() > 0) { + if (await (await isar).currencies.count() > 0) { pair?.setSend( await getAggregateCurrency( AppConfig.swapDefaults.from, @@ -108,9 +122,10 @@ class ExchangeDataLoadingService { String? contract, ) async { final List currencies; + if (contract != null) { currencies = - await ExchangeDataLoadingService.instance.isar.currencies + await (await isar).currencies .filter() .tokenContractEqualTo(contract) .and() @@ -129,7 +144,7 @@ class ExchangeDataLoadingService { .findAll(); } else { currencies = - await ExchangeDataLoadingService.instance.isar.currencies + await (await isar).currencies .filter() .group( (q) => @@ -232,15 +247,15 @@ class ExchangeDataLoadingService { final exchange = ChangeNowExchange.instance; final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { + await (await isar).writeTxn(() async { final idsToDelete = - await isar.currencies + await (await isar).currencies .where() .exchangeNameEqualTo(ChangeNowExchange.exchangeName) .idProperty() .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { Logging.instance.w( @@ -389,15 +404,15 @@ class ExchangeDataLoadingService { final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { + await (await isar).writeTxn(() async { final idsToDelete = - await isar.currencies + await (await isar).currencies .where() .exchangeNameEqualTo(TrocadorExchange.exchangeName) .idProperty() .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { Logging.instance.w("loadTrocadorCurrencies: $responseCurrencies"); @@ -413,15 +428,15 @@ class ExchangeDataLoadingService { ); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { + await (await isar).writeTxn(() async { final idsToDelete = - await isar.currencies + await (await isar).currencies .where() .exchangeNameEqualTo(NanswapExchange.exchangeName) .idProperty() .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { Logging.instance.w("loadNanswapCurrencies: $responseCurrencies"); diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 89f2bee37..cc5e6bdc0 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -212,6 +212,11 @@ final class MwebdService { Future poll() async { if (!controller.isClosed) { final file = File(path); + + if (!file.existsSync()) { + return; + } + final length = await file.length(); if (length > offset) { diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 679187c5c..d14cdc38c 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -32,7 +32,7 @@ class _SOCIALS { String get discord => "${_path}discord.svg"; String get reddit => "${_path}reddit-alien-brands.svg"; - String get twitter => "${_path}twitter-brands.svg"; + String get twitter => "${_path}x.svg"; String get telegram => "${_path}telegram-brands.svg"; } diff --git a/lib/utilities/idle_monitor.dart b/lib/utilities/idle_monitor.dart new file mode 100644 index 000000000..554f78c2a --- /dev/null +++ b/lib/utilities/idle_monitor.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class IdleMonitor with WidgetsBindingObserver { + final Duration timeout; + final VoidCallback onIdle; + + final WidgetsBinding binding = WidgetsBinding.instance; + + IdleMonitor({required this.timeout, required this.onIdle}); + + Timer? _idleTimer; + bool _isAttached = false; + void Function(PointerDataPacket)? _prevPointerHandler; + KeyEventCallback? _keyboardHandler; + + void attach() { + if (_isAttached) return; + _isAttached = true; + _resetTimer(); + _prevPointerHandler = binding.platformDispatcher.onPointerDataPacket; + binding.platformDispatcher.onPointerDataPacket = (packet) { + _onUserActivity(); + _prevPointerHandler?.call(packet); + }; + _keyboardHandler = (event) { + _onUserActivity(); + return false; + }; + binding.keyboard.addHandler(_keyboardHandler!); + binding.addObserver(this); + } + + void detach() { + if (!_isAttached) return; + _isAttached = false; + binding.platformDispatcher.onPointerDataPacket = _prevPointerHandler; + if (_keyboardHandler != null) { + binding.keyboard.removeHandler(_keyboardHandler!); + } + binding.removeObserver(this); + _cancelTimer(); + } + + void _onUserActivity() { + _resetTimer(); + } + + void _resetTimer() { + _cancelTimer(); + _idleTimer = Timer(timeout, onIdle); + } + + void _cancelTimer() { + _idleTimer?.cancel(); + _idleTimer = null; + } + + // @override + // void didChangeAppLifecycleState(AppLifecycleState state) { + // if (!_isAttached) return; + // if (state == AppLifecycleState.paused || + // state == AppLifecycleState.inactive || + // state == AppLifecycleState.detached) { + // _cancelTimer(); + // } else if (state == AppLifecycleState.resumed) { + // _resetTimer(); + // } + // } +} diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 087aaa631..09b2bbd97 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -26,6 +26,8 @@ import 'enums/backup_frequency_type.dart'; import 'enums/languages_enum.dart'; import 'enums/sync_type_enum.dart'; +typedef AutoLockInfo = ({bool enabled, int minutes}); + class Prefs extends ChangeNotifier { Prefs._(); static final Prefs _instance = Prefs._(); @@ -78,6 +80,7 @@ class Prefs extends ChangeNotifier { _advancedFiroFeatures = await _getAdvancedFiroFeatures(); _logsPath = await _getLogsPath(); _logLevel = await _getLogLevel(); + _autoLockInfo = await _getAutoLockInfo(); _initialized = true; } @@ -1347,4 +1350,37 @@ class Prefs extends ChangeNotifier { return Level.warning; } } + + // auto lock timeout + + AutoLockInfo _autoLockInfo = (enabled: false, minutes: 10); + + AutoLockInfo get autoLockInfo => _autoLockInfo; + + set autoLockInfo(AutoLockInfo autoLockInfo) { + if (_autoLockInfo != autoLockInfo) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "autoLockInfo", + value: { + "enabled": autoLockInfo.enabled, + "minutes": autoLockInfo.minutes, + }, + ); + _autoLockInfo = autoLockInfo; + notifyListeners(); + } + } + + Future _getAutoLockInfo() async { + final map = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "autoLockInfo", + ) + as Map? ?? + {"enabled": false, "minutes": 10}; + + return (enabled: map["enabled"] as bool, minutes: map["minutes"] as int); + } } diff --git a/lib/wallets/crypto_currency/coins/cardano.dart b/lib/wallets/crypto_currency/coins/cardano.dart index a59669eb7..ce7268176 100644 --- a/lib/wallets/crypto_currency/coins/cardano.dart +++ b/lib/wallets/crypto_currency/coins/cardano.dart @@ -1,3 +1,6 @@ +import 'package:blockchain_utils/bip/address/ada/ada.dart'; +import 'package:on_chain/ada/ada.dart'; + import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; @@ -123,7 +126,18 @@ class Cardano extends Bip39Currency { bool validateAddress(String address) { switch (network) { case CryptoCurrencyNetwork.main: - return RegExp(r"^addr1[0-9a-zA-Z]{98}$").hasMatch(address); + try { + final adaAddress = ADAAddress.fromAddress( + address, + network: ADANetwork.mainnet, + ); + + return (adaAddress is ADABaseAddress || + adaAddress is ADAEnterpriseAddress); + } catch (_) { + return false; + } + default: throw Exception("Unsupported network: $network"); } diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 3909c5b4b..5ae2d14a6 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -202,6 +202,17 @@ class CardanoWallet extends Bip39Wallet { address: ADABaseAddress((await getCurrentReceivingAddress())!.value), amount: Value(coin: totalBalance - (txData.amount!.raw)), ); + + final outputAddress = ADAAddress.fromAddress( + txData.recipients!.first.address, + ); + if (!(outputAddress is ADABaseAddress || + outputAddress is ADAEnterpriseAddress)) { + throw Exception( + "Address of type ${outputAddress.runtimeType} currently not supported.", + ); + } + final body = TransactionBody( inputs: listOfUtxosToBeUsed @@ -215,7 +226,7 @@ class CardanoWallet extends Bip39Wallet { outputs: [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), + address: outputAddress, amount: Value(coin: txData.amount!.raw - exampleFee), ), ], @@ -323,11 +334,22 @@ class CardanoWallet extends Bip39Wallet { coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw), ), ); + + final outputAddress = ADAAddress.fromAddress( + txData.recipients!.first.address, + ); + if (!(outputAddress is ADABaseAddress || + outputAddress is ADAEnterpriseAddress)) { + throw Exception( + "Address of type ${outputAddress.runtimeType} currently not supported.", + ); + } + List outputs = []; if (totalBalance == (txData.amount!.raw + txData.fee!.raw)) { outputs = [ TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), + address: outputAddress, amount: Value(coin: txData.amount!.raw), ), ]; @@ -335,7 +357,7 @@ class CardanoWallet extends Bip39Wallet { outputs = [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), + address: outputAddress, amount: Value(coin: txData.amount!.raw), ), ]; diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 233bfd344..0bb1b1fed 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -55,6 +55,20 @@ class FiroWallet extends Bip39HDWallet @override Future updateSentCachedTxData({required TxData txData}) async { if (txData.tempTx != null) { + final otherDataString = txData.tempTx!.otherData; + final Map map; + if (otherDataString == null) { + map = {}; + } else { + map = jsonDecode(otherDataString) as Map? ?? {}; + } + + map[TxV2OdKeys.isInstantLock] = true; + + txData = txData.copyWith( + tempTx: txData.tempTx!.copyWith(otherData: jsonEncode(map)), + ); + await mainDB.updateOrPutTransactionV2s([txData.tempTx!]); _unconfirmedTxids.add(txData.tempTx!.txid); Logging.instance.d("Added firo unconfirmed: ${txData.tempTx!.txid}"); @@ -563,9 +577,14 @@ class FiroWallet extends Bip39HDWallet continue; } - String? otherData; + final isInstantLock = txData["instantlock"] as bool? ?? false; + + final otherData = { + TxV2OdKeys.isInstantLock: isInstantLock, + }; + if (anonFees != null) { - otherData = jsonEncode({"overrideFee": anonFees!.toJsonString()}); + otherData[TxV2OdKeys.overrideFee] = anonFees!.toJsonString(); } final tx = TransactionV2( @@ -582,7 +601,7 @@ class FiroWallet extends Bip39HDWallet outputs: List.unmodifiable(outputs), type: type, subType: subType, - otherData: otherData, + otherData: jsonEncode(otherData), ); if (_unconfirmedTxids.contains(tx.txid)) { @@ -591,14 +610,10 @@ class FiroWallet extends Bip39HDWallet cryptoCurrency.minConfirms, cryptoCurrency.minCoinbaseConfirms, )) { - txns.add(tx); _unconfirmedTxids.removeWhere((e) => e == tx.txid); - } else { - // don't update in db until confirmed } - } else { - txns.add(tx); } + txns.add(tx); } await mainDB.updateOrPutTransactionV2s(txns); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index af6894676..671c79770 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -497,7 +497,7 @@ mixin SparkInterface subtractFeeFromAmount: true, serializedCoins: serializedCoins, privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), - utxoNum: 0, // ?? + utxoNum: recipientCount, additionalTxSize: 0, // name script size ); estimatedFee = BigInt.from(estFee); diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart index 5908270f6..b837eeeb7 100644 --- a/lib/widgets/icon_widgets/eth_token_icon.dart +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -12,7 +12,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; + import '../../models/isar/exchange_cache/currency.dart'; +import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../themes/coin_icon_provider.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -32,17 +34,36 @@ class EthTokenIcon extends ConsumerStatefulWidget { } class _EthTokenIconState extends ConsumerState { - late final String? imageUrl; + String? imageUrl; @override void initState() { - imageUrl = ExchangeDataLoadingService.instance.isar.currencies - .where() - .filter() - .tokenContractEqualTo(widget.contractAddress, caseSensitive: false) - .findFirstSync() - ?.image; super.initState(); + + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.contractAddress, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + imageUrl = currency?.image; + }); + } + }); + } + }); } @override diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index 74d91b33f..14783eb14 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -23,7 +23,7 @@ import '../../../themes/theme_providers.dart'; import '../../../utilities/constants.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; -class WalletInfoCoinIcon extends ConsumerWidget { +class WalletInfoCoinIcon extends ConsumerStatefulWidget { const WalletInfoCoinIcon({ super.key, required this.coin, @@ -36,36 +36,62 @@ class WalletInfoCoinIcon extends ConsumerWidget { final double size; @override - Widget build(BuildContext context, WidgetRef ref) { - Currency? currency; - if (contractAddress != null) { - currency = - ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo(contractAddress!, caseSensitive: false) - .and() - .imageIsNotEmpty() - .findFirstSync(); - } + ConsumerState createState() => _WalletInfoCoinIconState(); +} + +class _WalletInfoCoinIconState extends ConsumerState { + String? imageUrl; + + @override + void initState() { + super.initState(); + ExchangeDataLoadingService.instance.isar.then((isar) async { + if (widget.contractAddress != null) { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.contractAddress!, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + imageUrl = currency?.image; + }); + } + }); + } + } + }); + } + + @override + Widget build(BuildContext context) { return Container( - width: size, - height: size, + width: widget.size, + height: widget.size, decoration: BoxDecoration( - color: ref.watch(pCoinColor(coin)).withOpacity(0.4), + color: ref.watch(pCoinColor(widget.coin)).withOpacity(0.4), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), child: Padding( - padding: EdgeInsets.all(size / 5), + padding: EdgeInsets.all(widget.size / 5), child: - currency != null && currency.image.isNotEmpty - ? SvgPicture.network(currency.image, width: 20, height: 20) + imageUrl != null && imageUrl!.isNotEmpty + ? SvgPicture.network(imageUrl!, width: 20, height: 20) : SvgPicture.file( - File(ref.watch(coinIconProvider(coin))), + File(ref.watch(coinIconProvider(widget.coin))), width: 20, height: 20, ),