From 6226c9212e58d29e1813021e399a9bfa359d536f Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 18:11:44 +0800 Subject: [PATCH 01/22] EntrustedAccount class added --- .../main/screens/account_settings_screen.dart | 27 ++++++++++++------- .../main/screens/accounts_screen.dart | 10 ++++--- .../providers/entrusted_account_provider.dart | 2 +- quantus_sdk/lib/quantus_sdk.dart | 2 ++ quantus_sdk/lib/src/models/account.dart | 15 +++-------- quantus_sdk/lib/src/models/base_account.dart | 4 +++ .../lib/src/models/entrusted_account.dart | 18 +++++++++++++ .../src/services/high_security_service.dart | 18 +++++++++---- 8 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 quantus_sdk/lib/src/models/base_account.dart create mode 100644 quantus_sdk/lib/src/models/entrusted_account.dart diff --git a/mobile-app/lib/features/main/screens/account_settings_screen.dart b/mobile-app/lib/features/main/screens/account_settings_screen.dart index 033148cd..47ba7c92 100644 --- a/mobile-app/lib/features/main/screens/account_settings_screen.dart +++ b/mobile-app/lib/features/main/screens/account_settings_screen.dart @@ -27,7 +27,7 @@ import 'package:resonance_network_wallet/shared/extensions/svg_extensions.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; class AccountSettingsScreen extends ConsumerStatefulWidget { - final Account account; + final BaseAccount account; final String balance; final String checksumName; final bool isHighSecurity; @@ -45,9 +45,11 @@ class AccountSettingsScreen extends ConsumerStatefulWidget { class _AccountSettingsScreenState extends ConsumerState { void _editAccountName() { + if (widget.account is! Account) return; + Navigator.push( context, - MaterialPageRoute(builder: (context) => CreateAccountScreen(accountToEdit: widget.account)), + MaterialPageRoute(builder: (context) => CreateAccountScreen(accountToEdit: widget.account as Account)), ).then((result) { if (result == true && mounted) { // Pop this screen with a result to force a refresh on the previous one @@ -130,9 +132,11 @@ class _AccountSettingsScreenState extends ConsumerState { } Future _disconnectWallet() async { + if (widget.account is! Account) return; + try { final accountsService = AccountsService(); - await accountsService.removeAccount(widget.account); + await accountsService.removeAccount(widget.account as Account); ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); ref.invalidate(accountAssociationsProvider); @@ -175,8 +179,8 @@ class _AccountSettingsScreenState extends ConsumerState { _buildShareSection(), const SizedBox(height: 20), _buildAddressSection(), - if (FeatureFlags.enableHighSecurity) ...[const SizedBox(height: 20), _buildHighSecuritySection(context)], - if (widget.account.accountType == AccountType.keystone) ...[ + if (FeatureFlags.enableHighSecurity && widget.account is Account) ...[const SizedBox(height: 20), _buildHighSecuritySection(context)], + if (widget.account is Account && (widget.account as Account).accountType == AccountType.keystone) ...[ const SizedBox(height: 20), _buildDisconnectWalletButton(), ], @@ -205,7 +209,7 @@ class _AccountSettingsScreenState extends ConsumerState { } Widget _buildAccountHeader() { - final isHighSecurity = widget.isHighSecurity && FeatureFlags.enableHighSecurity; + final isHighSecurity = widget.isHighSecurity && FeatureFlags.enableHighSecurity && widget.account is Account; return _buildSettingCard( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), @@ -213,13 +217,14 @@ class _AccountSettingsScreenState extends ConsumerState { children: [ const SizedBox(height: 10), InkWell( - onTap: _editAccountName, + onTap: widget.account is Account ? _editAccountName : null, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.account.name, style: context.themeText.smallTitle), const SizedBox(width: 8), - const Icon(Icons.edit, color: Colors.white70, size: 16), + if (widget.account is Account) + const Icon(Icons.edit, color: Colors.white70, size: 16), ], ), ), @@ -291,6 +296,8 @@ class _AccountSettingsScreenState extends ConsumerState { } Widget _buildHighSecuritySection(BuildContext context) { + if (widget.account is! Account) return const SizedBox(); + final isHighSecurity = widget.isHighSecurity && FeatureFlags.enableHighSecurity; final textColor = isHighSecurity ? context.themeColors.textSecondary : context.themeColors.textPrimary; final secondRowTextColor = isHighSecurity ? context.themeColors.darkGray : context.themeColors.textPrimary; @@ -306,8 +313,8 @@ class _AccountSettingsScreenState extends ConsumerState { context, MaterialPageRoute( builder: (context) => isHighSecurity - ? HighSecurityDetailsScreen(account: widget.account) - : HighSecurityGetStartedScreen(account: widget.account), + ? HighSecurityDetailsScreen(account: widget.account as Account) + : HighSecurityGetStartedScreen(account: widget.account as Account), ), ); }, diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 13de1645..1a6106fb 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -295,7 +295,9 @@ class _AccountsScreenState extends ConsumerState { final entrustedAccountsAsync = ref.watch(entrustedAccountsProvider(account)); final entrustedAccountsData = entrustedAccountsAsync.value ?? []; - final entrustedNodes = entrustedAccountsData.map((entrusted) => TreeNode(data: entrusted)).toList(); + final entrustedNodes = entrustedAccountsData + .map((entrusted) => TreeNode(data: entrusted)) + .toList(); final double constraintMaxHeight = min(entrustedNodes.length * 52, 104); @@ -488,7 +490,7 @@ class _AccountsScreenState extends ConsumerState { if (entrustedNodes.isNotEmpty) ConstrainedBox( constraints: BoxConstraints(maxHeight: constraintMaxHeight), - child: TreeListView( + child: TreeListView( showExpandCollapse: false, nodes: entrustedNodes, nodeBuilder: (context, node, depth) { @@ -509,7 +511,9 @@ class _AccountsScreenState extends ConsumerState { child: InkWell( borderRadius: BorderRadius.circular(5), onTap: () async { - await ref.read(activeAccountProvider.notifier).setActiveAccount(entrusted); + print('onTap: ${entrusted.accountId}'); + + // ignore: use_build_context_synchronously if (mounted) Navigator.pop(context); }, child: Padding( diff --git a/mobile-app/lib/providers/entrusted_account_provider.dart b/mobile-app/lib/providers/entrusted_account_provider.dart index e0a9a88e..24fbdfc5 100644 --- a/mobile-app/lib/providers/entrusted_account_provider.dart +++ b/mobile-app/lib/providers/entrusted_account_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; -final entrustedAccountsProvider = FutureProvider.family, Account>((ref, account) async { +final entrustedAccountsProvider = FutureProvider.family, Account>((ref, account) async { final highSecurityService = HighSecurityService(); final interceptedAccounts = await highSecurityService.getEntrustedAccounts(account); print('intercepted accounts: ${interceptedAccounts.map((account) => account.accountId).join(', ')}'); diff --git a/quantus_sdk/lib/quantus_sdk.dart b/quantus_sdk/lib/quantus_sdk.dart index 70c67a1f..9eb9417e 100644 --- a/quantus_sdk/lib/quantus_sdk.dart +++ b/quantus_sdk/lib/quantus_sdk.dart @@ -15,6 +15,7 @@ export 'src/extensions/keypair_extensions.dart'; export 'src/extensions/string_extensions.dart'; // UI-related exports export 'src/models/account.dart'; +export 'src/models/base_account.dart'; export 'src/models/high_security_data.dart'; export 'src/models/account_stats.dart'; export 'src/models/account_associations.dart'; @@ -58,6 +59,7 @@ export 'src/services/taskmaster_service.dart'; export 'src/extensions/account_extension.dart'; export 'src/quantus_signing_payload.dart'; export 'src/quantus_payload_parser.dart'; +export 'src/models/entrusted_account.dart'; class QuantusSdk { /// Initialise the SDK (loads Rust FFI, etc). diff --git a/quantus_sdk/lib/src/models/account.dart b/quantus_sdk/lib/src/models/account.dart index 76985c5f..2a4ed3b8 100644 --- a/quantus_sdk/lib/src/models/account.dart +++ b/quantus_sdk/lib/src/models/account.dart @@ -1,12 +1,15 @@ import 'package:flutter/foundation.dart'; +import 'package:quantus_sdk/src/models/base_account.dart'; enum AccountType { local, keystone, external } @immutable -class Account { +class Account implements BaseAccount { final int walletIndex; final int index; // derivation index + @override final String name; + @override final String accountId; // address final AccountType accountType; const Account({ @@ -27,16 +30,6 @@ class Account { ); } - factory Account.fromSs58Address(String ss58Address) { - return Account( - walletIndex: -1, - index: -2, - accountId: ss58Address, - name: 'External Account', - accountType: AccountType.external, - ); - } - Map toJson() { return { 'walletIndex': walletIndex, diff --git a/quantus_sdk/lib/src/models/base_account.dart b/quantus_sdk/lib/src/models/base_account.dart new file mode 100644 index 00000000..75985331 --- /dev/null +++ b/quantus_sdk/lib/src/models/base_account.dart @@ -0,0 +1,4 @@ +abstract class BaseAccount { + String get name; + String get accountId; +} diff --git a/quantus_sdk/lib/src/models/entrusted_account.dart b/quantus_sdk/lib/src/models/entrusted_account.dart new file mode 100644 index 00000000..da905e1a --- /dev/null +++ b/quantus_sdk/lib/src/models/entrusted_account.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:quantus_sdk/src/models/base_account.dart'; + +@immutable +class EntrustedAccount implements BaseAccount { + final String parentAccountId; + final int index; // derivation index + @override + final String name; + @override + final String accountId; // address + const EntrustedAccount({ + required this.parentAccountId, + required this.index, + required this.name, + required this.accountId, + }); +} diff --git a/quantus_sdk/lib/src/services/high_security_service.dart b/quantus_sdk/lib/src/services/high_security_service.dart index 8784d46a..b1ea8ae2 100644 --- a/quantus_sdk/lib/src/services/high_security_service.dart +++ b/quantus_sdk/lib/src/services/high_security_service.dart @@ -51,12 +51,20 @@ class HighSecurityService { return await _reversibleTransfersService.isGuardian(account.accountId); } - Future> getEntrustedAccounts(Account account) async { + Future> getEntrustedAccounts(Account account) async { final accounts = await AccountsService().getAccounts(); - Account? mapExistingAccount(String ss58Address) => accounts.firstWhereOrNull((a) => a.accountId == ss58Address); - return (await _reversibleTransfersService.getInterceptedAccounts( - account.accountId, - )).map((account) => mapExistingAccount(account) ?? Account.fromSs58Address(account)).toList(); + String getAccountName(String ss58Address) => + accounts.firstWhereOrNull((a) => a.accountId == ss58Address)?.name ?? 'Entrusted Account'; + return (await _reversibleTransfersService.getInterceptedAccounts(account.accountId)) + .map( + (accountId) => EntrustedAccount( + parentAccountId: account.accountId, + index: 0, + name: getAccountName(accountId), + accountId: accountId, + ), + ) + .toList(); } Future getHighSecurityConfig(String address) async { From 3e4b949554a452fb8776965938b0fafafeab93f8 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 19:06:11 +0800 Subject: [PATCH 02/22] add address book service --- .../src/services/address_book_service.dart | 25 +++++++++++++ .../lib/src/services/settings_service.dart | 35 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 quantus_sdk/lib/src/services/address_book_service.dart diff --git a/quantus_sdk/lib/src/services/address_book_service.dart b/quantus_sdk/lib/src/services/address_book_service.dart new file mode 100644 index 00000000..c2066855 --- /dev/null +++ b/quantus_sdk/lib/src/services/address_book_service.dart @@ -0,0 +1,25 @@ +import 'package:quantus_sdk/src/services/settings_service.dart'; + +class AddressBookService { + static final AddressBookService _instance = AddressBookService._internal(); + factory AddressBookService() => _instance; + AddressBookService._internal(); + + final SettingsService _settingsService = SettingsService(); + + Future setAddressName(String address, String name) async { + await _settingsService.setAddressName(address, name); + } + + Future getAddressName(String address) async { + return await _settingsService.getAddressName(address); + } + + Future removeAddressName(String address) async { + await _settingsService.removeAddressName(address); + } + + Future> getAddressBook() async { + return await _settingsService.getAddressBook(); + } +} diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index 0661acb0..e7b5d74f 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -15,6 +15,7 @@ class SettingsService { // New keys for multi-account support static const String _accountsKey = 'accounts_v4'; static const String _accountsToMigrateKey = 'accounts_to_migrate'; + static const String _addressBookKey = 'address_book'; static const String _oldAccountsKeyV3 = 'accounts_v3'; static const String _oldAccountsKeyV2 = 'accounts_v2'; @@ -189,6 +190,40 @@ class SettingsService { return maxIndex + 1; } + // --- Address Book Methods --- + + Future> getAddressBook() async { + final jsonStr = _prefs.getString(_addressBookKey); + if (jsonStr == null) return {}; + try { + final decoded = jsonDecode(jsonStr) as Map; + return decoded.map((key, value) => MapEntry(key, value as String)); + } catch (_) { + return {}; + } + } + + Future saveAddressBook(Map addressBook) async { + await _prefs.setString(_addressBookKey, jsonEncode(addressBook)); + } + + Future setAddressName(String address, String name) async { + final addressBook = await getAddressBook(); + addressBook[address] = name; + await saveAddressBook(addressBook); + } + + Future getAddressName(String address) async { + final addressBook = await getAddressBook(); + return addressBook[address]; + } + + Future removeAddressName(String address) async { + final addressBook = await getAddressBook(); + addressBook.remove(address); + await saveAddressBook(addressBook); + } + // --- End Multi-Account Methods --- Future getHasWallet() async { From 2811513035b57e5ae36ec1bc539590d545430aec Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 19:23:15 +0800 Subject: [PATCH 03/22] index map --- quantus_sdk/lib/src/services/high_security_service.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quantus_sdk/lib/src/services/high_security_service.dart b/quantus_sdk/lib/src/services/high_security_service.dart index b1ea8ae2..a18406b8 100644 --- a/quantus_sdk/lib/src/services/high_security_service.dart +++ b/quantus_sdk/lib/src/services/high_security_service.dart @@ -56,10 +56,10 @@ class HighSecurityService { String getAccountName(String ss58Address) => accounts.firstWhereOrNull((a) => a.accountId == ss58Address)?.name ?? 'Entrusted Account'; return (await _reversibleTransfersService.getInterceptedAccounts(account.accountId)) - .map( - (accountId) => EntrustedAccount( + .mapIndexed( + (index, accountId) => EntrustedAccount( parentAccountId: account.accountId, - index: 0, + index: index, name: getAccountName(accountId), accountId: accountId, ), From ffd11c617f362084b5a8c71b3b290a847977e6a6 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 21:13:45 +0800 Subject: [PATCH 04/22] show entrusted accounts --- mobile-app/assets/entrusted_pull_all.png | Bin 0 -> 24891 bytes .../high_security/big_red_button_icon.png | Bin 0 -> 3495 bytes .../components/account_copy_action_sheet.dart | 4 +- .../features/components/emergency_button.dart | 71 ++++++++++++++++ .../main/screens/accounts_screen.dart | 7 +- .../screens/wallet_main/account_details.dart | 76 ++++++++++++------ .../screens/wallet_main/error_display.dart | 6 +- .../screens/wallet_main/history_section.dart | 2 +- .../screens/wallet_main/pull_component.dart | 63 +++++++++++++++ .../main/screens/wallet_main/wallet_main.dart | 74 +++++++++-------- .../lib/providers/account_providers.dart | 67 +++++++++++++++ .../active_account_transactions_provider.dart | 29 +++++++ .../lib/providers/wallet_providers.dart | 47 +++++++++++ .../lib/services/history_polling_manager.dart | 10 +-- .../notification_integration_service.dart | 2 +- mobile-app/pubspec.yaml | 1 + .../reversible_transfers_service.dart | 9 ++- 17 files changed, 397 insertions(+), 71 deletions(-) create mode 100644 mobile-app/assets/entrusted_pull_all.png create mode 100644 mobile-app/assets/high_security/big_red_button_icon.png create mode 100644 mobile-app/lib/features/components/emergency_button.dart create mode 100644 mobile-app/lib/features/main/screens/wallet_main/pull_component.dart diff --git a/mobile-app/assets/entrusted_pull_all.png b/mobile-app/assets/entrusted_pull_all.png new file mode 100644 index 0000000000000000000000000000000000000000..4c417fc485433b33aa80431b929f6f978b61f248 GIT binary patch literal 24891 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 IA#eWXe`0fmXaE2J literal 0 HcmV?d00001 diff --git a/mobile-app/assets/high_security/big_red_button_icon.png b/mobile-app/assets/high_security/big_red_button_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03b191a1bcd82dcb91f8d85e70f3f32c5a3b5b45 GIT binary patch literal 3495 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 VON+^3%X$C+002ovPDHLkV1f|%vAF;M literal 0 HcmV?d00001 diff --git a/mobile-app/lib/features/components/account_copy_action_sheet.dart b/mobile-app/lib/features/components/account_copy_action_sheet.dart index f8b40962..ab74ae90 100644 --- a/mobile-app/lib/features/components/account_copy_action_sheet.dart +++ b/mobile-app/lib/features/components/account_copy_action_sheet.dart @@ -5,7 +5,7 @@ import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; class AccountCopyActionSheet extends StatefulWidget { - final Account activeAccount; + final BaseAccount activeAccount; const AccountCopyActionSheet({super.key, required this.activeAccount}); @@ -92,7 +92,7 @@ class _AccountCopyActionSheetState extends State { } } -void showAccountCopyActionSheet(BuildContext context, Account activeAccount) { +void showAccountCopyActionSheet(BuildContext context, BaseAccount activeAccount) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, diff --git a/mobile-app/lib/features/components/emergency_button.dart b/mobile-app/lib/features/components/emergency_button.dart new file mode 100644 index 00000000..e312dbae --- /dev/null +++ b/mobile-app/lib/features/components/emergency_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +class EmergencyButton extends StatelessWidget { + const EmergencyButton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 52, + decoration: BoxDecoration( + gradient: const RadialGradient( + center: Alignment.center, + radius: 1.0, + colors: [ + Color(0xFF0AD4F6), + Color(0x26FFFFFF), // #FFFFFF with 15% opacity + ], + ), + borderRadius: BorderRadius.circular(4), + ), + child: Container( + margin: const EdgeInsets.all(1), // Creates the border effect + decoration: BoxDecoration(color: Colors.black.withValues(alpha: 1.0), borderRadius: BorderRadius.circular(3)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 17, + children: [ + SizedBox(width: 30, height: 30, child: Image.asset('assets/high_security/big_red_button_icon.png')), + const SizedBox( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'IN CASE OF EMERGENCY\n', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.40, + ), + ), + TextSpan( + text: 'PULL ALL FUNDS FROM THIS ACCOUNT', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w300, + height: 1.40, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 1a6106fb..7125f481 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -306,7 +306,7 @@ class _AccountsScreenState extends ConsumerState { return InkWell( onTap: () async { - await ref.read(activeAccountProvider.notifier).setActiveAccount(account); + await ref.read(activeDisplayAccountProvider.notifier).setActiveDisplayAccount(RegularAccount(account)); if (mounted) Navigator.pop(context); }, child: Stack( @@ -513,6 +513,11 @@ class _AccountsScreenState extends ConsumerState { onTap: () async { print('onTap: ${entrusted.accountId}'); + // Set the entrusted account as the active display account + await ref + .read(activeDisplayAccountProvider.notifier) + .setActiveDisplayAccount(EntrustedDisplayAccount(entrusted)); + // ignore: use_build_context_synchronously if (mounted) Navigator.pop(context); }, diff --git a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart index 1352be0b..5cfe47e9 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_copy_action_sheet.dart'; +import 'package:resonance_network_wallet/features/components/account_tag.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; class AccountDetails extends StatefulWidget { - final Account activeAccount; + final BaseAccount activeAccount; + final bool isEntrustedAccount; - const AccountDetails({super.key, required this.activeAccount}); + const AccountDetails({super.key, required this.activeAccount, this.isEntrustedAccount = false}); @override State createState() => _AccountDetailsState(); @@ -35,37 +37,61 @@ class _AccountDetailsState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration(color: context.themeColors.navbarBg), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + spacing: 2, children: [ - Image.asset('assets/active_dot.png', width: context.isTablet ? 28 : 20), - const SizedBox(width: 12), - Expanded( + if (widget.isEntrustedAccount) + AccountTag(text: 'Entrusted Account', color: context.themeColors.accountTagEntrusted), + Container( + width: double.infinity, + padding: const EdgeInsets.only(right: 10), child: Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 23, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12, children: [ - Text(widget.activeAccount.name, style: context.themeText.smallParagraph), - FutureBuilder( - future: checksumFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Text('Failed getting checksum', style: context.themeText.smallParagraph); - } + Image.asset('assets/active_dot.png', width: context.isTablet ? 28 : 20), + Container( + width: 195, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 1, + children: [ + SizedBox( + width: 195, + child: Text(widget.activeAccount.name, style: context.themeText.smallParagraph), + ), + FutureBuilder( + future: checksumFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text('Failed getting checksum', style: context.themeText.smallParagraph); + } - if (snapshot.hasData) { - return Text( - snapshot.data!, - style: context.themeText.tiny?.copyWith(color: context.themeColors.checksum), - ); - } + if (snapshot.hasData) { + return Text( + snapshot.data!, + style: context.themeText.tiny?.copyWith(color: context.themeColors.checksum), + ); + } - return Text('Loading checksum...', style: context.themeText.smallParagraph); - }, + return Text('Loading checksum...', style: context.themeText.smallParagraph); + }, + ), + ], + ), ), ], ), diff --git a/mobile-app/lib/features/main/screens/wallet_main/error_display.dart b/mobile-app/lib/features/main/screens/wallet_main/error_display.dart index 2fdb68b6..cfa71319 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/error_display.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/error_display.dart @@ -53,9 +53,9 @@ class _ErrorDisplayState extends ConsumerState { onPressed: () { widget.setIsErrorSheetDisplayed(false); - ref.invalidate(activeAccountProvider); - ref.invalidate(balanceProvider); - ref.invalidate(activeAccountTransactionsProvider); + ref.invalidate(activeDisplayAccountProvider); + ref.invalidate(displayBalanceProvider); + ref.invalidate(activeDisplayAccountTransactionsProvider); Navigator.pop(context); }, diff --git a/mobile-app/lib/features/main/screens/wallet_main/history_section.dart b/mobile-app/lib/features/main/screens/wallet_main/history_section.dart index dc6fba1f..b4392e2b 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/history_section.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/history_section.dart @@ -15,7 +15,7 @@ import 'package:resonance_network_wallet/services/transaction_service.dart'; class HistorySection extends ConsumerStatefulWidget { final AsyncValue allTransactionsAsync; - final Account activeAccount; + final BaseAccount activeAccount; const HistorySection({super.key, required this.allTransactionsAsync, required this.activeAccount}); diff --git a/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart b/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart new file mode 100644 index 00000000..4e3f0d84 --- /dev/null +++ b/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class PullComponent extends StatelessWidget { + const PullComponent({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 320, + height: 52, + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: ShapeDecoration( + color: Colors.black.withValues(alpha: 0.65), + shape: RoundedRectangleBorder( + side: const BorderSide(width: 1, color: Color(0xFF0AD4F6)), + borderRadius: BorderRadius.circular(4), + ), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 17, + children: [ + SizedBox(width: 30, height: 30, child: Stack()), + SizedBox( + width: 192, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'IN CASE OF EMERGENCY\n', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.40, + ), + ), + TextSpan( + text: 'PULL ALL FUNDS FROM THIS ACCOUNT', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w300, + height: 1.40, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart index efc896b8..533915e9 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart @@ -13,6 +13,7 @@ import 'package:resonance_network_wallet/features/main/screens/notifications_scr import 'package:resonance_network_wallet/features/main/screens/wallet_main/account_details.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_main/action_button.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_main/history_section.dart'; +import 'package:resonance_network_wallet/features/components/emergency_button.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_id_list_cache.dart'; @@ -57,14 +58,14 @@ class _WalletMainState extends ConsumerState { Widget build(BuildContext context) { _processIntentIfAvailable(); - final activeAccountAsync = ref.watch(activeAccountProvider); - final balanceAsync = ref.watch(balanceProvider); - final activeAccountTransactionsAsync = ref.watch(activeAccountTransactionsProvider); + final activeDisplayAccountAsync = ref.watch(activeDisplayAccountProvider); + final balanceAsync = ref.watch(displayBalanceProvider); + final activeAccountTransactionsAsync = ref.watch(activeDisplayAccountTransactionsProvider); final hasNotifications = ref.watch(notificationProvider).isNotEmpty; - return activeAccountAsync.when( - data: (activeAccount) { - if (activeAccount == null) { + return activeDisplayAccountAsync.when( + data: (activeDisplayAccount) { + if (activeDisplayAccount == null) { return const Center(child: Text('No active account. Please log in.')); // Safe empty state } return ScaffoldBase.refreshable( @@ -121,22 +122,22 @@ class _WalletMainState extends ConsumerState { scrollController: _scrollController, onRefresh: () async { // Refresh balances with loading indicator - final activeAccount = ref.read(activeAccountProvider).value; - if (activeAccount != null) { + final activeDisplayAccount = ref.read(activeDisplayAccountProvider).value; + if (activeDisplayAccount != null) { ref.invalidate(balanceProviderFamily); // Trigger a loading refresh on the filtered controller // used by active transactions await ref .read( filteredPaginationControllerProviderFamily( - AccountIdListCache.get([activeAccount.accountId]), + AccountIdListCache.get([activeDisplayAccount.account.accountId]), ).notifier, ) .loadingRefresh(); } - ref.invalidate(balanceProviderRaw); - // Invalidate combined active account provider to recompute - ref.invalidate(activeAccountTransactionsProvider); + ref.invalidate(displayBalanceProviderRaw); + // Invalidate combined active display account provider to recompute + ref.invalidate(activeDisplayAccountTransactionsProvider); }, slivers: [ SliverToBoxAdapter( @@ -146,7 +147,10 @@ class _WalletMainState extends ConsumerState { Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - AccountDetails(activeAccount: activeAccount), + AccountDetails( + activeAccount: activeDisplayAccount.account, + isEntrustedAccount: activeDisplayAccount is EntrustedDisplayAccount, + ), const SizedBox(height: 20), balanceAsync.when( data: (balance) => Text.rich( @@ -187,30 +191,36 @@ class _WalletMainState extends ConsumerState { ], ), const SizedBox(height: 18), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ActionButton( - type: ActionType.send, - onPressed: () { - Navigator.pushNamed(context, '/send'); - }, - ), - const SizedBox(width: 33), - ActionButton( - type: ActionType.receive, - onPressed: () { - showReceiveSheet(context); - }, - ), - ], - ), + if (activeDisplayAccount is RegularAccount) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ActionButton( + type: ActionType.send, + onPressed: () { + Navigator.pushNamed(context, '/send'); + }, + ), + const SizedBox(width: 33), + ActionButton( + type: ActionType.receive, + onPressed: () { + showReceiveSheet(context); + }, + ), + ], + ) + else if (activeDisplayAccount is EntrustedDisplayAccount) + const EmergencyButton(), const SizedBox(height: 30), ], ), ), SliverToBoxAdapter( - child: HistorySection(allTransactionsAsync: activeAccountTransactionsAsync, activeAccount: activeAccount), + child: HistorySection( + allTransactionsAsync: activeAccountTransactionsAsync, + activeAccount: activeDisplayAccount.account, + ), ), ], ); diff --git a/mobile-app/lib/providers/account_providers.dart b/mobile-app/lib/providers/account_providers.dart index f041c6aa..07e9ce59 100644 --- a/mobile-app/lib/providers/account_providers.dart +++ b/mobile-app/lib/providers/account_providers.dart @@ -2,6 +2,25 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; +// Union type for display accounts +sealed class DisplayAccount { + const DisplayAccount(); + + BaseAccount get account; +} + +class RegularAccount extends DisplayAccount { + @override + final Account account; + const RegularAccount(this.account); +} + +class EntrustedDisplayAccount extends DisplayAccount { + @override + final EntrustedAccount account; + const EntrustedDisplayAccount(this.account); +} + class AccountsNotifier extends StateNotifier>> { final AccountsService _accountsService; @@ -92,3 +111,51 @@ final activeAccountProvider = StateNotifierProvider> { + final SettingsService _settingsService; + + ActiveDisplayAccountNotifier(this._settingsService) : super(const AsyncValue.loading()) { + _loadActiveDisplayAccount(); + } + + Future _loadActiveDisplayAccount() async { + try { + final account = await _settingsService.getActiveAccount(); + if (account != null) { + state = AsyncValue.data(RegularAccount(account)); + } else { + state = const AsyncValue.data(null); + } + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } + + Future setActiveDisplayAccount(DisplayAccount displayAccount) async { + try { + switch (displayAccount) { + case RegularAccount(account: final account): + await _settingsService.setActiveAccount(account); + state = AsyncValue.data(displayAccount); + case EntrustedDisplayAccount(): + // For entrusted accounts, we don't save them as active account in settings + // They are temporary display accounts + state = AsyncValue.data(displayAccount); + } + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } + + void reset() { + state = const AsyncValue.loading(); + } +} + +final activeDisplayAccountProvider = StateNotifierProvider>(( + ref, +) { + final settingsService = ref.watch(settingsServiceProvider); + return ActiveDisplayAccountNotifier(settingsService); +}); diff --git a/mobile-app/lib/providers/active_account_transactions_provider.dart b/mobile-app/lib/providers/active_account_transactions_provider.dart index f339e5eb..2db6c72f 100644 --- a/mobile-app/lib/providers/active_account_transactions_provider.dart +++ b/mobile-app/lib/providers/active_account_transactions_provider.dart @@ -30,3 +30,32 @@ final activeAccountTransactionsProvider = Provider AsyncValue.error(err, stack), ); }); + +/// Provides a list of transactions for the currently active display account. +/// +/// This provider handles the logic of watching the active display account and fetching +/// the appropriate transaction list. It returns an [AsyncValue] that can be +/// in a loading, data, or error state. +final activeDisplayAccountTransactionsProvider = Provider>((ref) { + final activeDisplayAccountValue = ref.watch(activeDisplayAccountProvider); + + return activeDisplayAccountValue.when( + data: (activeDisplayAccount) { + if (activeDisplayAccount == null) { + return AsyncValue.data( + CombinedTransactionsList( + pendingCancellationIds: {}, + pendingTransactions: [], + reversibleTransfers: [], + otherTransfers: [], + ), + ); + } + return ref.watch( + filteredTransactionsProviderFamily(AccountIdListCache.get([activeDisplayAccount.account.accountId])), + ); + }, + loading: () => const AsyncValue.loading(), + error: (err, stack) => AsyncValue.error(err, stack), + ); +}); diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index 451d52ec..b69487cf 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -97,6 +97,53 @@ final balanceProvider = Provider>((ref) { ); }); +// Display account balance providers +final displayBalanceProviderRaw = Provider>((ref) { + final activeDisplayAccountAsyncValue = ref.watch(activeDisplayAccountProvider); + + return activeDisplayAccountAsyncValue.when( + data: (activeDisplayAccount) { + if (activeDisplayAccount == null) { + return AsyncValue.data(BigInt.zero); + } + return ref.watch(balanceProviderFamily(activeDisplayAccount.account.accountId)); + }, + loading: () => const AsyncValue.loading(), + error: (err, stack) => AsyncValue.error(err, stack), + ); +}); + +// Store for cached display balance to return on error +BigInt _cachedDisplayBalance = BigInt.zero; + +// Effective display balance (blockchain balance minus pending outgoing transactions) +final displayBalanceProvider = Provider>((ref) { + final balanceAsync = ref.watch(displayBalanceProviderRaw); + final pendingTransactions = ref.watch(pendingTransactionsProvider); + final activeDisplayAccountAsync = ref.watch(activeDisplayAccountProvider); + + return balanceAsync.when( + data: (blockchainBalance) { + final activeDisplayAccount = activeDisplayAccountAsync.value; + if (activeDisplayAccount == null) { + _cachedDisplayBalance = BigInt.zero; + return AsyncValue.data(BigInt.zero); + } + + final pendingOutgoing = _calculatePendingOutgoing(pendingTransactions, activeDisplayAccount.account.accountId); + final effectiveBalance = blockchainBalance - pendingOutgoing; + final result = effectiveBalance >= BigInt.zero ? effectiveBalance : BigInt.zero; + _cachedDisplayBalance = result; + return AsyncValue.data(result); + }, + loading: () => const AsyncValue.loading(), + error: (err, stack) { + // On error, return last cached balance + return AsyncValue.data(_cachedDisplayBalance); + }, + ); +}); + /// Calculates the total amount of pending outgoing transactions for a /// specific account BigInt _calculatePendingOutgoing(List pendingTransactions, String accountId) { diff --git a/mobile-app/lib/services/history_polling_manager.dart b/mobile-app/lib/services/history_polling_manager.dart index 415ee367..2f4fdac7 100644 --- a/mobile-app/lib/services/history_polling_manager.dart +++ b/mobile-app/lib/services/history_polling_manager.dart @@ -84,16 +84,16 @@ class HistoryPollingManager { void _refreshBalance({required bool showLoading}) { if (showLoading) { // For manual refresh - invalidate balance providers to show loading - final activeAccount = _ref.read(activeAccountProvider).value; - if (activeAccount != null) { + final activeDisplayAccount = _ref.read(activeDisplayAccountProvider).value; + if (activeDisplayAccount != null) { _ref.invalidate(balanceProviderFamily); } - _ref.invalidate(balanceProviderRaw); // Invalidate raw balance for loading state - // balanceProvider (effective) will auto-update when raw balance changes + _ref.invalidate(displayBalanceProviderRaw); // Invalidate raw balance for loading state + // displayBalanceProvider (effective) will auto-update when raw balance changes } else { // For silent refresh - just invalidate family to refresh data silently _ref.invalidate(balanceProviderFamily); - // balanceProvider (effective) will auto-update when raw balance changes + // displayBalanceProvider (effective) will auto-update when raw balance changes } } diff --git a/mobile-app/lib/services/notification_integration_service.dart b/mobile-app/lib/services/notification_integration_service.dart index 8406cabc..d160da87 100644 --- a/mobile-app/lib/services/notification_integration_service.dart +++ b/mobile-app/lib/services/notification_integration_service.dart @@ -61,7 +61,7 @@ class NotificationIntegrationService { void _setupBalanceListeners() { // Listen to balance changes for low balance alerts - _ref.listen>(balanceProvider, (previous, next) { + _ref.listen>(displayBalanceProvider, (previous, next) { next.whenData((balance) { // Check if balance is at or near existential deposit final existentialDeposit = balances.Constants().existentialDeposit; diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 14ebec12..42e3ea04 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -97,6 +97,7 @@ flutter: - assets/high_security/security_icon_big.svg - assets/high_security/step_indicator_active_icon.svg - assets/high_security/step_indicator_icon.svg + - assets/high_security/big_red_button_icon.png fonts: - family: Fira Code diff --git a/quantus_sdk/lib/src/services/reversible_transfers_service.dart b/quantus_sdk/lib/src/services/reversible_transfers_service.dart index 1741dfcc..0c968025 100644 --- a/quantus_sdk/lib/src/services/reversible_transfers_service.dart +++ b/quantus_sdk/lib/src/services/reversible_transfers_service.dart @@ -271,11 +271,18 @@ class ReversibleTransfersService { final quantusApi = Schrodinger(_substrateService.provider!); final accountId = crypto.ss58ToAccountId(s: guardianAddress); final interceptedAccounts = await quantusApi.query.reversibleTransfers.interceptorIndex(accountId); - return interceptedAccounts.map((id) { + + List result = interceptedAccounts.map((id) { final address = AddressExtension.ss58AddressFromBytes(Uint8List.fromList(id)); print('intercepted account: $address'); return address; }).toList(); + + // for testing , add random valid address... + if (result.isNotEmpty) { + result.add('qzkaf6wMjRqXzWyBuxc6VwfYtUmjUF5tqJXsFs47PXspR67wh'); + } + return result; } catch (e) { throw Exception('Failed to get intercepted accounts: $e'); } From d321f808b84b30fded8055bc2c627998fee43d82 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 21:51:34 +0800 Subject: [PATCH 05/22] remove unused files --- .../screens/wallet_main/error_display.dart | 114 ------------------ .../screens/wallet_main/pull_component.dart | 63 ---------- 2 files changed, 177 deletions(-) delete mode 100644 mobile-app/lib/features/main/screens/wallet_main/error_display.dart delete mode 100644 mobile-app/lib/features/main/screens/wallet_main/pull_component.dart diff --git a/mobile-app/lib/features/main/screens/wallet_main/error_display.dart b/mobile-app/lib/features/main/screens/wallet_main/error_display.dart deleted file mode 100644 index cfa71319..00000000 --- a/mobile-app/lib/features/main/screens/wallet_main/error_display.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; -import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; -import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; -import 'package:resonance_network_wallet/providers/account_providers.dart'; -import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; -import 'package:resonance_network_wallet/providers/wallet_providers.dart'; - -class ErrorDisplay extends ConsumerStatefulWidget { - final AsyncValue activeAccountAsync; - final Function(bool) setIsErrorSheetDisplayed; - - const ErrorDisplay({super.key, required this.activeAccountAsync, required this.setIsErrorSheetDisplayed}); - - @override - ConsumerState createState() => _ErrorDisplayState(); -} - -class _ErrorDisplayState extends ConsumerState { - @override - Widget build(BuildContext context) { - return SafeArea( - child: Container( - height: MediaQuery.of(context).size.height, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - decoration: ShapeDecoration( - color: context.themeColors.background, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - ), - child: Column( - children: [ - const Spacer(), - Icon(Icons.error_outline, color: context.themeColors.error, size: 50), - const SizedBox(height: 20), - Text('Failed to Connect', style: context.themeText.smallTitle, textAlign: TextAlign.center), - const SizedBox(height: 10), - Text( - widget.activeAccountAsync.error?.toString() ?? - 'Could not load wallet data. Please check your ' - 'network connection and try again.', - style: context.themeText.detail?.copyWith(color: context.themeColors.textError), - textAlign: TextAlign.center, - ), - const Spacer(), - Button( - variant: ButtonVariant.glassOutline, - label: 'Retry', - onPressed: () { - widget.setIsErrorSheetDisplayed(false); - - ref.invalidate(activeDisplayAccountProvider); - ref.invalidate(displayBalanceProvider); - ref.invalidate(activeDisplayAccountTransactionsProvider); - - Navigator.pop(context); - }, - ), - SizedBox(height: context.themeSize.bottomButtonSpacing), - ], - ), - ), - ); - } -} - -void showErrorDisplaySheet( - BuildContext context, { - required AsyncValue activeAccountAsync, - required Function(bool) setIsErrorSheetDisplayed, -}) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width, // Ensure full width - ), - builder: (context) => Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], - ), - ), - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), - child: Container( - color: Colors.black.useOpacity(0.3), - child: ErrorDisplay( - activeAccountAsync: activeAccountAsync, - setIsErrorSheetDisplayed: setIsErrorSheetDisplayed, - ), - ), - ), - ), - ], - ), - ); -} diff --git a/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart b/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart deleted file mode 100644 index 4e3f0d84..00000000 --- a/mobile-app/lib/features/main/screens/wallet_main/pull_component.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -class PullComponent extends StatelessWidget { - const PullComponent({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - width: 320, - height: 52, - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: ShapeDecoration( - color: Colors.black.withValues(alpha: 0.65), - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1, color: Color(0xFF0AD4F6)), - borderRadius: BorderRadius.circular(4), - ), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 17, - children: [ - SizedBox(width: 30, height: 30, child: Stack()), - SizedBox( - width: 192, - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'IN CASE OF EMERGENCY\n', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontFamily: 'Inter', - fontWeight: FontWeight.w600, - height: 1.40, - ), - ), - TextSpan( - text: 'PULL ALL FUNDS FROM THIS ACCOUNT', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontFamily: 'Inter', - fontWeight: FontWeight.w300, - height: 1.40, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ], - ); - } -} From cf25baa5fe3ab96ce7ed730c1ee7c2b2db3f031b Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 21:52:00 +0800 Subject: [PATCH 06/22] add high security tag --- .../screens/wallet_main/account_details.dart | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart index 5cfe47e9..10be5813 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart @@ -1,39 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_copy_action_sheet.dart'; import 'package:resonance_network_wallet/features/components/account_tag.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; -class AccountDetails extends StatefulWidget { +class AccountDetails extends ConsumerWidget { final BaseAccount activeAccount; final bool isEntrustedAccount; const AccountDetails({super.key, required this.activeAccount, this.isEntrustedAccount = false}); - @override - State createState() => _AccountDetailsState(); -} - -class _AccountDetailsState extends State { - final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); - - @override - void initState() { - super.initState(); - } - - void _showActionSheet() { - showAccountCopyActionSheet(context, widget.activeAccount); + void _showActionSheet(BuildContext context, BaseAccount account) { + showAccountCopyActionSheet(context, account); } @override - Widget build(BuildContext context) { - final checksumFuture = _checksumService.getHumanReadableName(widget.activeAccount.accountId); + Widget build(BuildContext context, WidgetRef ref) { + final checksumFuture = HumanReadableChecksumService().getHumanReadableName(activeAccount.accountId); + final isHighSecurityAsync = ref.watch(isHighSecurityProvider(activeAccount as Account)); + final isHighSecurity = isHighSecurityAsync.value ?? false; return GestureDetector( - onTap: _showActionSheet, + onTap: () => _showActionSheet(context, activeAccount), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration(color: context.themeColors.navbarBg), @@ -43,8 +35,10 @@ class _AccountDetailsState extends State { crossAxisAlignment: CrossAxisAlignment.end, spacing: 2, children: [ - if (widget.isEntrustedAccount) + if (isEntrustedAccount) AccountTag(text: 'Entrusted Account', color: context.themeColors.accountTagEntrusted), + if (isHighSecurity && !isEntrustedAccount) + AccountTag(text: 'High Security', color: context.themeColors.accountTagEntrusted), Container( width: double.infinity, padding: const EdgeInsets.only(right: 10), @@ -61,7 +55,7 @@ class _AccountDetailsState extends State { spacing: 12, children: [ Image.asset('assets/active_dot.png', width: context.isTablet ? 28 : 20), - Container( + SizedBox( width: 195, child: Column( mainAxisSize: MainAxisSize.min, @@ -71,7 +65,7 @@ class _AccountDetailsState extends State { children: [ SizedBox( width: 195, - child: Text(widget.activeAccount.name, style: context.themeText.smallParagraph), + child: Text(activeAccount.name, style: context.themeText.smallParagraph), ), FutureBuilder( future: checksumFuture, From efe3513cac272534553cdcf900ba4ee3f3bdb21e Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 21:52:10 +0800 Subject: [PATCH 07/22] unify refresh code --- .../screens/wallet_main/history_section.dart | 10 ++--- .../main/screens/wallet_main/wallet_main.dart | 41 ++++++++++--------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/mobile-app/lib/features/main/screens/wallet_main/history_section.dart b/mobile-app/lib/features/main/screens/wallet_main/history_section.dart index b4392e2b..6484f9f1 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/history_section.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/history_section.dart @@ -9,15 +9,14 @@ import 'package:resonance_network_wallet/features/main/screens/transactions_scre import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/models/combined_transactions_list.dart'; -import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; -import 'package:resonance_network_wallet/providers/filtered_all_transactions_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; class HistorySection extends ConsumerStatefulWidget { final AsyncValue allTransactionsAsync; final BaseAccount activeAccount; + final Future Function()? onRetry; - const HistorySection({super.key, required this.allTransactionsAsync, required this.activeAccount}); + const HistorySection({super.key, required this.allTransactionsAsync, required this.activeAccount, this.onRetry}); @override ConsumerState createState() => _HistorySectionState(); @@ -118,9 +117,8 @@ class _HistorySectionState extends ConsumerState { Button( variant: ButtonVariant.neutral, label: 'Retry', - onPressed: () { - ref.invalidate(filteredTransactionsProviderFamily); - ref.invalidate(activeAccountTransactionsProvider); + onPressed: () async { + widget.onRetry?.call(); }, ), ], diff --git a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart index 533915e9..2c9f9f85 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart @@ -42,6 +42,26 @@ class _WalletMainState extends ConsumerState { super.dispose(); } + Future _refreshData() async { + // Refresh balances with loading indicator + final activeDisplayAccount = ref.read(activeDisplayAccountProvider).value; + if (activeDisplayAccount != null) { + ref.invalidate(balanceProviderFamily); + // Trigger a loading refresh on the filtered controller + // used by active transactions + await ref + .read( + filteredPaginationControllerProviderFamily( + AccountIdListCache.get([activeDisplayAccount.account.accountId]), + ).notifier, + ) + .loadingRefresh(); + } + ref.invalidate(displayBalanceProviderRaw); + // Invalidate combined active display account provider to recompute + ref.invalidate(activeDisplayAccountTransactionsProvider); + } + void _processIntentIfAvailable() { final sharedAccount = ref.read(sharedAccountIntentProvider); @@ -120,25 +140,7 @@ class _WalletMainState extends ConsumerState { ), ], scrollController: _scrollController, - onRefresh: () async { - // Refresh balances with loading indicator - final activeDisplayAccount = ref.read(activeDisplayAccountProvider).value; - if (activeDisplayAccount != null) { - ref.invalidate(balanceProviderFamily); - // Trigger a loading refresh on the filtered controller - // used by active transactions - await ref - .read( - filteredPaginationControllerProviderFamily( - AccountIdListCache.get([activeDisplayAccount.account.accountId]), - ).notifier, - ) - .loadingRefresh(); - } - ref.invalidate(displayBalanceProviderRaw); - // Invalidate combined active display account provider to recompute - ref.invalidate(activeDisplayAccountTransactionsProvider); - }, + onRefresh: _refreshData, slivers: [ SliverToBoxAdapter( child: Column( @@ -220,6 +222,7 @@ class _WalletMainState extends ConsumerState { child: HistorySection( allTransactionsAsync: activeAccountTransactionsAsync, activeAccount: activeDisplayAccount.account, + onRetry: _refreshData, ), ), ], From d5223d669c34c89910e0c6fd61c39ba69d61dece Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 22:16:05 +0800 Subject: [PATCH 08/22] send from high security account implemented --- .../components/segmented_control.dart | 8 +- .../screens/send/send_progress_overlay.dart | 30 +++++-- .../main/screens/send/send_screen.dart | 87 +++++++++++++++---- .../transaction_submission_service.dart | 37 ++++++++ 4 files changed, 135 insertions(+), 27 deletions(-) diff --git a/mobile-app/lib/features/components/segmented_control.dart b/mobile-app/lib/features/components/segmented_control.dart index e7554e4a..d99ca307 100644 --- a/mobile-app/lib/features/components/segmented_control.dart +++ b/mobile-app/lib/features/components/segmented_control.dart @@ -19,6 +19,7 @@ class SegmentedControl extends StatefulWidget { final SegmentWidthMode widthMode; final double? minSegmentWidth; final double? maxSegmentWidth; + final Color? selectedColor; const SegmentedControl({ super.key, @@ -32,6 +33,7 @@ class SegmentedControl extends StatefulWidget { this.widthMode = SegmentWidthMode.equal, this.minSegmentWidth, this.maxSegmentWidth, + this.selectedColor, }); @override @@ -237,8 +239,8 @@ class _SegmentedControlState extends State> with SingleTi @override Widget build(BuildContext context) { - final backgroundColor = context.themeColors.darkGray; - final selectedColor = context.themeColors.buttonNeutral; + final borderColor = context.themeColors.darkGray; + final selectedColor = widget.selectedColor ?? context.themeColors.buttonNeutral; final selectedTextColor = context.themeColors.textSecondary; final unselectedTextColor = context.themeColors.textMuted; @@ -248,7 +250,7 @@ class _SegmentedControlState extends State> with SingleTi return Container( height: widget.height, - decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(widget.borderRadius)), + decoration: BoxDecoration(color: borderColor, borderRadius: BorderRadius.circular(widget.borderRadius)), padding: widget.padding, child: Stack( children: [ diff --git a/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart b/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart index 3f5b0ff2..c21fbf6f 100644 --- a/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart +++ b/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart @@ -37,6 +37,7 @@ class SendConfirmationOverlay extends ConsumerStatefulWidget { final BigInt fee; final int reversibleTimeSeconds; final int blockHeight; + final bool isHighSecurity; const SendConfirmationOverlay({ required this.amount, @@ -46,6 +47,7 @@ class SendConfirmationOverlay extends ConsumerStatefulWidget { required this.fee, required this.reversibleTimeSeconds, required this.blockHeight, + this.isHighSecurity = false, super.key, }); @@ -272,14 +274,26 @@ class SendConfirmationOverlayState extends ConsumerState { BigInt _maxBalance = BigInt.zero; BigInt _networkFee = BigInt.zero; // Actual network fee fetched from chain bool _isFetchingFee = false; + bool _isHighSecurity = false; BigInt _amount = BigInt.zero; bool _hasAddressError = false; bool _hasAmountError = false; @@ -417,6 +418,7 @@ class SendScreenState extends ConsumerState { fee: _networkFee, reversibleTimeSeconds: _sendMode.isReversible ? _reversibleTimeSeconds : 0, blockHeight: _blockHeight ?? 0, + isHighSecurity: _isHighSecurity, onClose: () => Navigator.pop(context), ), ), @@ -459,12 +461,51 @@ class SendScreenState extends ConsumerState { builder: (context, ref, child) { final balanceAsyncValue = ref.watch(effectiveMaxBalanceProvider); final includeExistentialDeposit = ref.watch(existentialDepositToggleProvider); + final highSecurityConfigAsync = activeAccount != null + ? ref.watch(highSecurityConfigProvider(activeAccount!)) + : null; + final highSecurityDelaySeconds = highSecurityConfigAsync?.value?.safeguardWindow.inSeconds ?? 0; + final isHighSecurity = highSecurityDelaySeconds > 0; + + // Update the class variable + _isHighSecurity = isHighSecurity; + + // For high security accounts, ensure send mode is reversible and override the time + if (isHighSecurity) { + if (_sendMode != SendMode.reversible) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _sendMode = SendMode.reversible; + }); + }); + } + if (highSecurityDelaySeconds > 0 && _reversibleTimeSeconds != highSecurityDelaySeconds) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _setReversibleTimeSeconds(highSecurityDelaySeconds); + }); + } + } return balanceAsyncValue.when( data: (balance) { _maxBalance = balance; - return _buildSendContent(context, ref, includeExistentialDeposit); + return _buildSendContent( + context, + ref, + includeExistentialDeposit, + isHighSecurity, + highSecurityDelaySeconds, + (value) { + // Prevent selecting "Now" for high security accounts + if (isHighSecurity && value == SendMode.immediate) { + return; + } + setState(() { + _sendMode = value; + }); + }, + ); }, loading: () => Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), error: (error, stack) => Center( @@ -483,7 +524,14 @@ class SendScreenState extends ConsumerState { ); } - Widget _buildSendContent(BuildContext context, WidgetRef ref, bool includeExistentialDeposit) { + Widget _buildSendContent( + BuildContext context, + WidgetRef ref, + bool includeExistentialDeposit, + bool isHighSecurity, + int highSecurityDelaySeconds, + Function(SendMode) onSelectionChanged, + ) { final formattingService = ref.read(numberFormattingServiceProvider); return Column( @@ -663,17 +711,14 @@ class SendScreenState extends ConsumerState { const SizedBox(height: 14), SegmentedControl( widthMode: SegmentWidthMode.custom, - selectedValue: _sendMode, - onSelectionChanged: (value) { - setState(() { - _sendMode = value; - }); - }, + selectedValue: isHighSecurity ? SendMode.reversible : _sendMode, + onSelectionChanged: isHighSecurity ? null : onSelectionChanged, + selectedColor: isHighSecurity ? context.themeColors.accountTagEntrusted.useOpacity(1) : null, items: [ SegmentedControlItem( value: SendMode.reversible, child: InkWell( - onTap: _sendMode == SendMode.reversible + onTap: _sendMode == SendMode.reversible && !isHighSecurity ? () { showTimePickerSheet( context, @@ -694,22 +739,32 @@ class SendScreenState extends ConsumerState { Row( spacing: 10, children: [ - SvgPicture.asset('assets/set_reversible.svg'), + Opacity( + opacity: isHighSecurity ? 0.7 : 1.0, + child: SvgPicture.asset('assets/set_reversible.svg'), + ), Text(_formatReversibleTime(), style: context.themeText.smallParagraph), ], ), - if (_sendMode.isReversible) + if (_sendMode.isReversible && !isHighSecurity) Icon(Icons.edit, color: const Color(0x75000000), size: context.isTablet ? 22 : 14), + if (isHighSecurity) + Icon( + Icons.lock, + color: context.themeColors.accountTagEntrusted, + size: context.isTablet ? 22 : 14, + ), ], ), ), ), ), - SegmentedControlItem( - customWidth: 76.0, - value: SendMode.immediate, - child: Text('Now', style: context.themeText.smallParagraph), - ), + if (!isHighSecurity) + SegmentedControlItem( + customWidth: 76.0, + value: SendMode.immediate, + child: Text('Now', style: context.themeText.smallParagraph), + ), ], ), const SizedBox(height: 37), diff --git a/mobile-app/lib/services/transaction_submission_service.dart b/mobile-app/lib/services/transaction_submission_service.dart index 3092457e..4b17ce56 100644 --- a/mobile-app/lib/services/transaction_submission_service.dart +++ b/mobile-app/lib/services/transaction_submission_service.dart @@ -93,6 +93,43 @@ class TransactionSubmissionService { await submitAndTrackTransaction(submissionBuilder, pending, maxRetries: maxRetries); } + Future scheduleTransfer({ + required Account account, + required String recipientAddress, + required BigInt amount, + required BigInt fee, + required int blockHeight, + }) async { + // A. Create the initial pending transaction event + final pendingTx = PendingTransactionEvent( + tempId: 'pending_${DateTime.now().millisecondsSinceEpoch}', + from: account.accountId, + to: recipientAddress, + amount: amount, + timestamp: DateTime.now(), + transactionState: TransactionState.created, + fee: fee, + blockNumber: blockHeight, + isReversible: true, + ); + + // B. Immediately add it to the state so the UI can update + _ref.read(pendingTransactionsProvider.notifier).add(pendingTx); + + // C. Define the builder function that creates fresh submissions on each retry + Future submissionBuilder() async { + return ReversibleTransfersService().scheduleReversibleTransfer( + account: account, + recipientAddress: recipientAddress, + amount: amount, + ); + } + + TelemetryService().sendEvent('send_high_security_reversible'); + + await submitAndTrackTransaction(submissionBuilder, pendingTx); + } + PendingTransactionEvent createPendingTransaction({ required String from, required String to, From 0367b1d90486bb89452d7764110b4dc1553911a3 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 15 Jan 2026 22:19:51 +0800 Subject: [PATCH 09/22] fix crash on home screen for entrusted account --- .../features/main/screens/wallet_main/account_details.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart index 10be5813..b17cc5d9 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/account_details.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/account_details.dart @@ -21,8 +21,10 @@ class AccountDetails extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final checksumFuture = HumanReadableChecksumService().getHumanReadableName(activeAccount.accountId); - final isHighSecurityAsync = ref.watch(isHighSecurityProvider(activeAccount as Account)); - final isHighSecurity = isHighSecurityAsync.value ?? false; + final isHighSecurityAsync = activeAccount is Account + ? ref.watch(isHighSecurityProvider(activeAccount as Account)) + : null; + final isHighSecurity = isHighSecurityAsync?.value ?? false; return GestureDetector( onTap: () => _showActionSheet(context, activeAccount), From e8002f8161904ab9f342fb2c77e5081624f5794d Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 16 Jan 2026 13:08:22 +0800 Subject: [PATCH 10/22] intercept transaction flow implemented --- .../assets/high_security/intercept_icon.svg | 4 + .../reversible_transaction_action_sheet.dart | 215 ++++++++++++++++-- .../components/transaction_list_item.dart | 20 +- mobile-app/pubspec.yaml | 1 + .../src/services/high_security_service.dart | 5 + .../reversible_transfers_service.dart | 4 + 6 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 mobile-app/assets/high_security/intercept_icon.svg diff --git a/mobile-app/assets/high_security/intercept_icon.svg b/mobile-app/assets/high_security/intercept_icon.svg new file mode 100644 index 00000000..1e0e41c0 --- /dev/null +++ b/mobile-app/assets/high_security/intercept_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart index c0a6cf29..03167853 100644 --- a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart +++ b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hex/hex.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; import 'package:resonance_network_wallet/features/components/reversible_timer.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; @@ -18,11 +17,21 @@ import 'package:resonance_network_wallet/providers/pending_cancellations_provide import 'package:resonance_network_wallet/services/reversible_transfer_monitoring_service.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/shared/extensions/snackbar_extensions.dart'; +import 'package:url_launcher/url_launcher.dart'; + +enum ReversibleTransactionMode { reversible, guardianIntercept } class ReversibleTransactionActionSheet extends ConsumerStatefulWidget { final ReversibleTransferEvent transaction; + final ReversibleTransactionMode mode; + final EntrustedAccount? entrustedAccount; - const ReversibleTransactionActionSheet({super.key, required this.transaction}); + const ReversibleTransactionActionSheet({ + super.key, + required this.transaction, + this.mode = ReversibleTransactionMode.reversible, + this.entrustedAccount, + }); @override ConsumerState createState() => _ReversibleTransactionActionSheetState(); @@ -83,6 +92,58 @@ class _ReversibleTransactionActionSheetState extends ConsumerState widget.mode == ReversibleTransactionMode.guardianIntercept; + + String get _iconPath => _isGuardianIntercept ? 'assets/high_security/intercept_icon.svg' : 'assets/hourglass.svg'; + + String get _title => _isGuardianIntercept ? 'Intercept Transaction' : 'Reversible Transaction'; + + String get _subtitle => + _isGuardianIntercept ? 'Pull this transaction to your account' : 'Reverse or keep your transaction'; + + Color get _titleColor => _isGuardianIntercept ? const Color(0xFFFFE91F) : context.themeColors.checksum; + + Color get _confirmationTextColor => _isGuardianIntercept ? const Color(0xFFFFE91F) : context.themeColors.textMuted; + + String get _confirmButtonLabel => _isGuardianIntercept ? 'Intercept' : 'Reverse'; + + String get _confirmationText => _isGuardianIntercept + ? 'Are you sure you want to intercept this transaction and pull it to your account?' + : 'Are you sure you want to reverse this tx?'; + + Color get _confirmButtonColor => _isGuardianIntercept ? const Color(0xFFFFE91F) : context.themeColors.buttonDanger; + + Color get _confirmButtonTextColor => _isGuardianIntercept ? const Color(0xFF0B0F14) : Colors.white; + @override Widget build(BuildContext context) { return SafeArea( @@ -139,7 +200,7 @@ class _ReversibleTransactionActionSheetState extends ConsumerState { } void showTransactionActionSheet(BuildContext context, {required TransactionEvent transaction, required role}) { + final container = ProviderScope.containerOf(context, listen: false); + final activeDisplayAccount = container.read(activeDisplayAccountProvider).value; + EntrustedAccount? entrustedAccount; + if (activeDisplayAccount is EntrustedDisplayAccount) { + entrustedAccount = activeDisplayAccount.account; + } + final isEntrustedAccount = entrustedAccount != null; + Widget sheet; - if (transaction.isReversibleScheduled && (role == TransactionRole.sender || role == TransactionRole.both)) { - sheet = ReversibleTransactionActionSheet(transaction: transaction as ReversibleTransferEvent); + if (transaction is ReversibleTransferEvent reversibleTx && + (reversibleTx.isReversibleScheduled || reversibleTx.isReversibleCancelled) && + (role == TransactionRole.sender || role == TransactionRole.both)) { + sheet = ReversibleTransactionActionSheet( + transaction: reversibleTx, + mode: isEntrustedAccount ? ReversibleTransactionMode.guardianIntercept : ReversibleTransactionMode.reversible, + entrustedAccount: entrustedAccount, + ); } else { sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 42e3ea04..4b58f1b4 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -98,6 +98,7 @@ flutter: - assets/high_security/step_indicator_active_icon.svg - assets/high_security/step_indicator_icon.svg - assets/high_security/big_red_button_icon.png + - assets/high_security/intercept_icon.svg fonts: - family: Fira Code diff --git a/quantus_sdk/lib/src/services/high_security_service.dart b/quantus_sdk/lib/src/services/high_security_service.dart index a18406b8..e682bbbd 100644 --- a/quantus_sdk/lib/src/services/high_security_service.dart +++ b/quantus_sdk/lib/src/services/high_security_service.dart @@ -67,6 +67,11 @@ class HighSecurityService { .toList(); } + Future getGuardianAccount(EntrustedAccount entrustedAccount) async { + final accounts = await AccountsService().getAccounts(); + return accounts.firstWhere((a) => a.accountId == entrustedAccount.parentAccountId); + } + Future getHighSecurityConfig(String address) async { final hsData = await _reversibleTransfersService.getHighSecurityConfig(address); print('getHighSecurityConfig: $address -> $hsData'); diff --git a/quantus_sdk/lib/src/services/reversible_transfers_service.dart b/quantus_sdk/lib/src/services/reversible_transfers_service.dart index 0c968025..b7562f38 100644 --- a/quantus_sdk/lib/src/services/reversible_transfers_service.dart +++ b/quantus_sdk/lib/src/services/reversible_transfers_service.dart @@ -257,6 +257,10 @@ class ReversibleTransfersService { } } + Future interceptTransaction({required Account guardianAccount, required H256 transactionId}) async { + return cancelReversibleTransfer(account: guardianAccount, transactionId: transactionId); + } + /// Check if account is a guardian (interceptor) for any accounts Future isGuardian(String address) async { print('isGuardian: $address'); From 8fc3574c7ae6085a359921e34385a06263d69006 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 16 Jan 2026 13:20:05 +0800 Subject: [PATCH 11/22] show intercept view in the entrusted account view for cancelled reversible --- .../reversible_transaction_action_sheet.dart | 37 +++++++++++-------- .../components/transaction_list_item.dart | 20 ++++++---- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart index 03167853..01a0d885 100644 --- a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart +++ b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart @@ -40,7 +40,7 @@ class ReversibleTransactionActionSheet extends ConsumerStatefulWidget { enum _SheetState { initial, confirmCancel, cancelled } class _ReversibleTransactionActionSheetState extends ConsumerState { - _SheetState _sheetState = _SheetState.initial; + late _SheetState _sheetState; bool _isCancelling = false; late Timer _timer; @@ -61,25 +61,32 @@ class _ReversibleTransactionActionSheetState extends ConsumerState Duration.zero) { - _remainingTime = _remainingTime - const Duration(seconds: 1); - } else { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { timer.cancel(); - // Maybe close the sheet or show a different state when timer ends. - // For now, just stopping the timer. + return; } + setState(() { + if (_remainingTime > Duration.zero) { + _remainingTime = _remainingTime - const Duration(seconds: 1); + } else { + timer.cancel(); + } + }); }); - }); + } } @override diff --git a/mobile-app/lib/features/components/transaction_list_item.dart b/mobile-app/lib/features/components/transaction_list_item.dart index 3bc2fa45..c866572f 100644 --- a/mobile-app/lib/features/components/transaction_list_item.dart +++ b/mobile-app/lib/features/components/transaction_list_item.dart @@ -245,14 +245,18 @@ void showTransactionActionSheet(BuildContext context, {required TransactionEvent Widget sheet; - if (transaction is ReversibleTransferEvent reversibleTx && - (reversibleTx.isReversibleScheduled || reversibleTx.isReversibleCancelled) && - (role == TransactionRole.sender || role == TransactionRole.both)) { - sheet = ReversibleTransactionActionSheet( - transaction: reversibleTx, - mode: isEntrustedAccount ? ReversibleTransactionMode.guardianIntercept : ReversibleTransactionMode.reversible, - entrustedAccount: entrustedAccount, - ); + if (transaction is ReversibleTransferEvent) { + final reversibleTx = transaction; + if ((reversibleTx.isReversibleScheduled || reversibleTx.isReversibleCancelled) && + (role == TransactionRole.sender || role == TransactionRole.both)) { + sheet = ReversibleTransactionActionSheet( + transaction: reversibleTx, + mode: isEntrustedAccount ? ReversibleTransactionMode.guardianIntercept : ReversibleTransactionMode.reversible, + entrustedAccount: entrustedAccount, + ); + } else { + sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); + } } else { sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); } From 1a345b12a6ceb7a5baef3f09b1d8332e2727a918 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 13:14:38 +0800 Subject: [PATCH 12/22] format --- .../main/screens/account_settings_screen.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mobile-app/lib/features/main/screens/account_settings_screen.dart b/mobile-app/lib/features/main/screens/account_settings_screen.dart index 47ba7c92..c81240b1 100644 --- a/mobile-app/lib/features/main/screens/account_settings_screen.dart +++ b/mobile-app/lib/features/main/screens/account_settings_screen.dart @@ -46,7 +46,7 @@ class AccountSettingsScreen extends ConsumerStatefulWidget { class _AccountSettingsScreenState extends ConsumerState { void _editAccountName() { if (widget.account is! Account) return; - + Navigator.push( context, MaterialPageRoute(builder: (context) => CreateAccountScreen(accountToEdit: widget.account as Account)), @@ -133,7 +133,7 @@ class _AccountSettingsScreenState extends ConsumerState { Future _disconnectWallet() async { if (widget.account is! Account) return; - + try { final accountsService = AccountsService(); await accountsService.removeAccount(widget.account as Account); @@ -179,7 +179,10 @@ class _AccountSettingsScreenState extends ConsumerState { _buildShareSection(), const SizedBox(height: 20), _buildAddressSection(), - if (FeatureFlags.enableHighSecurity && widget.account is Account) ...[const SizedBox(height: 20), _buildHighSecuritySection(context)], + if (FeatureFlags.enableHighSecurity && widget.account is Account) ...[ + const SizedBox(height: 20), + _buildHighSecuritySection(context), + ], if (widget.account is Account && (widget.account as Account).accountType == AccountType.keystone) ...[ const SizedBox(height: 20), _buildDisconnectWalletButton(), @@ -223,8 +226,7 @@ class _AccountSettingsScreenState extends ConsumerState { children: [ Text(widget.account.name, style: context.themeText.smallTitle), const SizedBox(width: 8), - if (widget.account is Account) - const Icon(Icons.edit, color: Colors.white70, size: 16), + if (widget.account is Account) const Icon(Icons.edit, color: Colors.white70, size: 16), ], ), ), From bb0225667a63b83056c7b83fcd697320ebc99285 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 17:19:27 +0800 Subject: [PATCH 13/22] add emergency button --- .../features/components/emergency_button.dart | 164 +++++++++----- .../pull_funds_confirmation_sheet.dart | 214 ++++++++++++++++++ .../lib/src/services/balances_service.dart | 7 + .../src/services/high_security_service.dart | 51 +++++ 4 files changed, 383 insertions(+), 53 deletions(-) create mode 100644 mobile-app/lib/features/components/pull_funds_confirmation_sheet.dart diff --git a/mobile-app/lib/features/components/emergency_button.dart b/mobile-app/lib/features/components/emergency_button.dart index e312dbae..928c4277 100644 --- a/mobile-app/lib/features/components/emergency_button.dart +++ b/mobile-app/lib/features/components/emergency_button.dart @@ -1,67 +1,125 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/pull_funds_confirmation_sheet.dart'; +import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; -class EmergencyButton extends StatelessWidget { +class EmergencyButton extends ConsumerWidget { const EmergencyButton({super.key}); @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16), + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: () async { + final activeDisplayAccount = ref.read(activeAccountProvider).value; + if (activeDisplayAccount is EntrustedDisplayAccount) { + final accounts = ref.read(accountsProvider).value; + final guardianAccount = accounts?.firstWhereOrNull( + (a) => a.accountId == activeDisplayAccount.account.parentAccountId, + ); + + if (guardianAccount != null) { + try { + if (context.mounted) { + showPullFundsConfirmationSheet( + context, + activeDisplayAccount.account.accountId, + guardianAccount, + () async { + try { + final highSecurityService = ref.read(highSecurityServiceProvider); + await highSecurityService.pullAllFunds(activeDisplayAccount.account.accountId, guardianAccount); + + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Emergency funds pull initiated successfully'))); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pull funds: $e'))); + } + } + }, + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to show confirmation: $e'))); + } + } + } else { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Guardian account not found on this device'))); + } + } + } + }, child: Container( - height: 52, - decoration: BoxDecoration( - gradient: const RadialGradient( - center: Alignment.center, - radius: 1.0, - colors: [ - Color(0xFF0AD4F6), - Color(0x26FFFFFF), // #FFFFFF with 15% opacity - ], - ), - borderRadius: BorderRadius.circular(4), - ), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( - margin: const EdgeInsets.all(1), // Creates the border effect - decoration: BoxDecoration(color: Colors.black.withValues(alpha: 1.0), borderRadius: BorderRadius.circular(3)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 17, - children: [ - SizedBox(width: 30, height: 30, child: Image.asset('assets/high_security/big_red_button_icon.png')), - const SizedBox( - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'IN CASE OF EMERGENCY\n', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontFamily: 'Inter', - fontWeight: FontWeight.w600, - height: 1.40, + height: 52, + decoration: BoxDecoration( + gradient: const RadialGradient( + center: Alignment.center, + radius: 1.0, + colors: [ + Color(0xFF0AD4F6), + Color(0x26FFFFFF), // #FFFFFF with 15% opacity + ], + ), + borderRadius: BorderRadius.circular(4), + ), + child: Container( + margin: const EdgeInsets.all(1), // Creates the border effect + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 1.0), + borderRadius: BorderRadius.circular(3), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 17, + children: [ + SizedBox(width: 30, height: 30, child: Image.asset('assets/high_security/big_red_button_icon.png')), + const SizedBox( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'IN CASE OF EMERGENCY\n', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.40, + ), ), - ), - TextSpan( - text: 'PULL ALL FUNDS FROM THIS ACCOUNT', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontFamily: 'Inter', - fontWeight: FontWeight.w300, - height: 1.40, + TextSpan( + text: 'PULL ALL FUNDS FROM THIS ACCOUNT', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w300, + height: 1.40, + ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/mobile-app/lib/features/components/pull_funds_confirmation_sheet.dart b/mobile-app/lib/features/components/pull_funds_confirmation_sheet.dart new file mode 100644 index 00000000..3c198f33 --- /dev/null +++ b/mobile-app/lib/features/components/pull_funds_confirmation_sheet.dart @@ -0,0 +1,214 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; +import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; + +class PullFundsConfirmationSheet extends ConsumerStatefulWidget { + final String lostAccountAddress; + final Account guardianAccount; + final VoidCallback onConfirm; + + const PullFundsConfirmationSheet({ + super.key, + required this.lostAccountAddress, + required this.guardianAccount, + required this.onConfirm, + }); + + @override + ConsumerState createState() => _PullFundsConfirmationSheetState(); +} + +class _PullFundsConfirmationSheetState extends ConsumerState { + final NumberFormattingService _formattingService = NumberFormattingService(); + BigInt? _fee; + BigInt? _guardianBalance; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _calculateFeeAndBalance(); + } + + Future _calculateFeeAndBalance() async { + try { + final highSecurityService = ref.read(highSecurityServiceProvider); + final substrateService = ref.read(substrateServiceProvider); + + // Run both fetch operations in parallel + final results = await Future.wait([ + highSecurityService.getPullAllFundsFee(widget.lostAccountAddress, widget.guardianAccount), + substrateService.queryBalance(widget.guardianAccount.accountId), + ]); + + if (mounted) { + setState(() { + _fee = (results[0] as ExtrinsicFeeData).fee; + _guardianBalance = results[1] as BigInt; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + } + + bool get _canProceed { + if (_fee == null || _guardianBalance == null) return false; + return _guardianBalance! >= _fee!; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + bottom: false, + child: Container( + height: MediaQuery.of(context).size.height * 0.8, + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 16), + decoration: ShapeDecoration( + color: context.themeColors.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(7), + decoration: ShapeDecoration(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100))), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, size: context.isTablet ? 28 : 24), + ), + ], + ), + ), + const SizedBox(height: 69), + Text( + 'Are you sure you want to pull all funds from the high security account into the guardian account?', + textAlign: TextAlign.center, + style: context.themeText.smallTitle, + ), + const SizedBox(height: 48), + if (_isLoading) + CircularProgressIndicator(color: context.themeColors.background, strokeWidth: 2.0) + else if (_error != null) + Text( + 'Error calculating fee: $_error', + textAlign: TextAlign.center, + style: context.themeText.detail?.copyWith(color: context.themeColors.textError), + ) + else + Column( + children: [ + Text( + 'Fee: ${_formattingService.formatBalance(_fee!)} ${AppConstants.tokenSymbol}', + textAlign: TextAlign.center, + style: context.themeText.smallTitle, + ), + if (!_canProceed) ...[ + const SizedBox(height: 8), + Text( + 'Insufficient funds in guardian account', + textAlign: TextAlign.center, + style: context.themeText.detail?.copyWith(color: context.themeColors.textError), + ), + ], + ], + ), + const SizedBox(height: 44), + Row( + spacing: context.themeSize.buttonsHorizontalSpacing, + children: [ + Expanded( + child: Button( + variant: ButtonVariant.danger, + label: 'Confirm', + textStyle: context.themeText.smallParagraph?.copyWith(fontWeight: FontWeight.w600), + isDisabled: _isLoading || !_canProceed, + onPressed: () { + Navigator.pop(context); + widget.onConfirm(); + }, + ), + ), + Expanded( + child: Button( + variant: ButtonVariant.neutral, + label: 'Cancel', + textStyle: context.themeText.smallParagraph?.copyWith(fontWeight: FontWeight.w600), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ), + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], + ), + ), + ); + } +} + +void showPullFundsConfirmationSheet( + BuildContext context, + String lostAccountAddress, + Account guardianAccount, + VoidCallback onConfirm, +) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, // Ensure full width + ), + builder: (context) => Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, const Color(0xFF312E6E).useOpacity(0.4), Colors.black], + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container( + color: Colors.black.useOpacity(0.3), + child: PullFundsConfirmationSheet( + lostAccountAddress: lostAccountAddress, + guardianAccount: guardianAccount, + onConfirm: onConfirm, + ), + ), + ), + ), + ], + ), + ); +} diff --git a/quantus_sdk/lib/src/services/balances_service.dart b/quantus_sdk/lib/src/services/balances_service.dart index b5ea0ea1..1fbec932 100644 --- a/quantus_sdk/lib/src/services/balances_service.dart +++ b/quantus_sdk/lib/src/services/balances_service.dart @@ -43,4 +43,11 @@ class BalancesService { final runtimeCall = quantusApi.tx.balances.transferAllowDeath(dest: multiDest, value: amount); return runtimeCall; } + + Balances getTransferAllCall(String targetAddress, {bool keepAlive = false}) { + final quantusApi = Schrodinger(_substrateService.provider!); + final multiDest = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: targetAddress)); + final runtimeCall = quantusApi.tx.balances.transferAll(dest: multiDest, keepAlive: keepAlive); + return runtimeCall; + } } diff --git a/quantus_sdk/lib/src/services/high_security_service.dart b/quantus_sdk/lib/src/services/high_security_service.dart index e682bbbd..52f50092 100644 --- a/quantus_sdk/lib/src/services/high_security_service.dart +++ b/quantus_sdk/lib/src/services/high_security_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:quantus_sdk/generated/schrodinger/schrodinger.dart'; import 'package:quantus_sdk/generated/schrodinger/types/qp_scheduler/block_number_or_timestamp.dart' as qp; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:quantus_sdk/src/extensions/address_extension.dart'; @@ -87,4 +88,54 @@ class HighSecurityService { return null; } } + + Future pullAllFunds(String lostAccountAddress, Account guardianAccount) async { + print('pullAllFunds: $lostAccountAddress, $guardianAccount'); + // 1. Initiate recovery (rescuer = guardian) + Utility batchCall = _getPullAllFundsCall(lostAccountAddress, guardianAccount); + // Submit batch signed by guardian + return await _substrateService.submitExtrinsic(guardianAccount, batchCall); + } + + Future getPullAllFundsFee(String lostAccountAddress, Account guardianAccount) async { + // Batch all calls + final batchCall = _getPullAllFundsCall(lostAccountAddress, guardianAccount); + + // Get transaction fee + final transactionFee = await _substrateService.getFeeForCall(guardianAccount, batchCall); + + // Add recovery deposit + final recoveryDeposit = RecoveryService().recoveryDeposit; + + return ExtrinsicFeeData( + fee: transactionFee.fee + recoveryDeposit, + blockHash: transactionFee.blockHash, + blockNumber: transactionFee.blockNumber, + ); + } + + Utility _getPullAllFundsCall(String lostAccountAddress, Account guardianAccount) { + final calls = []; + + final recoveryService = RecoveryService(); + final balancesService = BalancesService(); + final quantusApi = Schrodinger(_substrateService.provider!); + + // 1. Initiate recovery (rescuer = guardian) + calls.add(recoveryService.getInitiateRecoveryCall(lostAccountAddress)); + + // 2. Vouch for recovery (friend = guardian) + calls.add(recoveryService.getVouchRecoveryCall(lostAccountAddress, guardianAccount.accountId)); + + // 3. Claim recovery (rescuer = guardian) + calls.add(recoveryService.getClaimRecoveryCall(lostAccountAddress)); + + // 4. Transfer all funds to guardian (as recovered) + final transferAllCall = balancesService.getTransferAllCall(guardianAccount.accountId, keepAlive: false); + calls.add(recoveryService.getAsRecoveredCall(lostAccountAddress, transferAllCall)); + + // Batch all calls + final batchCall = quantusApi.tx.utility.batch(calls: calls); + return batchCall; + } } From 52d908f84a732d36c7bc12c83388a8c1916f10cd Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 17:19:40 +0800 Subject: [PATCH 14/22] fix linter errors --- .../components/reversible_transaction_action_sheet.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart index 01a0d885..b498e297 100644 --- a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart +++ b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart @@ -312,7 +312,7 @@ class _ReversibleTransactionActionSheetState extends ConsumerState Date: Mon, 19 Jan 2026 17:20:03 +0800 Subject: [PATCH 15/22] remove send route --- mobile-app/lib/features/main/screens/app.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mobile-app/lib/features/main/screens/app.dart b/mobile-app/lib/features/main/screens/app.dart index 245bd831..408ec839 100644 --- a/mobile-app/lib/features/main/screens/app.dart +++ b/mobile-app/lib/features/main/screens/app.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/features/main/screens/authentication_wrapper.dart'; -import 'package:resonance_network_wallet/features/main/screens/send/send_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_initializer.dart'; import 'package:resonance_network_wallet/features/styles/app_theme.dart'; import 'package:resonance_network_wallet/services/local_notifications_service.dart'; @@ -52,12 +51,6 @@ class _ResonanceWalletAppState extends ConsumerState { initialRoute: '/', routes: { '/': (context) => const WalletInitializer(), - - // The send route is really just an internal thing and not accessible - // to the outside. So no fancy auth logic, it just doesn't work from - // outside the app when not authenticated. - '/send': (context) => const SendScreen(), - // These routes are for deep linking, each will carry an intent '/account': (context) => const WalletInitializer(), '/transactions': (context) => const WalletInitializer(), From 5a2fe31455ea43fbd27c40ee88cdcaff90fc26e7 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 17:20:31 +0800 Subject: [PATCH 16/22] remove send from Q --- mobile-app/lib/features/main/screens/navbar.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mobile-app/lib/features/main/screens/navbar.dart b/mobile-app/lib/features/main/screens/navbar.dart index d4e15007..9f4412cc 100644 --- a/mobile-app/lib/features/main/screens/navbar.dart +++ b/mobile-app/lib/features/main/screens/navbar.dart @@ -169,9 +169,7 @@ class _NavbarState extends ConsumerState { height: context.themeSize.floatingBtnHeight, width: context.themeSize.floatingBtnWidth, child: GestureDetector( - onTap: () { - Navigator.pushNamed(context, '/send'); - }, + onTap: () {}, child: Transform.translate(offset: const Offset(1, -2), child: SvgPicture.asset(item.onIcon)), ), ); From 2b722210b35aed82124dcd9a4c7013cb1ef54d08 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 17:24:30 +0800 Subject: [PATCH 17/22] refactor display account, remove special providers, move into settings --- .../components/transaction_list_item.dart | 2 +- .../main/screens/accounts_screen.dart | 37 ++++++--- .../main/screens/create_account_screen.dart | 2 +- .../main/screens/notifications_screen.dart | 19 +++-- .../features/main/screens/receive_screen.dart | 8 +- .../screens/send/send_progress_overlay.dart | 2 +- .../main/screens/send/send_screen.dart | 2 +- .../main/screens/transactions_screen.dart | 4 +- .../main/screens/wallet_main/wallet_main.dart | 11 +-- .../account_associations_providers.dart | 2 +- .../lib/providers/account_providers.dart | 77 ++----------------- .../active_account_transactions_provider.dart | 32 +------- .../lib/providers/raider_quest_providers.dart | 2 +- .../lib/providers/wallet_providers.dart | 51 +----------- .../global_history_polling_service.dart | 2 +- .../lib/services/history_polling_manager.dart | 4 +- .../notification_integration_service.dart | 6 +- ...eversible_transfer_monitoring_service.dart | 4 +- .../transaction_submission_service.dart | 2 +- mobile-app/pubspec.yaml | 1 + .../test/widget/send_screen_widget_test.dart | 2 +- .../widget/send_screen_widget_test.mocks.dart | 24 +++--- quantus_sdk/lib/quantus_sdk.dart | 1 + .../lib/src/models/display_account.dart | 54 +++++++++++++ .../lib/src/models/entrusted_account.dart | 13 ++++ .../lib/src/services/recovery_service.dart | 52 +++++++------ .../lib/src/services/settings_service.dart | 49 ++++++++---- .../lib/src/services/substrate_service.dart | 2 +- .../test/services/settings_service_test.dart | 12 +-- 29 files changed, 227 insertions(+), 252 deletions(-) create mode 100644 quantus_sdk/lib/src/models/display_account.dart diff --git a/mobile-app/lib/features/components/transaction_list_item.dart b/mobile-app/lib/features/components/transaction_list_item.dart index c866572f..840f2df0 100644 --- a/mobile-app/lib/features/components/transaction_list_item.dart +++ b/mobile-app/lib/features/components/transaction_list_item.dart @@ -236,7 +236,7 @@ class TransactionListItemState extends State { void showTransactionActionSheet(BuildContext context, {required TransactionEvent transaction, required role}) { final container = ProviderScope.containerOf(context, listen: false); - final activeDisplayAccount = container.read(activeDisplayAccountProvider).value; + final activeDisplayAccount = container.read(activeAccountProvider).value; EntrustedAccount? entrustedAccount; if (activeDisplayAccount is EntrustedDisplayAccount) { entrustedAccount = activeDisplayAccount.account; diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 7125f481..d3b311f2 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; @@ -141,6 +142,7 @@ class _AccountsScreenState extends ConsumerState { if (result == true && mounted) { ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); + ref.invalidate(activeAccountProvider); } }); } @@ -203,7 +205,7 @@ class _AccountsScreenState extends ConsumerState { Widget _buildAccountsList() { final accountsAsync = ref.watch(accountsProvider); - final activeAccountAsync = ref.watch(activeAccountProvider); + final activeDisplayAccountAsync = ref.watch(activeAccountProvider); return accountsAsync.when( loading: () => Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), @@ -220,14 +222,25 @@ class _AccountsScreenState extends ConsumerState { ); } - return activeAccountAsync.when( + return activeDisplayAccountAsync.when( loading: () => const Center(child: CircularProgressIndicator(color: Colors.white)), error: (error, _) => Center( child: Text('Failed to load active account: $error', style: const TextStyle(color: Colors.white70)), ), - data: (activeAccount) { + data: (activeDisplayAccount) { if (_selectedWalletIndex == null) { - final initial = activeAccount?.walletIndex ?? accounts.first.walletIndex; + // Try to determine wallet index from active display account + int? initialWalletIndex; + if (activeDisplayAccount is RegularAccount) { + initialWalletIndex = activeDisplayAccount.account.walletIndex; + } else if (activeDisplayAccount is EntrustedDisplayAccount) { + // Find parent account to get wallet index + final parentId = activeDisplayAccount.account.parentAccountId; + final parent = accounts.firstWhereOrNull((a) => a.accountId == parentId); + initialWalletIndex = parent?.walletIndex; + } + + final initial = initialWalletIndex ?? accounts.first.walletIndex; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && _selectedWalletIndex == null) setState(() => _selectedWalletIndex = initial); }); @@ -244,7 +257,7 @@ class _AccountsScreenState extends ConsumerState { separatorBuilder: (context, index) => const SizedBox(height: 25), itemBuilder: (context, index) { final account = walletAccounts[index]; - return _buildAccountListItem(account, activeAccount, index); + return _buildAccountListItem(account, activeDisplayAccount?.account.accountId, index); }, ), ); @@ -275,7 +288,7 @@ class _AccountsScreenState extends ConsumerState { for (var i = 0; i < walletAccounts.length; i++) { if (i > 0) children.add(const SizedBox(height: 25)); final account = walletAccounts[i]; - children.add(_buildAccountListItem(account, activeAccount, i)); + children.add(_buildAccountListItem(account, activeDisplayAccount?.account.accountId, i)); } sectionIndex++; } @@ -290,8 +303,8 @@ class _AccountsScreenState extends ConsumerState { ); } - Widget _buildAccountListItem(Account account, Account? activeAccount, int index) { - final bool isActive = account.accountId == activeAccount?.accountId; + Widget _buildAccountListItem(Account account, String? activeAccountId, int index) { + final bool isActive = account.accountId == activeAccountId; final entrustedAccountsAsync = ref.watch(entrustedAccountsProvider(account)); final entrustedAccountsData = entrustedAccountsAsync.value ?? []; @@ -306,7 +319,7 @@ class _AccountsScreenState extends ConsumerState { return InkWell( onTap: () async { - await ref.read(activeDisplayAccountProvider.notifier).setActiveDisplayAccount(RegularAccount(account)); + await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); if (mounted) Navigator.pop(context); }, child: Stack( @@ -495,7 +508,7 @@ class _AccountsScreenState extends ConsumerState { nodes: entrustedNodes, nodeBuilder: (context, node, depth) { final entrusted = node.data; - final isEntrustedActive = entrusted.accountId == activeAccount?.accountId; + final isEntrustedActive = entrusted.accountId == activeAccountId; return Row( children: [ @@ -515,8 +528,8 @@ class _AccountsScreenState extends ConsumerState { // Set the entrusted account as the active display account await ref - .read(activeDisplayAccountProvider.notifier) - .setActiveDisplayAccount(EntrustedDisplayAccount(entrusted)); + .read(activeAccountProvider.notifier) + .setActiveAccount(EntrustedDisplayAccount(entrusted)); // ignore: use_build_context_synchronously if (mounted) Navigator.pop(context); diff --git a/mobile-app/lib/features/main/screens/create_account_screen.dart b/mobile-app/lib/features/main/screens/create_account_screen.dart index 61ee487c..a41da98d 100644 --- a/mobile-app/lib/features/main/screens/create_account_screen.dart +++ b/mobile-app/lib/features/main/screens/create_account_screen.dart @@ -111,7 +111,7 @@ class _CreateAccountScreenState extends ConsumerState { ref.invalidate(accountsProvider); final activeAccount = ref.read(activeAccountProvider).value; - if (activeAccount?.accountId == _provisionalAccount.accountId) { + if (activeAccount?.account.accountId == _provisionalAccount.accountId) { ref.invalidate(activeAccountProvider); } diff --git a/mobile-app/lib/features/main/screens/notifications_screen.dart b/mobile-app/lib/features/main/screens/notifications_screen.dart index 0c5c486c..07c11d80 100644 --- a/mobile-app/lib/features/main/screens/notifications_screen.dart +++ b/mobile-app/lib/features/main/screens/notifications_screen.dart @@ -61,8 +61,8 @@ class _NotificationsScreenState extends ConsumerState { void _addNotification() { final account = ref.read(activeAccountProvider).value; - final accountName = account?.name ?? 'Unknown'; - final accountId = account?.accountId ?? 'unknown'; + final accountName = account?.account.name ?? 'Unknown'; + final accountId = account?.account.accountId ?? 'unknown'; final notifier = ref.read(notificationProvider.notifier); @@ -120,7 +120,8 @@ class _NotificationsScreenState extends ConsumerState { } void _addTransactionFailed() { - final account = ref.read(activeAccountProvider).value; + final displayAccount = ref.read(activeAccountProvider).value; + final account = displayAccount is RegularAccount ? displayAccount.account : null; final notifier = ref.read(notificationProvider.notifier); @@ -147,21 +148,27 @@ class _NotificationsScreenState extends ConsumerState { } void _addBalanceAlert() { - final account = ref.read(activeAccountProvider).value; + final displayAccount = ref.read(activeAccountProvider).value; + final account = displayAccount is RegularAccount ? displayAccount.account : null; + if (account == null) return; final notifier = ref.read(notificationProvider.notifier); notifier.addBalanceLow(account: account); } void _addAccountSuccess() { - final account = ref.read(activeAccountProvider).value; + final displayAccount = ref.read(activeAccountProvider).value; + final account = displayAccount is RegularAccount ? displayAccount.account : null; + if (account == null) return; final notifier = ref.read(notificationProvider.notifier); notifier.addAccountAdded(account: account); } void _addReversibleReminder() { - final account = ref.read(activeAccountProvider).value; + final displayAccount = ref.read(activeAccountProvider).value; + final account = displayAccount is RegularAccount ? displayAccount.account : null; + if (account == null) return; final notifier = ref.read(notificationProvider.notifier); notifier.addReversibleTransactionReminder( diff --git a/mobile-app/lib/features/main/screens/receive_screen.dart b/mobile-app/lib/features/main/screens/receive_screen.dart index 1bbad6c4..7be156d5 100644 --- a/mobile-app/lib/features/main/screens/receive_screen.dart +++ b/mobile-app/lib/features/main/screens/receive_screen.dart @@ -43,10 +43,10 @@ class _ReceiveSheetState extends State { try { final account = (await _settingsService.getActiveAccount())!; setState(() { - _accountName = account.name; - _accountId = account.accountId; - _checksumFuture = _checksumService.getHumanReadableName(account.accountId); - _splittedAddress = AddressFormattingService.splitIntoChunks(account.accountId); + _accountName = account.account.name; + _accountId = account.account.accountId; + _checksumFuture = _checksumService.getHumanReadableName(account.account.accountId); + _splittedAddress = AddressFormattingService.splitIntoChunks(account.account.accountId); }); } catch (e) { debugPrint('Error loading account data: $e'); diff --git a/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart b/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart index c21fbf6f..73c6f085 100644 --- a/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart +++ b/mobile-app/lib/features/main/screens/send/send_progress_overlay.dart @@ -139,7 +139,7 @@ class SendConfirmationOverlayState extends ConsumerState { Future _loadActiveAccount() async { final settingService = ref.read(settingsServiceProvider); - activeAccount = await settingService.getActiveAccount(); + activeAccount = await settingService.getActiveRegularAccount(); } Future _loadReversibleTimeSetting() async { diff --git a/mobile-app/lib/features/main/screens/transactions_screen.dart b/mobile-app/lib/features/main/screens/transactions_screen.dart index 81849329..855540da 100644 --- a/mobile-app/lib/features/main/screens/transactions_screen.dart +++ b/mobile-app/lib/features/main/screens/transactions_screen.dart @@ -48,8 +48,8 @@ class _TransactionsScreenState extends ConsumerState { if (widget.fixedAccountId != null) { accountIds = [widget.fixedAccountId!]; } else if (!widget.showAccountFilter) { - final activeAccount = ref.read(activeAccountProvider).value; - accountIds = activeAccount != null ? [activeAccount.accountId] : []; + final activeDisplayAccount = ref.read(activeAccountProvider).value; + accountIds = activeDisplayAccount != null ? [activeDisplayAccount.account.accountId] : []; } else { accountIds = accounts.map((a) => a.accountId).toList(); } diff --git a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart index 2c9f9f85..bfec4fdc 100644 --- a/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart +++ b/mobile-app/lib/features/main/screens/wallet_main/wallet_main.dart @@ -10,6 +10,7 @@ import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart import 'package:resonance_network_wallet/features/main/screens/accounts_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/receive_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/notifications_screen.dart'; +import 'package:resonance_network_wallet/features/main/screens/send/send_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_main/account_details.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_main/action_button.dart'; import 'package:resonance_network_wallet/features/main/screens/wallet_main/history_section.dart'; @@ -44,7 +45,7 @@ class _WalletMainState extends ConsumerState { Future _refreshData() async { // Refresh balances with loading indicator - final activeDisplayAccount = ref.read(activeDisplayAccountProvider).value; + final activeDisplayAccount = ref.read(activeAccountProvider).value; if (activeDisplayAccount != null) { ref.invalidate(balanceProviderFamily); // Trigger a loading refresh on the filtered controller @@ -57,7 +58,7 @@ class _WalletMainState extends ConsumerState { ) .loadingRefresh(); } - ref.invalidate(displayBalanceProviderRaw); + ref.invalidate(balanceProviderRaw); // Invalidate combined active display account provider to recompute ref.invalidate(activeDisplayAccountTransactionsProvider); } @@ -78,8 +79,8 @@ class _WalletMainState extends ConsumerState { Widget build(BuildContext context) { _processIntentIfAvailable(); - final activeDisplayAccountAsync = ref.watch(activeDisplayAccountProvider); - final balanceAsync = ref.watch(displayBalanceProvider); + final activeDisplayAccountAsync = ref.watch(activeAccountProvider); + final balanceAsync = ref.watch(balanceProvider); final activeAccountTransactionsAsync = ref.watch(activeDisplayAccountTransactionsProvider); final hasNotifications = ref.watch(notificationProvider).isNotEmpty; @@ -200,7 +201,7 @@ class _WalletMainState extends ConsumerState { ActionButton( type: ActionType.send, onPressed: () { - Navigator.pushNamed(context, '/send'); + Navigator.push(context, MaterialPageRoute(builder: (context) => const SendScreen())); }, ), const SizedBox(width: 33), diff --git a/mobile-app/lib/providers/account_associations_providers.dart b/mobile-app/lib/providers/account_associations_providers.dart index 4acafab8..40ce3768 100644 --- a/mobile-app/lib/providers/account_associations_providers.dart +++ b/mobile-app/lib/providers/account_associations_providers.dart @@ -38,6 +38,6 @@ class AccountAssociationsNotifier extends StateNotifier>( (ref) { final activeAccount = ref.watch(activeAccountProvider).value; - return AccountAssociationsNotifier(activeAccount); + return AccountAssociationsNotifier(activeAccount is RegularAccount ? activeAccount.account : null); }, ); diff --git a/mobile-app/lib/providers/account_providers.dart b/mobile-app/lib/providers/account_providers.dart index 07e9ce59..91aa5910 100644 --- a/mobile-app/lib/providers/account_providers.dart +++ b/mobile-app/lib/providers/account_providers.dart @@ -2,25 +2,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; -// Union type for display accounts -sealed class DisplayAccount { - const DisplayAccount(); - - BaseAccount get account; -} - -class RegularAccount extends DisplayAccount { - @override - final Account account; - const RegularAccount(this.account); -} - -class EntrustedDisplayAccount extends DisplayAccount { - @override - final EntrustedAccount account; - const EntrustedDisplayAccount(this.account); -} - class AccountsNotifier extends StateNotifier>> { final AccountsService _accountsService; @@ -75,7 +56,7 @@ final accountsProvider = StateNotifierProvider> { +class ActiveAccountNotifier extends StateNotifier> { final SettingsService _settingsService; ActiveAccountNotifier(this._settingsService) : super(const AsyncValue.loading()) { @@ -85,15 +66,15 @@ class ActiveAccountNotifier extends StateNotifier> { Future _loadActiveAccount() async { try { final account = await _settingsService.getActiveAccount(); - print('loaded active account: ${account?.index} ${account?.name}'); + print('loaded active account: ${account?.account.name}'); state = AsyncValue.data(account); } catch (e, st) { - print('error loading acctive account: $e $st'); + print('error loading active account: $e $st'); state = AsyncValue.error(e, st); } } - Future setActiveAccount(Account account) async { + Future setActiveAccount(DisplayAccount account) async { try { await _settingsService.setActiveAccount(account); state = AsyncValue.data(account); @@ -107,55 +88,7 @@ class ActiveAccountNotifier extends StateNotifier> { } } -final activeAccountProvider = StateNotifierProvider>((ref) { +final activeAccountProvider = StateNotifierProvider>((ref) { final settingsService = ref.watch(settingsServiceProvider); return ActiveAccountNotifier(settingsService); }); - -class ActiveDisplayAccountNotifier extends StateNotifier> { - final SettingsService _settingsService; - - ActiveDisplayAccountNotifier(this._settingsService) : super(const AsyncValue.loading()) { - _loadActiveDisplayAccount(); - } - - Future _loadActiveDisplayAccount() async { - try { - final account = await _settingsService.getActiveAccount(); - if (account != null) { - state = AsyncValue.data(RegularAccount(account)); - } else { - state = const AsyncValue.data(null); - } - } catch (e, st) { - state = AsyncValue.error(e, st); - } - } - - Future setActiveDisplayAccount(DisplayAccount displayAccount) async { - try { - switch (displayAccount) { - case RegularAccount(account: final account): - await _settingsService.setActiveAccount(account); - state = AsyncValue.data(displayAccount); - case EntrustedDisplayAccount(): - // For entrusted accounts, we don't save them as active account in settings - // They are temporary display accounts - state = AsyncValue.data(displayAccount); - } - } catch (e, st) { - state = AsyncValue.error(e, st); - } - } - - void reset() { - state = const AsyncValue.loading(); - } -} - -final activeDisplayAccountProvider = StateNotifierProvider>(( - ref, -) { - final settingsService = ref.watch(settingsServiceProvider); - return ActiveDisplayAccountNotifier(settingsService); -}); diff --git a/mobile-app/lib/providers/active_account_transactions_provider.dart b/mobile-app/lib/providers/active_account_transactions_provider.dart index 2db6c72f..aa843a4c 100644 --- a/mobile-app/lib/providers/active_account_transactions_provider.dart +++ b/mobile-app/lib/providers/active_account_transactions_provider.dart @@ -24,38 +24,12 @@ final activeAccountTransactionsProvider = Provider const AsyncValue.loading(), error: (err, stack) => AsyncValue.error(err, stack), ); }); -/// Provides a list of transactions for the currently active display account. -/// -/// This provider handles the logic of watching the active display account and fetching -/// the appropriate transaction list. It returns an [AsyncValue] that can be -/// in a loading, data, or error state. -final activeDisplayAccountTransactionsProvider = Provider>((ref) { - final activeDisplayAccountValue = ref.watch(activeDisplayAccountProvider); - - return activeDisplayAccountValue.when( - data: (activeDisplayAccount) { - if (activeDisplayAccount == null) { - return AsyncValue.data( - CombinedTransactionsList( - pendingCancellationIds: {}, - pendingTransactions: [], - reversibleTransfers: [], - otherTransfers: [], - ), - ); - } - return ref.watch( - filteredTransactionsProviderFamily(AccountIdListCache.get([activeDisplayAccount.account.accountId])), - ); - }, - loading: () => const AsyncValue.loading(), - error: (err, stack) => AsyncValue.error(err, stack), - ); -}); +// Alias for backward compatibility or refactoring +final activeDisplayAccountTransactionsProvider = activeAccountTransactionsProvider; diff --git a/mobile-app/lib/providers/raider_quest_providers.dart b/mobile-app/lib/providers/raider_quest_providers.dart index 1bb3a496..27fceb74 100644 --- a/mobile-app/lib/providers/raider_quest_providers.dart +++ b/mobile-app/lib/providers/raider_quest_providers.dart @@ -39,5 +39,5 @@ final raiderSubmissionsProvider = StateNotifierProvider>((ref) { if (activeAccount == null) { return AsyncValue.data(BigInt.zero); } - return ref.watch(balanceProviderFamily(activeAccount.accountId)); + return ref.watch(balanceProviderFamily(activeAccount.account.accountId)); }, loading: () => const AsyncValue.loading(), error: (err, stack) => AsyncValue.error(err, stack), @@ -83,7 +83,7 @@ final balanceProvider = Provider>((ref) { return AsyncValue.data(BigInt.zero); } - final pendingOutgoing = _calculatePendingOutgoing(pendingTransactions, activeAccount.accountId); + final pendingOutgoing = _calculatePendingOutgoing(pendingTransactions, activeAccount.account.accountId); final effectiveBalance = blockchainBalance - pendingOutgoing; final result = effectiveBalance >= BigInt.zero ? effectiveBalance : BigInt.zero; _cachedBalance = result; @@ -97,53 +97,6 @@ final balanceProvider = Provider>((ref) { ); }); -// Display account balance providers -final displayBalanceProviderRaw = Provider>((ref) { - final activeDisplayAccountAsyncValue = ref.watch(activeDisplayAccountProvider); - - return activeDisplayAccountAsyncValue.when( - data: (activeDisplayAccount) { - if (activeDisplayAccount == null) { - return AsyncValue.data(BigInt.zero); - } - return ref.watch(balanceProviderFamily(activeDisplayAccount.account.accountId)); - }, - loading: () => const AsyncValue.loading(), - error: (err, stack) => AsyncValue.error(err, stack), - ); -}); - -// Store for cached display balance to return on error -BigInt _cachedDisplayBalance = BigInt.zero; - -// Effective display balance (blockchain balance minus pending outgoing transactions) -final displayBalanceProvider = Provider>((ref) { - final balanceAsync = ref.watch(displayBalanceProviderRaw); - final pendingTransactions = ref.watch(pendingTransactionsProvider); - final activeDisplayAccountAsync = ref.watch(activeDisplayAccountProvider); - - return balanceAsync.when( - data: (blockchainBalance) { - final activeDisplayAccount = activeDisplayAccountAsync.value; - if (activeDisplayAccount == null) { - _cachedDisplayBalance = BigInt.zero; - return AsyncValue.data(BigInt.zero); - } - - final pendingOutgoing = _calculatePendingOutgoing(pendingTransactions, activeDisplayAccount.account.accountId); - final effectiveBalance = blockchainBalance - pendingOutgoing; - final result = effectiveBalance >= BigInt.zero ? effectiveBalance : BigInt.zero; - _cachedDisplayBalance = result; - return AsyncValue.data(result); - }, - loading: () => const AsyncValue.loading(), - error: (err, stack) { - // On error, return last cached balance - return AsyncValue.data(_cachedDisplayBalance); - }, - ); -}); - /// Calculates the total amount of pending outgoing transactions for a /// specific account BigInt _calculatePendingOutgoing(List pendingTransactions, String accountId) { diff --git a/mobile-app/lib/services/global_history_polling_service.dart b/mobile-app/lib/services/global_history_polling_service.dart index d6b5a149..a8e9a7a0 100644 --- a/mobile-app/lib/services/global_history_polling_service.dart +++ b/mobile-app/lib/services/global_history_polling_service.dart @@ -126,7 +126,7 @@ class GlobalHistoryPollingService { final active = _ref.read(activeAccountProvider).value; if (active != null) { await _ref - .read(filteredPaginationControllerProviderFamily(AccountIdListCache.get([active.accountId])).notifier) + .read(filteredPaginationControllerProviderFamily(AccountIdListCache.get([active.account.accountId])).notifier) .loadingRefresh(); } diff --git a/mobile-app/lib/services/history_polling_manager.dart b/mobile-app/lib/services/history_polling_manager.dart index 2f4fdac7..6c1670f3 100644 --- a/mobile-app/lib/services/history_polling_manager.dart +++ b/mobile-app/lib/services/history_polling_manager.dart @@ -84,11 +84,11 @@ class HistoryPollingManager { void _refreshBalance({required bool showLoading}) { if (showLoading) { // For manual refresh - invalidate balance providers to show loading - final activeDisplayAccount = _ref.read(activeDisplayAccountProvider).value; + final activeDisplayAccount = _ref.read(activeAccountProvider).value; if (activeDisplayAccount != null) { _ref.invalidate(balanceProviderFamily); } - _ref.invalidate(displayBalanceProviderRaw); // Invalidate raw balance for loading state + _ref.invalidate(balanceProviderRaw); // Invalidate raw balance for loading state // displayBalanceProvider (effective) will auto-update when raw balance changes } else { // For silent refresh - just invalidate family to refresh data silently diff --git a/mobile-app/lib/services/notification_integration_service.dart b/mobile-app/lib/services/notification_integration_service.dart index d160da87..760e1452 100644 --- a/mobile-app/lib/services/notification_integration_service.dart +++ b/mobile-app/lib/services/notification_integration_service.dart @@ -61,15 +61,15 @@ class NotificationIntegrationService { void _setupBalanceListeners() { // Listen to balance changes for low balance alerts - _ref.listen>(displayBalanceProvider, (previous, next) { + _ref.listen>(balanceProvider, (previous, next) { next.whenData((balance) { // Check if balance is at or near existential deposit final existentialDeposit = balances.Constants().existentialDeposit; if (balance <= existentialDeposit) { // Example threshold final activeAccount = _ref.read(activeAccountProvider).value; - if (activeAccount != null) { - _notifyLowBalance(activeAccount, activeAccount.accountId); + if (activeAccount is RegularAccount) { + _notifyLowBalance(activeAccount.account, activeAccount.account.accountId); } } }); diff --git a/mobile-app/lib/services/reversible_transfer_monitoring_service.dart b/mobile-app/lib/services/reversible_transfer_monitoring_service.dart index dce60fe9..42bbf5b9 100644 --- a/mobile-app/lib/services/reversible_transfer_monitoring_service.dart +++ b/mobile-app/lib/services/reversible_transfer_monitoring_service.dart @@ -232,7 +232,9 @@ class ReversibleTransferMonitoringService { final active = _ref.read(activeAccountProvider).value; if (active != null) { await _ref - .read(filteredPaginationControllerProviderFamily(AccountIdListCache.get([active.accountId])).notifier) + .read( + filteredPaginationControllerProviderFamily(AccountIdListCache.get([active.account.accountId])).notifier, + ) .silentRefresh(); } } diff --git a/mobile-app/lib/services/transaction_submission_service.dart b/mobile-app/lib/services/transaction_submission_service.dart index 4b17ce56..53ac9733 100644 --- a/mobile-app/lib/services/transaction_submission_service.dart +++ b/mobile-app/lib/services/transaction_submission_service.dart @@ -338,7 +338,7 @@ class TransactionSubmissionService { final targets = {...?(affectedAccountIds)}; final active = _ref.read(activeAccountProvider).value; if (active != null) { - targets.add(active.accountId); + targets.add(active.account.accountId); } for (final accountId in targets) { diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 4b58f1b4..79bf9904 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: flutter_local_notifications: ^19.5.0 timezone: ^0.10.1 flutter_timezone: ^5.0.1 + collection: ^1.19.1 dev_dependencies: flutter_test: diff --git a/mobile-app/test/widget/send_screen_widget_test.dart b/mobile-app/test/widget/send_screen_widget_test.dart index 27af0645..9a6037d4 100644 --- a/mobile-app/test/widget/send_screen_widget_test.dart +++ b/mobile-app/test/widget/send_screen_widget_test.dart @@ -42,7 +42,7 @@ void main() { // --- 1. Settings Service Stubs --- when(mockSettingsService.getActiveAccount()).thenAnswer((_) async { - return const Account(walletIndex: 0, index: 0, name: 'Test User', accountId: 'test_account_id'); + return const RegularAccount(Account(walletIndex: 0, index: 0, name: 'Test User', accountId: 'test_account_id')); }); when(mockSettingsService.getReversibleTimeSeconds()).thenAnswer((_) async => 600); diff --git a/mobile-app/test/widget/send_screen_widget_test.mocks.dart b/mobile-app/test/widget/send_screen_widget_test.mocks.dart index b426ac78..961db841 100644 --- a/mobile-app/test/widget/send_screen_widget_test.mocks.dart +++ b/mobile-app/test/widget/send_screen_widget_test.mocks.dart @@ -138,19 +138,19 @@ class MockSettingsService extends _i1.Mock implements _i2.SettingsService { ) as _i3.Future); - @override - _i3.Future setActiveAccount(_i4.Account? account) => - (super.noSuchMethod( - Invocation.method(#setActiveAccount, [account]), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) - as _i3.Future); + // @override + // _i3.Future setActiveAccount(_i4.Account? account) => + // (super.noSuchMethod( + // Invocation.method(#setActiveAccount, [account]), + // returnValue: _i3.Future.value(), + // returnValueForMissingStub: _i3.Future.value(), + // ) + // as _i3.Future); - @override - _i3.Future<_i4.Account?> getActiveAccount() => - (super.noSuchMethod(Invocation.method(#getActiveAccount, []), returnValue: _i3.Future<_i4.Account?>.value()) - as _i3.Future<_i4.Account?>); + // @override + // _i3.Future<_i4.Account?> getActiveAccount() => + // (super.noSuchMethod(Invocation.method(#getActiveAccount, []), returnValue: _i3.Future<_i4.Account?>.value()) + // as _i3.Future<_i4.Account?>); @override _i3.Future<_i4.Account?> getAccount({required int? walletIndex, required int? index}) => diff --git a/quantus_sdk/lib/quantus_sdk.dart b/quantus_sdk/lib/quantus_sdk.dart index 9eb9417e..b417ce73 100644 --- a/quantus_sdk/lib/quantus_sdk.dart +++ b/quantus_sdk/lib/quantus_sdk.dart @@ -60,6 +60,7 @@ export 'src/extensions/account_extension.dart'; export 'src/quantus_signing_payload.dart'; export 'src/quantus_payload_parser.dart'; export 'src/models/entrusted_account.dart'; +export 'src/models/display_account.dart'; class QuantusSdk { /// Initialise the SDK (loads Rust FFI, etc). diff --git a/quantus_sdk/lib/src/models/display_account.dart b/quantus_sdk/lib/src/models/display_account.dart new file mode 100644 index 00000000..8b84dafd --- /dev/null +++ b/quantus_sdk/lib/src/models/display_account.dart @@ -0,0 +1,54 @@ +import 'package:quantus_sdk/src/models/account.dart'; +import 'package:quantus_sdk/src/models/base_account.dart'; +import 'package:quantus_sdk/src/models/entrusted_account.dart'; + +// Union type for display accounts +sealed class DisplayAccount { + const DisplayAccount(); + + BaseAccount get account; + + Map toJson(); + + static DisplayAccount fromJson(Map json) { + final type = json['type'] as String; + switch (type) { + case 'regular': + return RegularAccount.fromJson(json); + case 'entrusted': + return EntrustedDisplayAccount.fromJson(json); + default: + throw Exception('Unknown display account type: $type'); + } + } +} + +class RegularAccount extends DisplayAccount { + @override + final Account account; + const RegularAccount(this.account); + + factory RegularAccount.fromJson(Map json) { + return RegularAccount(Account.fromJson(json['account'] as Map)); + } + + @override + Map toJson() { + return {'type': 'regular', 'account': account.toJson()}; + } +} + +class EntrustedDisplayAccount extends DisplayAccount { + @override + final EntrustedAccount account; + const EntrustedDisplayAccount(this.account); + + factory EntrustedDisplayAccount.fromJson(Map json) { + return EntrustedDisplayAccount(EntrustedAccount.fromJson(json['account'] as Map)); + } + + @override + Map toJson() { + return {'type': 'entrusted', 'account': account.toJson()}; + } +} diff --git a/quantus_sdk/lib/src/models/entrusted_account.dart b/quantus_sdk/lib/src/models/entrusted_account.dart index da905e1a..159df8ea 100644 --- a/quantus_sdk/lib/src/models/entrusted_account.dart +++ b/quantus_sdk/lib/src/models/entrusted_account.dart @@ -15,4 +15,17 @@ class EntrustedAccount implements BaseAccount { required this.name, required this.accountId, }); + + factory EntrustedAccount.fromJson(Map json) { + return EntrustedAccount( + parentAccountId: json['parentAccountId'] as String, + index: json['index'] as int, + name: json['name'] as String, + accountId: json['accountId'] as String, + ); + } + + Map toJson() { + return {'parentAccountId': parentAccountId, 'index': index, 'name': name, 'accountId': accountId}; + } } diff --git a/quantus_sdk/lib/src/services/recovery_service.dart b/quantus_sdk/lib/src/services/recovery_service.dart index fd3a26d9..3d366245 100644 --- a/quantus_sdk/lib/src/services/recovery_service.dart +++ b/quantus_sdk/lib/src/services/recovery_service.dart @@ -51,11 +51,7 @@ class RecoveryService { /// Initiate recovery process for a lost account Future initiateRecovery({required Account rescuerAccount, required String lostAccountAddress}) async { try { - final quantusApi = Schrodinger(_substrateService.provider!); - final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); - - // Create the call - final call = quantusApi.tx.recovery.initiateRecovery(account: lostAccount); + final call = getInitiateRecoveryCall(lostAccountAddress); // Submit the transaction using substrate service return await _substrateService.submitExtrinsic(rescuerAccount, call); @@ -64,6 +60,12 @@ class RecoveryService { } } + RuntimeCall getInitiateRecoveryCall(String lostAccountAddress) { + final quantusApi = Schrodinger(_substrateService.provider!); + final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); + return quantusApi.tx.recovery.initiateRecovery(account: lostAccount); + } + /// Vouch for an active recovery process (called by friends) Future vouchForRecovery({ required Account friendAccount, @@ -71,12 +73,7 @@ class RecoveryService { required String rescuerAddress, }) async { try { - final quantusApi = Schrodinger(_substrateService.provider!); - final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); - final rescuer = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: rescuerAddress)); - - // Create the call - final call = quantusApi.tx.recovery.vouchRecovery(lost: lostAccount, rescuer: rescuer); + final call = getVouchRecoveryCall(lostAccountAddress, rescuerAddress); // Submit the transaction using substrate service return await _substrateService.submitExtrinsic(friendAccount, call); @@ -85,14 +82,17 @@ class RecoveryService { } } + RuntimeCall getVouchRecoveryCall(String lostAccountAddress, String rescuerAddress) { + final quantusApi = Schrodinger(_substrateService.provider!); + final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); + final rescuer = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: rescuerAddress)); + return quantusApi.tx.recovery.vouchRecovery(lost: lostAccount, rescuer: rescuer); + } + /// Claim recovery of a lost account (called by rescuer after threshold is met) Future claimRecovery({required Account rescuerAccount, required String lostAccountAddress}) async { try { - final quantusApi = Schrodinger(_substrateService.provider!); - final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); - - // Create the call - final call = quantusApi.tx.recovery.claimRecovery(account: lostAccount); + final call = getClaimRecoveryCall(lostAccountAddress); // Submit the transaction using substrate service return await _substrateService.submitExtrinsic(rescuerAccount, call); @@ -101,6 +101,12 @@ class RecoveryService { } } + RuntimeCall getClaimRecoveryCall(String lostAccountAddress) { + final quantusApi = Schrodinger(_substrateService.provider!); + final lostAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: lostAccountAddress)); + return quantusApi.tx.recovery.claimRecovery(account: lostAccount); + } + /// Close an active recovery process (called by the lost account owner) Future closeRecovery({required Account lostAccount, required String rescuerAddress}) async { try { @@ -139,13 +145,7 @@ class RecoveryService { required RuntimeCall call, }) async { try { - final quantusApi = Schrodinger(_substrateService.provider!); - final recoveredAccount = const multi_address.$MultiAddress().id( - crypto.ss58ToAccountId(s: recoveredAccountAddress), - ); - - // Create the call - final proxyCall = quantusApi.tx.recovery.asRecovered(account: recoveredAccount, call: call); + final proxyCall = getAsRecoveredCall(recoveredAccountAddress, call); // Submit the transaction using substrate service return await _substrateService.submitExtrinsic(rescuerAccount, proxyCall); @@ -154,6 +154,12 @@ class RecoveryService { } } + RuntimeCall getAsRecoveredCall(String recoveredAccountAddress, RuntimeCall call) { + final quantusApi = Schrodinger(_substrateService.provider!); + final recoveredAccount = const multi_address.$MultiAddress().id(crypto.ss58ToAccountId(s: recoveredAccountAddress)); + return quantusApi.tx.recovery.asRecovered(account: recoveredAccount, call: call); + } + /// Cancel the ability to use a recovered account Future cancelRecovered({required Account rescuerAccount, required String recoveredAccountAddress}) async { try { diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index e7b5d74f..8fa58ce4 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:quantus_sdk/src/models/account.dart'; +import 'package:quantus_sdk/src/models/display_account.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SettingsService { @@ -22,6 +23,7 @@ class SettingsService { static const String _oldAccountsKeyV1 = 'accounts'; static const String _activeAccountIndexKey = 'active_account_index'; static const String _activeAccountIdKey = 'active_account_id'; + static const String _activeDisplayAccountKey = 'active_display_account'; // Local authentication keys static const String _isLocalAuthEnabledKey = 'is_local_auth_enabled'; @@ -59,7 +61,7 @@ class SettingsService { final oldWalletName = _prefs.getString('wallet_name') ?? 'Account 1'; final account = Account(walletIndex: 0, index: 0, name: oldWalletName, accountId: oldAccountId); await saveAccounts([account]); - await setActiveAccount(account); + await setActiveAccount(RegularAccount(account)); // Clean up old keys after migration await _prefs.remove('account_id'); await _prefs.remove('wallet_name'); @@ -105,7 +107,7 @@ class SettingsService { await saveAccounts(accounts); if (accounts.length == 1) { // make sure that active account is always a valid account - await setActiveAccount(account); + await setActiveAccount(RegularAccount(account)); } } else { throw Exception('Account already exists'); @@ -127,19 +129,41 @@ class SettingsService { throw Exception('Cant remove last account!'); } if (account.accountId == await _getActiveAccountId()) { - await _setActiveAccountId(accounts[0].accountId); + await setActiveAccount(RegularAccount(accounts[0])); } accounts.removeWhere((a) => a.accountId == account.accountId); await saveAccounts(accounts); } - Future setActiveAccount(Account account) async { - final exists = (await getAccounts()).any((a) => a.accountId == account.accountId); - if (exists) { - await _setActiveAccountId(account.accountId); - } else { - throw Exception('Account index does not exist'); + Future setActiveAccount(DisplayAccount account) async { + await _prefs.setString(_activeDisplayAccountKey, jsonEncode(account.toJson())); + if (account is RegularAccount) { + final exists = (await getAccounts()).any((a) => a.accountId == account.account.accountId); + if (exists) { + await _setActiveAccountId(account.account.accountId); + } else { + throw Exception('Account index does not exist'); + } + } + } + + Future getActiveAccount() async { + final jsonStr = _prefs.getString(_activeDisplayAccountKey); + if (jsonStr != null) { + return DisplayAccount.fromJson(jsonDecode(jsonStr)); + } + final activeAccountId = await _getActiveAccountId(); + final accounts = await getAccounts(); + final ix = accounts.indexWhere((a) => a.accountId == activeAccountId); + return ix != -1 ? RegularAccount(accounts[ix]) : (accounts.isNotEmpty ? RegularAccount(accounts.first) : null); + } + + Future getActiveRegularAccount() async { + final activeAccount = await getActiveAccount(); + if (activeAccount is RegularAccount) { + return activeAccount.account; } + return null; } Future _getActiveAccountId() async { @@ -169,13 +193,6 @@ class SettingsService { } } - Future getActiveAccount() async { - final activeAccountId = await _getActiveAccountId(); - final accounts = await getAccounts(); - final ix = accounts.indexWhere((a) => a.accountId == activeAccountId); - return ix != -1 ? accounts[ix] : (accounts.isNotEmpty ? accounts.first : null); - } - Future getAccount({required int walletIndex, required int index}) async { final accounts = await getAccounts(); final ix = accounts.indexWhere((a) => a.walletIndex == walletIndex && a.index == index); diff --git a/quantus_sdk/lib/src/services/substrate_service.dart b/quantus_sdk/lib/src/services/substrate_service.dart index a37ec465..2899674b 100644 --- a/quantus_sdk/lib/src/services/substrate_service.dart +++ b/quantus_sdk/lib/src/services/substrate_service.dart @@ -53,7 +53,7 @@ class SubstrateService { } Future _getUserWallet() async { - final account = (await SettingsService().getActiveAccount())!; + final account = (await SettingsService().getActiveRegularAccount())!; final keypair = await account.getKeypair(); return keypair; } diff --git a/quantus_sdk/test/services/settings_service_test.dart b/quantus_sdk/test/services/settings_service_test.dart index 842127c6..646bcc3a 100644 --- a/quantus_sdk/test/services/settings_service_test.dart +++ b/quantus_sdk/test/services/settings_service_test.dart @@ -38,7 +38,7 @@ void main() { // Act final accounts = await migrationService.getAccounts(); - final activeAccount = await migrationService.getActiveAccount(); + final activeAccount = await migrationService.getActiveRegularAccount(); // Assert expect(accounts.length, 1); @@ -108,11 +108,11 @@ void main() { await settingsService.saveAccounts([account1, account2]); // Act - await settingsService.setActiveAccount(account2); + await settingsService.setActiveAccount(const RegularAccount(account2)); final activeAccount = (await settingsService.getActiveAccount())!; // Assert - expect(activeAccount.accountId, account2.accountId); + expect(activeAccount.account.accountId, account2.accountId); }); test('setActiveAccount should throw for a non-existent account', () async { @@ -122,7 +122,7 @@ void main() { // Act & Assert expect( - () async => await settingsService.setActiveAccount(account2), + () async => await settingsService.setActiveAccount(const RegularAccount(account2)), throwsA(isA().having((e) => e.toString(), 'message', contains('Account index does not exist'))), ); }); @@ -157,7 +157,7 @@ void main() { // Arrange await settingsService.initialize(); await settingsService.saveAccounts([account1, account2, account3]); - await settingsService.setActiveAccount(account2); + await settingsService.setActiveAccount(const RegularAccount(account2)); // Act await settingsService.removeAccount(account2); @@ -165,7 +165,7 @@ void main() { // Assert // It should fall back to the first account in the remaining list - expect(activeAccount.accountId, account1.accountId); + expect(activeAccount.account.accountId, account1.accountId); }); test('getNextFreeAccountIndex should return correct next index', () async { From 969a7a6e076127d3b3cfa05c7ac2c39bc3b45278 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Mon, 19 Jan 2026 17:44:53 +0800 Subject: [PATCH 18/22] fix intercept button --- .../components/reversible_transaction_action_sheet.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart index b498e297..f16c0e44 100644 --- a/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart +++ b/mobile-app/lib/features/components/reversible_transaction_action_sheet.dart @@ -510,7 +510,7 @@ class _ReversibleTransactionActionSheetState extends ConsumerState Date: Mon, 19 Jan 2026 21:16:59 +0800 Subject: [PATCH 19/22] fix tree view scroll bug --- .../lib/features/components/tree_list.dart | 15 +- .../main/screens/accounts_screen.dart | 164 +++++++++--------- 2 files changed, 88 insertions(+), 91 deletions(-) diff --git a/mobile-app/lib/features/components/tree_list.dart b/mobile-app/lib/features/components/tree_list.dart index 535111fe..9bf5774f 100644 --- a/mobile-app/lib/features/components/tree_list.dart +++ b/mobile-app/lib/features/components/tree_list.dart @@ -43,12 +43,17 @@ class _TreeListViewState extends State> { @override Widget build(BuildContext context) { - return ListView( - padding: widget.padding, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - children: _buildTreeNodes(widget.nodes, 0, []), + final children = _buildTreeNodes(widget.nodes, 0, []); + final content = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, ); + + if (widget.padding != null) { + return Padding(padding: widget.padding!, child: content); + } + return content; } List _buildTreeNodes(List> nodes, int depth, List parentLines) { diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index d3b311f2..ad3a66b0 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -25,7 +25,6 @@ import 'package:resonance_network_wallet/providers/entrusted_account_provider.da import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; -import 'dart:math'; enum _WalletMoreAction { createWallet, importWallet, addHardwareWallet } @@ -312,8 +311,6 @@ class _AccountsScreenState extends ConsumerState { .map((entrusted) => TreeNode(data: entrusted)) .toList(); - final double constraintMaxHeight = min(entrustedNodes.length * 52, 104); - final isHighSecurityAsync = ref.watch(isHighSecurityProvider(account)); final isHighSecurity = isHighSecurityAsync.value ?? false; @@ -501,95 +498,90 @@ class _AccountsScreenState extends ConsumerState { ], ), if (entrustedNodes.isNotEmpty) - ConstrainedBox( - constraints: BoxConstraints(maxHeight: constraintMaxHeight), - child: TreeListView( - showExpandCollapse: false, - nodes: entrustedNodes, - nodeBuilder: (context, node, depth) { - final entrusted = node.data; - final isEntrustedActive = entrusted.accountId == activeAccountId; - - return Row( - children: [ - Expanded( - child: Material( - color: isEntrustedActive - ? context.themeColors.surfaceActive - : context.themeColors.darkGray, - shape: RoundedRectangleBorder( - side: const BorderSide(color: Color(0x26FFFFFF)), - borderRadius: BorderRadius.circular(5), - ), - child: InkWell( - borderRadius: BorderRadius.circular(5), - onTap: () async { - print('onTap: ${entrusted.accountId}'); - - // Set the entrusted account as the active display account - await ref - .read(activeAccountProvider.notifier) - .setActiveAccount(EntrustedDisplayAccount(entrusted)); - - // ignore: use_build_context_synchronously - if (mounted) Navigator.pop(context); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - entrusted.name, - style: context.themeText.smallParagraph?.copyWith( - color: isEntrustedActive ? Colors.black : Colors.white, - ), + TreeListView( + showExpandCollapse: false, + nodes: entrustedNodes, + nodeBuilder: (context, node, depth) { + final entrusted = node.data; + final isEntrustedActive = entrusted.accountId == activeAccountId; + + return Row( + children: [ + Expanded( + child: Material( + color: isEntrustedActive ? context.themeColors.surfaceActive : context.themeColors.darkGray, + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x26FFFFFF)), + borderRadius: BorderRadius.circular(5), + ), + child: InkWell( + borderRadius: BorderRadius.circular(5), + onTap: () async { + print('onTap: ${entrusted.accountId}'); + + // Set the entrusted account as the active display account + await ref + .read(activeAccountProvider.notifier) + .setActiveAccount(EntrustedDisplayAccount(entrusted)); + + // ignore: use_build_context_synchronously + if (mounted) Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + entrusted.name, + style: context.themeText.smallParagraph?.copyWith( + color: isEntrustedActive ? Colors.black : Colors.white, ), - AccountTag(text: 'Entrusted', color: context.themeColors.accountTagEntrusted), - ], - ), + ), + AccountTag(text: 'Entrusted', color: context.themeColors.accountTagEntrusted), + ], ), ), ), ), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: SvgPicture.asset( - 'assets/settings_icon_off.svg', - width: context.isTablet ? 28 : 21, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - onPressed: () async { - // Get current data from providers - final balanceAsync = ref.read(balanceProviderFamily(entrusted.accountId)); - final checksumName = await _checksumService.getHumanReadableName(entrusted.accountId); - - balanceAsync.when( - loading: () {}, - error: (error, _) {}, - data: (balance) async { - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), - builder: (context) => AccountSettingsScreen( - account: entrusted, - balance: _formattingService.formatBalance(balance, addSymbol: true), - checksumName: checksumName, - isHighSecurity: true, - ), - ), - ); - }, - ); - }, + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: SvgPicture.asset( + 'assets/settings_icon_off.svg', + width: context.isTablet ? 28 : 21, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - ], - ); - }, - ), + onPressed: () async { + // Get current data from providers + final balanceAsync = ref.read(balanceProviderFamily(entrusted.accountId)); + final checksumName = await _checksumService.getHumanReadableName(entrusted.accountId); + + balanceAsync.when( + loading: () {}, + error: (error, _) {}, + data: (balance) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), + builder: (context) => AccountSettingsScreen( + account: entrusted, + balance: _formattingService.formatBalance(balance, addSymbol: true), + checksumName: checksumName, + isHighSecurity: true, + ), + ), + ); + }, + ); + }, + ), + ], + ); + }, ), ], ), From d0329bb567768452a29fc0068c46d31519aba462 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 20 Jan 2026 13:33:00 +0800 Subject: [PATCH 20/22] fix tree view again --- .../main/screens/accounts_screen.dart | 166 +++++++++--------- 1 file changed, 79 insertions(+), 87 deletions(-) diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 6e46aba0..333179d8 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -25,7 +25,6 @@ import 'package:resonance_network_wallet/providers/entrusted_account_provider.da import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; -import 'dart:math'; enum _WalletMoreAction { createWallet, importWallet, addHardwareWallet } @@ -312,8 +311,6 @@ class _AccountsScreenState extends ConsumerState { .map((entrusted) => TreeNode(data: entrusted)) .toList(); - final double constraintMaxHeight = min(entrustedNodes.length * 52, 104); - final isHighSecurityAsync = ref.watch(isHighSecurityProvider(account)); final isHighSecurity = isHighSecurityAsync.value ?? false; @@ -502,96 +499,91 @@ class _AccountsScreenState extends ConsumerState { ], ), if (entrustedNodes.isNotEmpty) - ConstrainedBox( - constraints: BoxConstraints(maxHeight: constraintMaxHeight), - child: TreeListView( - showExpandCollapse: false, - nodes: entrustedNodes, - nodeBuilder: (context, node, depth) { - final entrusted = node.data; - final isEntrustedActive = entrusted.accountId == activeAccountId; - - return Row( - children: [ - Expanded( - child: Material( - color: isEntrustedActive - ? context.themeColors.surfaceActive - : context.themeColors.darkGray, - shape: RoundedRectangleBorder( - side: const BorderSide(color: Color(0x26FFFFFF)), - borderRadius: BorderRadius.circular(5), - ), - child: InkWell( - borderRadius: BorderRadius.circular(5), - onTap: () async { - print('onTap: ${entrusted.accountId}'); - - // Set the entrusted account as the active display account - await ref - .read(activeAccountProvider.notifier) - .setActiveAccount(EntrustedDisplayAccount(entrusted)); - - // ignore: use_build_context_synchronously - if (mounted) Navigator.pop(context); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - entrusted.name, - style: context.themeText.smallParagraph?.copyWith( - color: isEntrustedActive ? Colors.black : Colors.white, - ), + TreeListView( + showExpandCollapse: false, + nodes: entrustedNodes, + nodeBuilder: (context, node, depth) { + final entrusted = node.data; + final isEntrustedActive = entrusted.accountId == activeAccountId; + + return Row( + children: [ + Expanded( + child: Material( + color: isEntrustedActive ? context.themeColors.surfaceActive : context.themeColors.darkGray, + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x26FFFFFF)), + borderRadius: BorderRadius.circular(5), + ), + child: InkWell( + borderRadius: BorderRadius.circular(5), + onTap: () async { + print('onTap: ${entrusted.accountId}'); + + // Set the entrusted account as the active display account + await ref + .read(activeAccountProvider.notifier) + .setActiveAccount(EntrustedDisplayAccount(entrusted)); + + // ignore: use_build_context_synchronously + if (mounted) Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + entrusted.name, + style: context.themeText.smallParagraph?.copyWith( + color: isEntrustedActive ? Colors.black : Colors.white, ), - AccountTag(text: 'Entrusted', color: context.themeColors.accountTagEntrusted), - ], - ), + ), + AccountTag(text: 'Entrusted', color: context.themeColors.accountTagEntrusted), + ], ), ), ), ), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: SvgPicture.asset( - 'assets/settings_icon_off.svg', - width: context.isTablet ? 28 : 21, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - onPressed: () async { - // Get current data from providers - final balanceAsync = ref.read(balanceProviderFamily(entrusted.accountId)); - final checksumName = await _checksumService.getHumanReadableName(entrusted.accountId); - - balanceAsync.when( - loading: () {}, - error: (error, _) {}, - data: (balance) async { - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), - builder: (context) => AccountSettingsScreen( - account: entrusted, - balance: _formattingService.formatBalance(balance, addSymbol: true), - checksumName: checksumName, - isHighSecurity: false, - isEntrustedAccount: true, - ), - ), - ); - }, - ); - }, + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: SvgPicture.asset( + 'assets/settings_icon_off.svg', + width: context.isTablet ? 28 : 21, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), ), - ], - ); - }, - ), + onPressed: () async { + // Get current data from providers + final balanceAsync = ref.read(balanceProviderFamily(entrusted.accountId)); + final checksumName = await _checksumService.getHumanReadableName(entrusted.accountId); + + balanceAsync.when( + loading: () {}, + error: (error, _) {}, + data: (balance) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), + builder: (context) => AccountSettingsScreen( + account: entrusted, + balance: _formattingService.formatBalance(balance, addSymbol: true), + checksumName: checksumName, + isHighSecurity: false, + isEntrustedAccount: true, + ), + ), + ); + }, + ); + }, + ), + ], + ); + }, ), ], ), From 2ac82902045e023aebf2142a59b81c226c4581eb Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 20 Jan 2026 13:37:19 +0800 Subject: [PATCH 21/22] Delete entrusted_pull_all.png --- mobile-app/assets/entrusted_pull_all.png | Bin 24891 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 mobile-app/assets/entrusted_pull_all.png diff --git a/mobile-app/assets/entrusted_pull_all.png b/mobile-app/assets/entrusted_pull_all.png deleted file mode 100644 index 4c417fc485433b33aa80431b929f6f978b61f248..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24891 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 IA#eWXe`0fmXaE2J From 5e42a14afabf844795e739cbafea58b85c820b07 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 20 Jan 2026 13:38:29 +0800 Subject: [PATCH 22/22] remove useless code --- .../lib/providers/active_account_transactions_provider.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/mobile-app/lib/providers/active_account_transactions_provider.dart b/mobile-app/lib/providers/active_account_transactions_provider.dart index aa843a4c..8c1f40ba 100644 --- a/mobile-app/lib/providers/active_account_transactions_provider.dart +++ b/mobile-app/lib/providers/active_account_transactions_provider.dart @@ -30,6 +30,3 @@ final activeAccountTransactionsProvider = Provider AsyncValue.error(err, stack), ); }); - -// Alias for backward compatibility or refactoring -final activeDisplayAccountTransactionsProvider = activeAccountTransactionsProvider;