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,
),