From d1de8d566c080db689c30aed5ea21375be014c91 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 18:11:36 +0800 Subject: [PATCH 01/20] new accounts list --- .../components/account_gradient_image.dart | 6 +- .../main/screens/accounts_screen.dart | 1029 +++++++++-------- .../features/main/screens/receive_screen.dart | 2 +- .../main/screens/settings_screen.dart | 2 +- .../main/screens/wallet_main/wallet_main.dart | 2 +- .../lib/v2/screens/home/home_screen.dart | 2 +- .../lib/src/services/accounts_service.dart | 24 +- 7 files changed, 549 insertions(+), 518 deletions(-) diff --git a/mobile-app/lib/features/components/account_gradient_image.dart b/mobile-app/lib/features/components/account_gradient_image.dart index dc8f4726..6bbc6e4c 100644 --- a/mobile-app/lib/features/components/account_gradient_image.dart +++ b/mobile-app/lib/features/components/account_gradient_image.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:resonance_network_wallet/utils/color_generator_engine.dart'; class AccountGradientImage extends StatelessWidget { - final dynamic accountId; - final dynamic width; - final dynamic height; + final String accountId; + final double width; + final double height; const AccountGradientImage({super.key, required this.accountId, required this.width, required this.height}); diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 333179d8..6d34ea94 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -1,32 +1,24 @@ -import 'package:flutter/material.dart'; +import 'dart:math' as math; + import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; -import 'package:resonance_network_wallet/features/components/account_tag.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; -import 'package:resonance_network_wallet/features/components/select.dart'; -import 'package:resonance_network_wallet/features/components/select_action_sheet.dart'; -import 'package:resonance_network_wallet/features/components/sphere.dart'; -import 'package:resonance_network_wallet/features/components/tree_list.dart'; -import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; -import 'package:resonance_network_wallet/features/main/screens/account_settings_screen.dart'; +import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/create_account_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/create_wallet_and_backup_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/import_wallet_screen.dart'; +import 'package:resonance_network_wallet/features/main/screens/receive_screen.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/entrusted_account_provider.dart'; 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 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; -enum _WalletMoreAction { createWallet, importWallet, addHardwareWallet } +Future showAccountsSheet(BuildContext context) { + return showAppModalBottomSheet(context: context, builder: (_) => const AccountsScreen()); +} class AccountsScreen extends ConsumerStatefulWidget { const AccountsScreen({super.key}); @@ -36,569 +28,588 @@ class AccountsScreen extends ConsumerStatefulWidget { } class _AccountsScreenState extends ConsumerState { - final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); + final AccountsService _accountsService = AccountsService(); final NumberFormattingService _formattingService = NumberFormattingService(); + final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); + final TextEditingController _nameController = TextEditingController(); bool _isCreatingAccount = false; - int? _selectedWalletIndex; + bool _isEditingName = false; + bool _isSavingName = false; + String? _editingAccountId; bool _isHardwareWallet(List accounts) { return accounts.isNotEmpty && accounts.every((a) => a.accountType == AccountType.keystone); } - int _nextWalletIndex(List accounts) { - if (accounts.isEmpty) return 0; - final maxIndex = accounts.map((a) => a.walletIndex).reduce((a, b) => a > b ? a : b); - return maxIndex + 1; - } - - Map> _groupByWallet(List accounts) { - final grouped = >{}; - for (final a in accounts) { - grouped.putIfAbsent(a.walletIndex, () => []).add(a); + int _walletIndexForActiveAccount(List accounts, DisplayAccount? activeDisplayAccount) { + if (activeDisplayAccount is RegularAccount) { + return activeDisplayAccount.account.walletIndex; } - for (final entry in grouped.entries) { - entry.value.sort((a, b) => a.index.compareTo(b.index)); + + if (activeDisplayAccount is EntrustedDisplayAccount) { + final parent = accounts.firstWhereOrNull((a) => a.accountId == activeDisplayAccount.account.parentAccountId); + if (parent != null) return parent.walletIndex; } - return Map.fromEntries(grouped.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); + + return accounts.isNotEmpty ? accounts.first.walletIndex : 0; } - String _walletLabel(int walletIndex, List accounts) { - if (_isHardwareWallet(accounts)) return 'Hardware Wallet'; - return 'Wallet ${walletIndex + 1}'; + List _displayAccounts(List accounts) { + final sorted = [...accounts]; + sorted.sort((a, b) { + final walletCmp = a.walletIndex.compareTo(b.walletIndex); + if (walletCmp != 0) return walletCmp; + return a.index.compareTo(b.index); + }); + return sorted; } Future _createNewAccount() async { - setState(() { - _isCreatingAccount = true; - }); + if (_isCreatingAccount) return; + + setState(() => _isCreatingAccount = true); try { final accounts = ref.read(accountsProvider).value ?? []; - int selectedWallet = getSelectedWalletIndex(accounts); - final grouped = _groupByWallet(accounts); - final selectedWalletAccounts = grouped[selectedWallet] ?? const []; - - if (_isHardwareWallet(selectedWalletAccounts)) { - await Navigator.push( - context, - MaterialPageRoute(builder: (context) => AddHardwareAccountScreen(walletIndex: selectedWallet)), - ); - } else { - await Navigator.push( - context, - MaterialPageRoute(builder: (context) => CreateAccountScreen(walletIndex: selectedWallet)), - ); + final activeDisplayAccount = ref.read(activeAccountProvider).value; + final walletIndex = _walletIndexForActiveAccount(accounts, activeDisplayAccount); + final selectedWalletAccounts = accounts.where((a) => a.walletIndex == walletIndex).toList(); + + final bool? created = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _isHardwareWallet(selectedWalletAccounts) + ? AddHardwareAccountScreen(walletIndex: walletIndex) + : CreateAccountScreen(walletIndex: walletIndex), + ), + ); + + if (created == true) { + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Could not add account.'); } - // Providers will automatically refresh when a new account is added } finally { if (mounted) { - setState(() { - _isCreatingAccount = false; - }); + setState(() => _isCreatingAccount = false); } } } - int getSelectedWalletIndex(List accounts) { - final selectedWallet = _selectedWalletIndex ?? (accounts.isNotEmpty ? accounts.first.walletIndex : 0); - return selectedWallet; + Future _switchAccount(Account account) async { + await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); + if (mounted) { + Navigator.of(context).pop(); + } } - Future _openWalletMoreActions() async { - final accounts = ref.read(accountsProvider).value ?? []; - final nextWalletIndex = _nextWalletIndex(accounts); - - final items = [ - Item(value: _WalletMoreAction.createWallet, label: 'Create new wallet'), - Item(value: _WalletMoreAction.importWallet, label: 'Import wallet'), - ]; - - if (FeatureFlags.enableKeystoneHardwareWallet) { - items.add(Item(value: _WalletMoreAction.addHardwareWallet, label: 'Add hardware wallet')); - } + Future _openEdit(Account account) async { + _nameController.text = account.name; + setState(() { + _editingAccountId = account.accountId; + _isEditingName = false; + }); + } - showSelectActionSheet<_WalletMoreAction>(context, items, (item) async { - final result = await (switch (item.value) { - _WalletMoreAction.createWallet => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CreateWalletAndBackupScreen(walletIndex: nextWalletIndex, popOnComplete: true), - ), - ), - _WalletMoreAction.importWallet => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ImportWalletScreen(walletIndex: nextWalletIndex, popOnComplete: true), - ), - ), - _WalletMoreAction.addHardwareWallet => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AddHardwareAccountScreen(walletIndex: nextWalletIndex, isNewWallet: true), - ), - ), - }); - if (result == true && mounted) { - ref.invalidate(accountsProvider); - ref.invalidate(activeAccountProvider); - ref.invalidate(activeAccountProvider); - } + void _closeEdit() { + setState(() { + _editingAccountId = null; + _isEditingName = false; + _isSavingName = false; }); } - Future _refreshAccounts() async { - // Invalidate main accounts provider - ref.invalidate(accountsProvider); + Future _saveEditedName(Account account) async { + final name = _nameController.text.trim(); + if (name.isEmpty) { + context.showErrorToaster(message: "Account name can't be empty"); + return; + } + if (name == account.name) { + setState(() => _isEditingName = false); + return; + } - // Invalidate per-account providers - final accounts = ref.read(accountsProvider).valueOrNull ?? []; - for (final account in accounts) { - ref.invalidate(isHighSecurityProvider(account)); - ref.invalidate(entrustedAccountsProvider(account)); - ref.invalidate(balanceProviderFamily(account.accountId)); + setState(() => _isSavingName = true); + try { + await _accountsService.updateAccountName(account, name); + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + if (mounted) { + setState(() { + _isEditingName = false; + }); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Failed to rename account.'); + } + } finally { + if (mounted) { + setState(() => _isSavingName = false); + } } + } - // Wait for accounts to reload - await ref.read(accountsProvider.notifier).stream.firstWhere((state) => !state.isLoading); + @override + void dispose() { + _nameController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - return ScaffoldBase( - decorations: [ - Positioned( - right: -40, - top: MediaQuery.of(context).size.height * 0.3, - child: const Sphere(variant: 2, size: 194), - ), - const Positioned(left: -40, bottom: 0, child: Sphere(variant: 7, size: 240.681)), - ], - appBar: WalletAppBar( - title: 'Your Accounts', - actions: [IconButton(onPressed: _openWalletMoreActions, icon: const Icon(Icons.more_horiz))], - ), - child: Column( - children: [ - // _buildWalletSelector(), - Expanded(child: _buildAccountsList()), + final media = MediaQuery.of(context); + final maxHeight = media.size.height - media.padding.top - 20; + final sheetHeight = math.min(610.0, maxHeight); - Button( - variant: ButtonVariant.glassOutline, - label: _walletActionLabel(), - onPressed: _isCreatingAccount ? null : _createNewAccount, - ), + final accountsAsync = ref.watch(accountsProvider); + final activeDisplayAccountAsync = ref.watch(activeAccountProvider); - SizedBox(height: context.themeSize.bottomButtonSpacing), - ], + final accounts = accountsAsync.value ?? []; + final activeDisplayAccount = activeDisplayAccountAsync.value; + final displayAccounts = _displayAccounts(accounts); + final activeAccountId = activeDisplayAccount?.account.accountId; + final editingAccount = _editingAccountId == null + ? null + : displayAccounts.firstWhereOrNull((a) => a.accountId == _editingAccountId); + + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 0, 14, 20), + child: SizedBox( + height: sheetHeight, + child: Container( + padding: const EdgeInsets.fromLTRB(24, 40, 24, 40), + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + border: Border.all(color: const Color(0xFF3D3D3D)), + borderRadius: BorderRadius.circular(24), + ), + child: _buildContent( + accountsAsync: accountsAsync, + activeDisplayAccountAsync: activeDisplayAccountAsync, + displayAccounts: displayAccounts, + activeAccountId: activeAccountId, + editingAccount: editingAccount, + ), + ), + ), ), ); } - String _walletActionLabel() { - final accounts = ref.watch(accountsProvider).value ?? []; - final grouped = _groupByWallet(accounts); - final selectedWallet = getSelectedWalletIndex(accounts); - final selectedAccounts = grouped[selectedWallet] ?? const []; - return _isHardwareWallet(selectedAccounts) ? 'Add Hardware Account' : 'Add Account'; - } + Widget _buildContent({ + required AsyncValue> accountsAsync, + required AsyncValue activeDisplayAccountAsync, + required List displayAccounts, + required String? activeAccountId, + required Account? editingAccount, + }) { + if (accountsAsync.isLoading || activeDisplayAccountAsync.isLoading) { + return const Center(child: CircularProgressIndicator(color: Colors.white)); + } - Widget _buildAccountsList() { - final accountsAsync = ref.watch(accountsProvider); - final activeDisplayAccountAsync = ref.watch(activeAccountProvider); + if (accountsAsync.hasError) { + return Center( + child: Text( + 'Failed to load accounts.', + style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), + ), + ); + } - return accountsAsync.when( - loading: () => Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), - error: (error, _) => Center( + if (activeDisplayAccountAsync.hasError) { + return Center( child: Text( - 'Failed to load accounts: $error', + 'Failed to load active account.', style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), ), - ), - data: (accounts) { - if (accounts.isEmpty) { - return Center( - child: Text('No accounts found.', style: context.themeText.smallParagraph?.copyWith(color: Colors.white70)), - ); - } - - 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: (activeDisplayAccount) { - if (_selectedWalletIndex == null) { - // 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); - }); - } - - final grouped = _groupByWallet(accounts); - if (grouped.length <= 1) { - final walletAccounts = grouped.values.first; - return RefreshIndicator( - onRefresh: _refreshAccounts, - child: ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 16.0), - itemCount: walletAccounts.length, - separatorBuilder: (context, index) => const SizedBox(height: 25), - itemBuilder: (context, index) { - final account = walletAccounts[index]; - return _buildAccountListItem(account, activeDisplayAccount?.account.accountId, index); - }, + ); + } + + if (editingAccount != null) { + return _buildEditAccountView(editingAccount); + } + + return _buildAccountsListView(displayAccounts, activeAccountId); + } + + Widget _buildAccountsListView(List displayAccounts, String? activeAccountId) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + width: 32.0, + height: 32.0, + child: activeAccountId == null + ? const SizedBox() + : AccountGradientImage(accountId: activeAccountId, width: 32.0, height: 32.0), + ), + const SizedBox(width: 12), + const Text( + 'Accounts', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), ), - ); - } - - final selectedWallet = _selectedWalletIndex ?? grouped.keys.first; - final children = []; - var sectionIndex = 0; - for (final entry in grouped.entries) { - final walletIndex = entry.key; - final walletAccounts = entry.value; - - if (sectionIndex > 0) children.add(const SizedBox(height: 18)); - children.add( - Padding( - padding: const EdgeInsets.only(bottom: 10), + ], + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + ], + ), + const SizedBox(height: 40), + Expanded( + child: displayAccounts.isEmpty + ? Center( child: Text( - _walletLabel(walletIndex, walletAccounts), - style: context.themeText.detail?.copyWith( - color: walletIndex == selectedWallet - ? context.themeColors.textPrimary - : context.themeColors.textMuted, - ), + 'No accounts found.', + style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), ), + ) + : ListView.separated( + itemCount: displayAccounts.length, + separatorBuilder: (_, _) => const SizedBox(height: 20), + itemBuilder: (_, index) { + final account = displayAccounts[index]; + return _buildAccountRow(account, account.accountId == activeAccountId); + }, ), - ); - - 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, activeDisplayAccount?.account.accountId, i)); - } - sectionIndex++; - } - - return RefreshIndicator( - onRefresh: _refreshAccounts, - child: ListView(padding: const EdgeInsets.symmetric(vertical: 16.0), children: children), - ); - }, - ); - }, + ), + const SizedBox(height: 24), + _buildPrimarySheetButton(label: 'Add Account', isLoading: _isCreatingAccount, onTap: _createNewAccount), + ], ); } - Widget _buildAccountListItem(Account account, String? activeAccountId, int index) { - final bool isActive = account.accountId == activeAccountId; - final entrustedAccountsAsync = ref.watch(entrustedAccountsProvider(account)); - final entrustedAccountsData = entrustedAccountsAsync.value ?? []; + Widget _buildEditAccountView(Account account) { + if (!_isEditingName && _nameController.text != account.name) { + _nameController.text = account.name; + } - final entrustedNodes = entrustedAccountsData - .map((entrusted) => TreeNode(data: entrusted)) - .toList(); + return FutureBuilder( + future: _checksumService.getHumanReadableName(account.accountId), + builder: (context, snapshot) { + final checksum = snapshot.connectionState == ConnectionState.done ? (snapshot.data ?? '-') : 'Loading...'; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: _closeEdit, + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + const Text( + 'Edit Account', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + ], + ), + const SizedBox(height: 40), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Account Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildAccountNameField(account), + const SizedBox(height: 40), + Text('Address Details', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildAddressDetails(account, checksum), + ], + ), + ), + ), + const SizedBox(height: 24), + _buildPrimarySheetButton( + label: 'Share Account Details', + onTap: () => showReceiveSheet(context, isReceiving: false), + ), + ], + ); + }, + ); + } - final isHighSecurityAsync = ref.watch(isHighSecurityProvider(account)); - final isHighSecurity = isHighSecurityAsync.value ?? false; + Widget _buildAccountRow(Account account, bool isActive) { + final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); + final balanceText = balanceAsync.when( + loading: () => 'Loading...', + error: (_, _) => 'Balance unavailable', + data: (balance) => '${_formattingService.formatBalance(balance)} ${AppConstants.tokenSymbol}', + ); - return InkWell( - onTap: () async { - await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); - if (mounted) Navigator.pop(context); - }, - child: Stack( - clipBehavior: Clip.hardEdge, - children: [ - Column( - children: [ - Row( + return GestureDetector( + onTap: () => _switchAccount(account), + child: Container( + padding: const EdgeInsets.fromLTRB(16, 12, 12, 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: context.isTablet ? 20 : 8, vertical: 8), - height: context.themeSize.accountListItemHeight, - decoration: ShapeDecoration( - color: isActive ? context.themeColors.surfaceActive : context.themeColors.surface, - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: context.themeColors.borderLight), - borderRadius: BorderRadius.circular(5), - ), - ), - child: Row( - children: [ - const SizedBox(width: 24), - Expanded( - child: Consumer( - builder: (context, ref, child) { - final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); - - return FutureBuilder( - future: _checksumService.getHumanReadableName(account.accountId), - builder: (context, checksumSnapshot) { - final humanChecksum = checksumSnapshot.data ?? ''; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - account.name, - style: context.themeText.smallParagraph?.copyWith( - color: isActive ? Colors.black : Colors.white, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isHighSecurity) - AccountTag( - text: 'High Security', - color: context.themeColors.accountTagEntrusted, - ), - if (isHighSecurity && entrustedNodes.isNotEmpty) - const SizedBox(width: 6), - if (entrustedNodes.isNotEmpty) - AccountTag( - text: 'Guardian', - color: context.themeColors.accountTagGuardian, - ), - ], - ), - ], - ), - Text( - humanChecksum, - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.checksumDarker - : context.themeColors.checksum, - ), - ), - Row( - children: [ - Text( - context.isTablet - ? account.accountId - // ignore: lines_longer_than_80_chars - : AddressFormattingService.formatAddress(account.accountId), - style: context.themeText.tiny?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textMuted, - ), - ), - ], - ), - const SizedBox(height: 2), - balanceAsync.when( - loading: () => Text( - 'loading balance...', - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.light, - ), - ), - error: (error, _) => Text( - 'error loading', - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textPrimary, - ), - ), - data: (balance) => Text.rich( - TextSpan( - children: [ - TextSpan( - text: _formattingService.formatBalance(balance), - style: context.themeText.detail?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textPrimary, - ), - ), - TextSpan( - text: ' ${AppConstants.tokenSymbol}', - style: context.themeText.tiny?.copyWith( - color: isActive - ? context.themeColors.darkGray - : context.themeColors.textPrimary, - ), - ), - ], - ), - ), - ), - ], - ); - }, - ); - }, - ), - ), - ], - ), + Text( + account.name, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + color: isActive ? context.themeColors.pink : Colors.white, + height: 1.35, ), ), - 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), + const SizedBox(height: 4), + Text( + balanceText, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 12, + fontWeight: FontWeight.w400, + color: Color(0x80FFFFFF), + height: 1.35, ), - onPressed: () async { - // Get current data from providers - final balanceAsync = ref.read(balanceProviderFamily(account.accountId)); - final checksumName = await _checksumService.getHumanReadableName(account.accountId); - - balanceAsync.when( - loading: () { - // Show loading or handle appropriately - }, - error: (error, _) { - // Handle error - }, - data: (balance) async { - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), - builder: (context) => AccountSettingsScreen( - account: account, - balance: _formattingService.formatBalance(balance, addSymbol: true), - checksumName: checksumName, - isHighSecurity: isHighSecurity, - isEntrustedAccount: false, - ), - ), - ); - // Providers will automatically refresh if needed - }, - ); - }, ), ], ), - if (entrustedNodes.isNotEmpty) - 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), - ], - ), - ), - ), - ), - ), - 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, - ), - ), - ); - }, - ); - }, - ), - ], - ); + ), + const SizedBox(width: 12), + _buildIconActionButton( + size: 40, + radius: 8, + icon: Icons.edit_outlined, + iconSize: 20, + onTap: () => _openEdit(account), + ), + ], + ), + ), + ); + } + + Widget _buildAccountNameField(Account account) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _nameController, + readOnly: !_isEditingName || _isSavingName, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.themeColors.pink, + height: 1.35, + ), + cursorColor: context.themeColors.pink, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onSubmitted: (_) { + if (_isEditingName && !_isSavingName) { + _saveEditedName(account); + } + }, + onTap: () { + if (!_isEditingName) { + setState(() => _isEditingName = true); + } + }, + ), + ), + _isSavingName + ? const SizedBox( + width: 40, + height: 40, + child: Center( + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ), + ), + ) + : _buildIconActionButton( + size: 40, + radius: 8, + icon: _isEditingName ? Icons.check : Icons.edit_outlined, + iconSize: 20, + onTap: () { + if (_isEditingName) { + _saveEditedName(account); + } else { + setState(() => _isEditingName = true); + } }, ), - ], + ], + ), + ); + } + + Widget _buildAddressDetails(Account account, String checksum) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + child: Column( + children: [ + _buildCopyRow( + value: account.accountId, + onCopy: () => context.copyTextWithToaster(account.accountId), + textStyle: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1.35, + ), + maxLines: null, + overflow: TextOverflow.visible, ), - Positioned( - // calculating the middle point - top: (context.themeSize.accountListItemHeight / 2) - (context.themeSize.accountListItemLogoWidth / 2), - left: (context.themeSize.accountListItemLogoWidth / 2) * -1, - child: AccountGradientImage( - accountId: account.accountId, - width: context.themeSize.accountListItemLogoWidth, - height: context.themeSize.accountListItemLogoWidth, + const SizedBox(height: 8), + _buildCopyRow( + value: checksum, + onCopy: () => context.copyTextWithToaster(checksum), + textStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: context.themeColors.pink, + height: 1, ), ), ], ), ); } + + Widget _buildCopyRow({ + required String value, + required VoidCallback onCopy, + required TextStyle textStyle, + int? maxLines = 1, + TextOverflow overflow = TextOverflow.ellipsis, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text(value, maxLines: maxLines, overflow: overflow, style: textStyle), + ), + const SizedBox(width: 8), + _buildIconActionButton(size: 20, radius: 4, icon: Icons.copy_outlined, iconSize: 12, onTap: onCopy), + ], + ); + } + + Widget _buildIconActionButton({ + required double size, + required double radius, + required IconData icon, + required double iconSize, + required VoidCallback onTap, + }) { + return Material( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(radius), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: size, + height: size, + child: Icon(icon, color: Colors.white, size: iconSize), + ), + ), + ); + } + + Widget _buildPrimarySheetButton({required String label, required VoidCallback onTap, bool isLoading = false}) { + return Material( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: isLoading ? null : onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + height: 64, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + ), + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : Text( + label, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + ), + ), + ); + } } diff --git a/mobile-app/lib/features/main/screens/receive_screen.dart b/mobile-app/lib/features/main/screens/receive_screen.dart index 2ab6b7a7..73fe854c 100644 --- a/mobile-app/lib/features/main/screens/receive_screen.dart +++ b/mobile-app/lib/features/main/screens/receive_screen.dart @@ -164,7 +164,7 @@ class _ReceiveSheetState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ AccountGradientImage( - accountId: _accountId, + accountId: _accountId!, width: context.isTablet ? 32.0 : 24.0, height: context.isTablet ? 32.0 : 24.0, ), diff --git a/mobile-app/lib/features/main/screens/settings_screen.dart b/mobile-app/lib/features/main/screens/settings_screen.dart index e07abeb5..fab4af2c 100644 --- a/mobile-app/lib/features/main/screens/settings_screen.dart +++ b/mobile-app/lib/features/main/screens/settings_screen.dart @@ -159,7 +159,7 @@ class _SettingsScreenState extends ConsumerState { ListItem( title: 'Manage Accounts', onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const AccountsScreen())); + showAccountsSheet(context); }, ), const SizedBox(height: 22), 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 786eec7b..2e8da690 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 @@ -127,7 +127,7 @@ class _WalletMainState extends ConsumerState { InkWell( child: SvgPicture.asset('assets/wallet_icon.svg', width: 26, height: 26), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const AccountsScreen())); + showAccountsSheet(context); }, ), ], diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 66ddd0b2..82079678 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -144,7 +144,7 @@ class _HomeScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AccountsScreen())), + onTap: () => showAccountsSheet(context), child: AccountGradientImage(accountId: active.account.accountId, width: 40.0, height: 40.0), ), Row( diff --git a/quantus_sdk/lib/src/services/accounts_service.dart b/quantus_sdk/lib/src/services/accounts_service.dart index e66c7e0e..3a91e82d 100644 --- a/quantus_sdk/lib/src/services/accounts_service.dart +++ b/quantus_sdk/lib/src/services/accounts_service.dart @@ -23,7 +23,9 @@ class AccountsService { if (mnemonic == null) { throw Exception('Mnemonic not found. Cannot create new account.'); } - final nextIndex = await _settingsService.getNextFreeAccountIndex(walletIndex); + final nextIndex = await _settingsService.getNextFreeAccountIndex( + walletIndex, + ); final keypair = HdWalletService().keyPairAtIndex(mnemonic, nextIndex); final newAccount = Account( walletIndex: walletIndex, @@ -35,7 +37,25 @@ class AccountsService { } Future updateAccountName(Account account, String name) async { - final updatedAccount = account.copyWith(name: name); + final normalizedName = name.trim(); + if (normalizedName.isEmpty) { + throw Exception("Account name can't be empty"); + } + + final accounts = await _settingsService.getAccounts(); + final currentAccountIndex = accounts.indexWhere( + (a) => a.accountId == account.accountId, + ); + if (currentAccountIndex == -1) { + throw Exception('Account not found'); + } + + final currentAccount = accounts[currentAccountIndex]; + if (currentAccount.name == normalizedName) { + return; + } + + final updatedAccount = currentAccount.copyWith(name: normalizedName); await _settingsService.updateAccount(updatedAccount); onAccountsChanged?.call(); } From d4fdaf02d015fae9e61cda45f1b28c14287706d6 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 21:58:29 +0800 Subject: [PATCH 02/20] share button fix on account edit --- .../main/screens/account_settings_screen.dart | 10 ++- .../main/screens/accounts_screen.dart | 77 +++++++++++++++---- .../features/main/screens/receive_screen.dart | 15 +--- mobile-app/lib/shared/utils/share_utils.dart | 24 ++++++ .../lib/v2/screens/receive/receive_sheet.dart | 15 +--- 5 files changed, 97 insertions(+), 44 deletions(-) create mode 100644 mobile-app/lib/shared/utils/share_utils.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 64539e5e..36011fca 100644 --- a/mobile-app/lib/features/main/screens/account_settings_screen.dart +++ b/mobile-app/lib/features/main/screens/account_settings_screen.dart @@ -18,7 +18,7 @@ import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart import 'package:resonance_network_wallet/features/main/screens/create_account_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_details_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/high_security/high_security_get_started_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/receive_screen.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.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/clipboard_extensions.dart'; @@ -256,9 +256,11 @@ class _AccountSettingsScreenState extends ConsumerState { Widget _buildShareSection() { return _buildSettingCard( child: InkWell( - onTap: () { - showReceiveSheet(context, isReceiving: false); - }, + onTap: () => shareAccountDetails( + context, + widget.account.accountId, + checksum: widget.checksumName.isNotEmpty ? widget.checksumName : null, + ), child: Padding( padding: const EdgeInsets.only(left: 10.0, top: 8.5, bottom: 8.5, right: 18.0), child: Row( diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 6d34ea94..36e6318b 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -7,8 +7,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/create_account_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/receive_screen.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.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_providers.dart'; @@ -32,11 +31,18 @@ class _AccountsScreenState extends ConsumerState { final NumberFormattingService _formattingService = NumberFormattingService(); final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); final TextEditingController _nameController = TextEditingController(); + final TextEditingController _createNameController = TextEditingController(); bool _isCreatingAccount = false; + bool _isSavingCreatedAccount = false; bool _isEditingName = false; + bool _isEditingCreatedName = false; bool _isSavingName = false; + bool _isCreateViewOpen = false; String? _editingAccountId; + String? _sharingAccountId; + Account? _draftAccount; + String _draftChecksum = 'Loading...'; bool _isHardwareWallet(List accounts) { return accounts.isNotEmpty && accounts.every((a) => a.accountType == AccountType.keystone); @@ -75,18 +81,26 @@ class _AccountsScreenState extends ConsumerState { final walletIndex = _walletIndexForActiveAccount(accounts, activeDisplayAccount); final selectedWalletAccounts = accounts.where((a) => a.walletIndex == walletIndex).toList(); - final bool? created = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _isHardwareWallet(selectedWalletAccounts) - ? AddHardwareAccountScreen(walletIndex: walletIndex) - : CreateAccountScreen(walletIndex: walletIndex), - ), - ); - - if (created == true) { - ref.invalidate(accountsProvider); - ref.invalidate(activeAccountProvider); + if (_isHardwareWallet(selectedWalletAccounts)) { + final created = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => AddHardwareAccountScreen(walletIndex: walletIndex)), + ); + if (created == true) { + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + } + } else { + final draft = await _accountsService.createNewAccount(walletIndex: walletIndex); + final checksum = await _checksumService.getHumanReadableName(draft.accountId); + if (!mounted) return; + _createNameController.text = draft.name; + setState(() { + _draftAccount = draft; + _draftChecksum = checksum; + _isCreateViewOpen = true; + _isEditingCreatedName = false; + }); } } catch (_) { if (mounted) { @@ -122,6 +136,25 @@ class _AccountsScreenState extends ConsumerState { }); } + void _closeCreateView() { + setState(() { + _isCreateViewOpen = false; + _isEditingCreatedName = false; + _isSavingCreatedAccount = false; + _draftAccount = null; + _draftChecksum = 'Loading...'; + _createNameController.clear(); + }); + } + + void _openShare(Account account) { + setState(() => _sharingAccountId = account.accountId); + } + + void _closeShare() { + setState(() => _sharingAccountId = null); + } + Future _saveEditedName(Account account) async { final name = _nameController.text.trim(); if (name.isEmpty) { @@ -157,6 +190,7 @@ class _AccountsScreenState extends ConsumerState { @override void dispose() { _nameController.dispose(); + _createNameController.dispose(); super.dispose(); } @@ -176,6 +210,9 @@ class _AccountsScreenState extends ConsumerState { final editingAccount = _editingAccountId == null ? null : displayAccounts.firstWhereOrNull((a) => a.accountId == _editingAccountId); + final sharingAccount = _sharingAccountId == null + ? null + : displayAccounts.firstWhereOrNull((a) => a.accountId == _sharingAccountId); return Align( alignment: Alignment.bottomCenter, @@ -196,6 +233,7 @@ class _AccountsScreenState extends ConsumerState { displayAccounts: displayAccounts, activeAccountId: activeAccountId, editingAccount: editingAccount, + sharingAccount: sharingAccount, ), ), ), @@ -209,6 +247,7 @@ class _AccountsScreenState extends ConsumerState { required List displayAccounts, required String? activeAccountId, required Account? editingAccount, + required Account? sharingAccount, }) { if (accountsAsync.isLoading || activeDisplayAccountAsync.isLoading) { return const Center(child: CircularProgressIndicator(color: Colors.white)); @@ -232,6 +271,14 @@ class _AccountsScreenState extends ConsumerState { ); } + if (_isCreateViewOpen && _draftAccount != null) { + return _buildCreateAccountView(); + } + + if (sharingAccount != null) { + return _buildShareAccountView(sharingAccount); + } + if (editingAccount != null) { return _buildEditAccountView(editingAccount); } @@ -362,7 +409,7 @@ class _AccountsScreenState extends ConsumerState { const SizedBox(height: 24), _buildPrimarySheetButton( label: 'Share Account Details', - onTap: () => showReceiveSheet(context, isReceiving: false), + onTap: () => _openShare(account), ), ], ); diff --git a/mobile-app/lib/features/main/screens/receive_screen.dart b/mobile-app/lib/features/main/screens/receive_screen.dart index 73fe854c..fff6281a 100644 --- a/mobile-app/lib/features/main/screens/receive_screen.dart +++ b/mobile-app/lib/features/main/screens/receive_screen.dart @@ -12,7 +12,7 @@ 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/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; class ReceiveSheet extends StatefulWidget { final bool isReceiving; @@ -69,17 +69,8 @@ class _ReceiveSheetState extends State { } void _share() { - if (_accountId != null && _checksum != null) { - final textToShare = - 'Hey! These are my Quantus account details:\n\nAddress:\n$_accountId\n\nCheckphrase:$_checksum\n\nTo open in the app or to download click the link below:\n${AppConstants.websiteBaseUrl}/account?id=$_accountId'; - SharePlus.instance.share( - ShareParams( - text: textToShare, - subject: 'Shared Address', - title: 'Shared Address', - sharePositionOrigin: context.sharePositionRect(), - ), - ); + if (_accountId != null) { + shareAccountDetails(context, _accountId!, checksum: _checksum); } } diff --git a/mobile-app/lib/shared/utils/share_utils.dart b/mobile-app/lib/shared/utils/share_utils.dart new file mode 100644 index 00000000..276d2edf --- /dev/null +++ b/mobile-app/lib/shared/utils/share_utils.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:share_plus/share_plus.dart'; + +String buildAccountShareText(String accountId, {String? checksum}) { + final checkphrasePart = checksum != null && + checksum.isNotEmpty && + checksum != 'Loading...' && + checksum != '-' + ? '\n\nCheckphrase:$checksum' + : ''; + return 'Hey! These are my Quantus account details:\n\nAddress:\n$accountId$checkphrasePart\n\nTo open in the app or to download click the link below:\n${AppConstants.websiteBaseUrl}/account?id=$accountId'; +} + +void shareAccountDetails(BuildContext context, String accountId, {String? checksum}) { + SharePlus.instance.share( + ShareParams( + text: buildAccountShareText(accountId, checksum: checksum), + subject: 'Shared Address', + title: 'Shared Address', + sharePositionOrigin: context.sharePositionRect(), + ), + ); +} diff --git a/mobile-app/lib/v2/screens/receive/receive_sheet.dart b/mobile-app/lib/v2/screens/receive/receive_sheet.dart index daa272d0..cecc5a95 100644 --- a/mobile-app/lib/v2/screens/receive/receive_sheet.dart +++ b/mobile-app/lib/v2/screens/receive/receive_sheet.dart @@ -4,9 +4,9 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -import 'package:share_plus/share_plus.dart'; class ReceiveSheet extends StatefulWidget { const ReceiveSheet({super.key}); @@ -55,18 +55,7 @@ class _ReceiveSheetState extends State { void _share() { if (_accountId != null) { - final text = - 'Hey! These are my Quantus account details:\n\nAddress:\n$_accountId' - '${_checksum != null ? '\n\nCheckphrase: $_checksum' : ''}' - '\n\nTo open in the app or download:\n${AppConstants.websiteBaseUrl}/account?id=$_accountId'; - SharePlus.instance.share( - ShareParams( - text: text, - subject: 'Shared Address', - title: 'Shared Address', - sharePositionOrigin: context.sharePositionRect(), - ), - ); + shareAccountDetails(context, _accountId!, checksum: _checksum); } } From 406c6b0095a502e41e93b290ad7bc60dfcb46df5 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 22:04:25 +0800 Subject: [PATCH 03/20] new edit account screen --- .../main/screens/accounts_screen.dart | 212 ++++++++++++++++-- 1 file changed, 193 insertions(+), 19 deletions(-) diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 36e6318b..8a80f926 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -40,7 +40,6 @@ class _AccountsScreenState extends ConsumerState { bool _isSavingName = false; bool _isCreateViewOpen = false; String? _editingAccountId; - String? _sharingAccountId; Account? _draftAccount; String _draftChecksum = 'Loading...'; @@ -147,14 +146,6 @@ class _AccountsScreenState extends ConsumerState { }); } - void _openShare(Account account) { - setState(() => _sharingAccountId = account.accountId); - } - - void _closeShare() { - setState(() => _sharingAccountId = null); - } - Future _saveEditedName(Account account) async { final name = _nameController.text.trim(); if (name.isEmpty) { @@ -210,9 +201,6 @@ class _AccountsScreenState extends ConsumerState { final editingAccount = _editingAccountId == null ? null : displayAccounts.firstWhereOrNull((a) => a.accountId == _editingAccountId); - final sharingAccount = _sharingAccountId == null - ? null - : displayAccounts.firstWhereOrNull((a) => a.accountId == _sharingAccountId); return Align( alignment: Alignment.bottomCenter, @@ -233,7 +221,6 @@ class _AccountsScreenState extends ConsumerState { displayAccounts: displayAccounts, activeAccountId: activeAccountId, editingAccount: editingAccount, - sharingAccount: sharingAccount, ), ), ), @@ -247,7 +234,6 @@ class _AccountsScreenState extends ConsumerState { required List displayAccounts, required String? activeAccountId, required Account? editingAccount, - required Account? sharingAccount, }) { if (accountsAsync.isLoading || activeDisplayAccountAsync.isLoading) { return const Center(child: CircularProgressIndicator(color: Colors.white)); @@ -275,10 +261,6 @@ class _AccountsScreenState extends ConsumerState { return _buildCreateAccountView(); } - if (sharingAccount != null) { - return _buildShareAccountView(sharingAccount); - } - if (editingAccount != null) { return _buildEditAccountView(editingAccount); } @@ -409,7 +391,11 @@ class _AccountsScreenState extends ConsumerState { const SizedBox(height: 24), _buildPrimarySheetButton( label: 'Share Account Details', - onTap: () => _openShare(account), + onTap: () => shareAccountDetails( + context, + account.accountId, + checksum: checksum != 'Loading...' && checksum != '-' ? checksum : null, + ), ), ], ); @@ -417,6 +403,63 @@ class _AccountsScreenState extends ConsumerState { ); } + Widget _buildCreateAccountView() { + final draft = _draftAccount!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSheetHeader(title: 'New Account', onBack: _closeCreateView), + const SizedBox(height: 40), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Wallet Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreatedNameField(), + const SizedBox(height: 40), + Text('Wallet Address', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreateField( + value: AddressFormattingService.formatAddress(draft.accountId), + onCopy: () => context.copyTextWithToaster(draft.accountId), + textStyle: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1.35, + ), + ), + const SizedBox(height: 40), + Text('Wallet Checkphrase', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreateField( + value: _draftChecksum, + onCopy: () => context.copyTextWithToaster(_draftChecksum), + textStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: context.themeColors.pink, + height: 1.0, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + _buildPrimarySheetButton( + label: 'Create Account', + isLoading: _isSavingCreatedAccount, + onTap: _submitCreatedAccount, + ), + ], + ); + } + Widget _buildAccountRow(Account account, bool isActive) { final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); final balanceText = balanceAsync.when( @@ -547,6 +590,108 @@ class _AccountsScreenState extends ConsumerState { ); } + Widget _buildCreatedNameField() { + return Container( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _createNameController, + readOnly: !_isEditingCreatedName || _isSavingCreatedAccount, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.white, + ), + cursorColor: Colors.white, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + _buildIconActionButton( + size: 40, + radius: 8, + icon: _isEditingCreatedName ? Icons.check : Icons.edit_outlined, + iconSize: 20, + onTap: () { + setState(() => _isEditingCreatedName = !_isEditingCreatedName); + }, + ), + ], + ), + ); + } + + Widget _buildCreateField({required String value, required VoidCallback onCopy, required TextStyle textStyle}) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Expanded( + child: Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle), + ), + const SizedBox(width: 8), + _buildIconActionButton(size: 40, radius: 8, icon: Icons.copy_outlined, iconSize: 20, onTap: onCopy), + ], + ), + ); + } + + Widget _buildSheetHeader({required String title, VoidCallback? onBack}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 24, + height: 24, + child: onBack == null + ? const SizedBox() + : IconButton( + onPressed: onBack, + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 24, minWidth: 24), + ), + ), + Text( + title, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 24, minWidth: 24), + ), + ), + ], + ); + } + Widget _buildAddressDetails(Account account, String checksum) { return Container( padding: const EdgeInsets.all(16), @@ -659,4 +804,33 @@ class _AccountsScreenState extends ConsumerState { ), ); } + + Future _submitCreatedAccount() async { + final draft = _draftAccount; + if (draft == null) return; + final name = _createNameController.text.trim(); + if (name.isEmpty) { + context.showErrorToaster(message: "Account name can't be empty"); + return; + } + + setState(() => _isSavingCreatedAccount = true); + try { + final accountToSave = draft.copyWith(name: name); + await _accountsService.addAccount(accountToSave); + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + if (mounted) { + _closeCreateView(); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Failed to create account.'); + } + } finally { + if (mounted) { + setState(() => _isSavingCreatedAccount = false); + } + } + } } From 68dd3ceabc3e412234d69fc13c5ecfd357b06f05 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 22:10:05 +0800 Subject: [PATCH 04/20] restore v1 accounts screen, move new code to accounts sheet --- .../main/screens/accounts_screen.dart | 1236 +++++++---------- .../lib/v2/screens/home/accounts_sheet.dart | 836 +++++++++++ 2 files changed, 1338 insertions(+), 734 deletions(-) create mode 100644 mobile-app/lib/v2/screens/home/accounts_sheet.dart diff --git a/mobile-app/lib/features/main/screens/accounts_screen.dart b/mobile-app/lib/features/main/screens/accounts_screen.dart index 8a80f926..333179d8 100644 --- a/mobile-app/lib/features/main/screens/accounts_screen.dart +++ b/mobile-app/lib/features/main/screens/accounts_screen.dart @@ -1,23 +1,32 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; 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'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; -import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; +import 'package:resonance_network_wallet/features/components/account_tag.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; +import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/features/components/select.dart'; +import 'package:resonance_network_wallet/features/components/select_action_sheet.dart'; +import 'package:resonance_network_wallet/features/components/sphere.dart'; +import 'package:resonance_network_wallet/features/components/tree_list.dart'; +import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; +import 'package:resonance_network_wallet/features/main/screens/account_settings_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; -import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; +import 'package:resonance_network_wallet/features/main/screens/create_account_screen.dart'; +import 'package:resonance_network_wallet/features/main/screens/create_wallet_and_backup_screen.dart'; +import 'package:resonance_network_wallet/features/main/screens/import_wallet_screen.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/entrusted_account_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; -import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; -import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; -Future showAccountsSheet(BuildContext context) { - return showAppModalBottomSheet(context: context, builder: (_) => const AccountsScreen()); -} +enum _WalletMoreAction { createWallet, importWallet, addHardwareWallet } class AccountsScreen extends ConsumerStatefulWidget { const AccountsScreen({super.key}); @@ -27,810 +36,569 @@ class AccountsScreen extends ConsumerStatefulWidget { } class _AccountsScreenState extends ConsumerState { - final AccountsService _accountsService = AccountsService(); - final NumberFormattingService _formattingService = NumberFormattingService(); final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _createNameController = TextEditingController(); + final NumberFormattingService _formattingService = NumberFormattingService(); bool _isCreatingAccount = false; - bool _isSavingCreatedAccount = false; - bool _isEditingName = false; - bool _isEditingCreatedName = false; - bool _isSavingName = false; - bool _isCreateViewOpen = false; - String? _editingAccountId; - Account? _draftAccount; - String _draftChecksum = 'Loading...'; + int? _selectedWalletIndex; bool _isHardwareWallet(List accounts) { return accounts.isNotEmpty && accounts.every((a) => a.accountType == AccountType.keystone); } - int _walletIndexForActiveAccount(List accounts, DisplayAccount? activeDisplayAccount) { - if (activeDisplayAccount is RegularAccount) { - return activeDisplayAccount.account.walletIndex; - } + int _nextWalletIndex(List accounts) { + if (accounts.isEmpty) return 0; + final maxIndex = accounts.map((a) => a.walletIndex).reduce((a, b) => a > b ? a : b); + return maxIndex + 1; + } - if (activeDisplayAccount is EntrustedDisplayAccount) { - final parent = accounts.firstWhereOrNull((a) => a.accountId == activeDisplayAccount.account.parentAccountId); - if (parent != null) return parent.walletIndex; + Map> _groupByWallet(List accounts) { + final grouped = >{}; + for (final a in accounts) { + grouped.putIfAbsent(a.walletIndex, () => []).add(a); } - - return accounts.isNotEmpty ? accounts.first.walletIndex : 0; + for (final entry in grouped.entries) { + entry.value.sort((a, b) => a.index.compareTo(b.index)); + } + return Map.fromEntries(grouped.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); } - List _displayAccounts(List accounts) { - final sorted = [...accounts]; - sorted.sort((a, b) { - final walletCmp = a.walletIndex.compareTo(b.walletIndex); - if (walletCmp != 0) return walletCmp; - return a.index.compareTo(b.index); - }); - return sorted; + String _walletLabel(int walletIndex, List accounts) { + if (_isHardwareWallet(accounts)) return 'Hardware Wallet'; + return 'Wallet ${walletIndex + 1}'; } Future _createNewAccount() async { - if (_isCreatingAccount) return; - - setState(() => _isCreatingAccount = true); + setState(() { + _isCreatingAccount = true; + }); try { final accounts = ref.read(accountsProvider).value ?? []; - final activeDisplayAccount = ref.read(activeAccountProvider).value; - final walletIndex = _walletIndexForActiveAccount(accounts, activeDisplayAccount); - final selectedWalletAccounts = accounts.where((a) => a.walletIndex == walletIndex).toList(); + int selectedWallet = getSelectedWalletIndex(accounts); + final grouped = _groupByWallet(accounts); + final selectedWalletAccounts = grouped[selectedWallet] ?? const []; if (_isHardwareWallet(selectedWalletAccounts)) { - final created = await Navigator.push( + await Navigator.push( context, - MaterialPageRoute(builder: (context) => AddHardwareAccountScreen(walletIndex: walletIndex)), + MaterialPageRoute(builder: (context) => AddHardwareAccountScreen(walletIndex: selectedWallet)), ); - if (created == true) { - ref.invalidate(accountsProvider); - ref.invalidate(activeAccountProvider); - } } else { - final draft = await _accountsService.createNewAccount(walletIndex: walletIndex); - final checksum = await _checksumService.getHumanReadableName(draft.accountId); - if (!mounted) return; - _createNameController.text = draft.name; - setState(() { - _draftAccount = draft; - _draftChecksum = checksum; - _isCreateViewOpen = true; - _isEditingCreatedName = false; - }); - } - } catch (_) { - if (mounted) { - context.showErrorToaster(message: 'Could not add account.'); + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => CreateAccountScreen(walletIndex: selectedWallet)), + ); } + // Providers will automatically refresh when a new account is added } finally { if (mounted) { - setState(() => _isCreatingAccount = false); + setState(() { + _isCreatingAccount = false; + }); } } } - Future _switchAccount(Account account) async { - await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); - if (mounted) { - Navigator.of(context).pop(); - } + int getSelectedWalletIndex(List accounts) { + final selectedWallet = _selectedWalletIndex ?? (accounts.isNotEmpty ? accounts.first.walletIndex : 0); + return selectedWallet; } - Future _openEdit(Account account) async { - _nameController.text = account.name; - setState(() { - _editingAccountId = account.accountId; - _isEditingName = false; - }); - } + Future _openWalletMoreActions() async { + final accounts = ref.read(accountsProvider).value ?? []; + final nextWalletIndex = _nextWalletIndex(accounts); - void _closeEdit() { - setState(() { - _editingAccountId = null; - _isEditingName = false; - _isSavingName = false; - }); - } + final items = [ + Item(value: _WalletMoreAction.createWallet, label: 'Create new wallet'), + Item(value: _WalletMoreAction.importWallet, label: 'Import wallet'), + ]; - void _closeCreateView() { - setState(() { - _isCreateViewOpen = false; - _isEditingCreatedName = false; - _isSavingCreatedAccount = false; - _draftAccount = null; - _draftChecksum = 'Loading...'; - _createNameController.clear(); + if (FeatureFlags.enableKeystoneHardwareWallet) { + items.add(Item(value: _WalletMoreAction.addHardwareWallet, label: 'Add hardware wallet')); + } + + showSelectActionSheet<_WalletMoreAction>(context, items, (item) async { + final result = await (switch (item.value) { + _WalletMoreAction.createWallet => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateWalletAndBackupScreen(walletIndex: nextWalletIndex, popOnComplete: true), + ), + ), + _WalletMoreAction.importWallet => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImportWalletScreen(walletIndex: nextWalletIndex, popOnComplete: true), + ), + ), + _WalletMoreAction.addHardwareWallet => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddHardwareAccountScreen(walletIndex: nextWalletIndex, isNewWallet: true), + ), + ), + }); + if (result == true && mounted) { + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + ref.invalidate(activeAccountProvider); + } }); } - Future _saveEditedName(Account account) async { - final name = _nameController.text.trim(); - if (name.isEmpty) { - context.showErrorToaster(message: "Account name can't be empty"); - return; - } - if (name == account.name) { - setState(() => _isEditingName = false); - return; - } + Future _refreshAccounts() async { + // Invalidate main accounts provider + ref.invalidate(accountsProvider); - setState(() => _isSavingName = true); - try { - await _accountsService.updateAccountName(account, name); - ref.invalidate(accountsProvider); - ref.invalidate(activeAccountProvider); - if (mounted) { - setState(() { - _isEditingName = false; - }); - } - } catch (_) { - if (mounted) { - context.showErrorToaster(message: 'Failed to rename account.'); - } - } finally { - if (mounted) { - setState(() => _isSavingName = false); - } + // Invalidate per-account providers + final accounts = ref.read(accountsProvider).valueOrNull ?? []; + for (final account in accounts) { + ref.invalidate(isHighSecurityProvider(account)); + ref.invalidate(entrustedAccountsProvider(account)); + ref.invalidate(balanceProviderFamily(account.accountId)); } - } - @override - void dispose() { - _nameController.dispose(); - _createNameController.dispose(); - super.dispose(); + // Wait for accounts to reload + await ref.read(accountsProvider.notifier).stream.firstWhere((state) => !state.isLoading); } @override Widget build(BuildContext context) { - final media = MediaQuery.of(context); - final maxHeight = media.size.height - media.padding.top - 20; - final sheetHeight = math.min(610.0, maxHeight); - - final accountsAsync = ref.watch(accountsProvider); - final activeDisplayAccountAsync = ref.watch(activeAccountProvider); + return ScaffoldBase( + decorations: [ + Positioned( + right: -40, + top: MediaQuery.of(context).size.height * 0.3, + child: const Sphere(variant: 2, size: 194), + ), + const Positioned(left: -40, bottom: 0, child: Sphere(variant: 7, size: 240.681)), + ], + appBar: WalletAppBar( + title: 'Your Accounts', + actions: [IconButton(onPressed: _openWalletMoreActions, icon: const Icon(Icons.more_horiz))], + ), + child: Column( + children: [ + // _buildWalletSelector(), + Expanded(child: _buildAccountsList()), - final accounts = accountsAsync.value ?? []; - final activeDisplayAccount = activeDisplayAccountAsync.value; - final displayAccounts = _displayAccounts(accounts); - final activeAccountId = activeDisplayAccount?.account.accountId; - final editingAccount = _editingAccountId == null - ? null - : displayAccounts.firstWhereOrNull((a) => a.accountId == _editingAccountId); - - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 0, 14, 20), - child: SizedBox( - height: sheetHeight, - child: Container( - padding: const EdgeInsets.fromLTRB(24, 40, 24, 40), - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - border: Border.all(color: const Color(0xFF3D3D3D)), - borderRadius: BorderRadius.circular(24), - ), - child: _buildContent( - accountsAsync: accountsAsync, - activeDisplayAccountAsync: activeDisplayAccountAsync, - displayAccounts: displayAccounts, - activeAccountId: activeAccountId, - editingAccount: editingAccount, - ), + Button( + variant: ButtonVariant.glassOutline, + label: _walletActionLabel(), + onPressed: _isCreatingAccount ? null : _createNewAccount, ), - ), + + SizedBox(height: context.themeSize.bottomButtonSpacing), + ], ), ); } - Widget _buildContent({ - required AsyncValue> accountsAsync, - required AsyncValue activeDisplayAccountAsync, - required List displayAccounts, - required String? activeAccountId, - required Account? editingAccount, - }) { - if (accountsAsync.isLoading || activeDisplayAccountAsync.isLoading) { - return const Center(child: CircularProgressIndicator(color: Colors.white)); - } + String _walletActionLabel() { + final accounts = ref.watch(accountsProvider).value ?? []; + final grouped = _groupByWallet(accounts); + final selectedWallet = getSelectedWalletIndex(accounts); + final selectedAccounts = grouped[selectedWallet] ?? const []; + return _isHardwareWallet(selectedAccounts) ? 'Add Hardware Account' : 'Add Account'; + } - if (accountsAsync.hasError) { - return Center( - child: Text( - 'Failed to load accounts.', - style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), - ), - ); - } + Widget _buildAccountsList() { + final accountsAsync = ref.watch(accountsProvider); + final activeDisplayAccountAsync = ref.watch(activeAccountProvider); - if (activeDisplayAccountAsync.hasError) { - return Center( + return accountsAsync.when( + loading: () => Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), + error: (error, _) => Center( child: Text( - 'Failed to load active account.', + 'Failed to load accounts: $error', style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), ), - ); - } - - if (_isCreateViewOpen && _draftAccount != null) { - return _buildCreateAccountView(); - } - - if (editingAccount != null) { - return _buildEditAccountView(editingAccount); - } - - return _buildAccountsListView(displayAccounts, activeAccountId); - } + ), + data: (accounts) { + if (accounts.isEmpty) { + return Center( + child: Text('No accounts found.', style: context.themeText.smallParagraph?.copyWith(color: Colors.white70)), + ); + } - Widget _buildAccountsListView(List displayAccounts, String? activeAccountId) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SizedBox( - width: 32.0, - height: 32.0, - child: activeAccountId == null - ? const SizedBox() - : AccountGradientImage(accountId: activeAccountId, width: 32.0, height: 32.0), - ), - const SizedBox(width: 12), - const Text( - 'Accounts', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1, - ), - ), - ], - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white, size: 20), - splashRadius: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: 20, minWidth: 20), - ), - ], - ), - const SizedBox(height: 40), - Expanded( - child: displayAccounts.isEmpty - ? Center( - child: Text( - 'No accounts found.', - style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), - ), - ) - : ListView.separated( - itemCount: displayAccounts.length, - separatorBuilder: (_, _) => const SizedBox(height: 20), - itemBuilder: (_, index) { - final account = displayAccounts[index]; - return _buildAccountRow(account, account.accountId == activeAccountId); + 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: (activeDisplayAccount) { + if (_selectedWalletIndex == null) { + // 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); + }); + } + + final grouped = _groupByWallet(accounts); + if (grouped.length <= 1) { + final walletAccounts = grouped.values.first; + return RefreshIndicator( + onRefresh: _refreshAccounts, + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 16.0), + itemCount: walletAccounts.length, + separatorBuilder: (context, index) => const SizedBox(height: 25), + itemBuilder: (context, index) { + final account = walletAccounts[index]; + return _buildAccountListItem(account, activeDisplayAccount?.account.accountId, index); }, ), - ), - const SizedBox(height: 24), - _buildPrimarySheetButton(label: 'Add Account', isLoading: _isCreatingAccount, onTap: _createNewAccount), - ], - ); - } - - Widget _buildEditAccountView(Account account) { - if (!_isEditingName && _nameController.text != account.name) { - _nameController.text = account.name; - } - - return FutureBuilder( - future: _checksumService.getHumanReadableName(account.accountId), - builder: (context, snapshot) { - final checksum = snapshot.connectionState == ConnectionState.done ? (snapshot.data ?? '-') : 'Loading...'; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: _closeEdit, - icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), - splashRadius: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: 20, minWidth: 20), - ), - const Text( - 'Edit Account', - style: TextStyle( - fontFamily: 'Inter', - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1, + ); + } + + final selectedWallet = _selectedWalletIndex ?? grouped.keys.first; + final children = []; + var sectionIndex = 0; + for (final entry in grouped.entries) { + final walletIndex = entry.key; + final walletAccounts = entry.value; + + if (sectionIndex > 0) children.add(const SizedBox(height: 18)); + children.add( + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text( + _walletLabel(walletIndex, walletAccounts), + style: context.themeText.detail?.copyWith( + color: walletIndex == selectedWallet + ? context.themeColors.textPrimary + : context.themeColors.textMuted, + ), ), ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white, size: 20), - splashRadius: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: 20, minWidth: 20), - ), - ], - ), - const SizedBox(height: 40), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Account Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), - const SizedBox(height: 12), - _buildAccountNameField(account), - const SizedBox(height: 40), - Text('Address Details', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), - const SizedBox(height: 12), - _buildAddressDetails(account, checksum), - ], - ), - ), - ), - const SizedBox(height: 24), - _buildPrimarySheetButton( - label: 'Share Account Details', - onTap: () => shareAccountDetails( - context, - account.accountId, - checksum: checksum != 'Loading...' && checksum != '-' ? checksum : null, - ), - ), - ], + ); + + 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, activeDisplayAccount?.account.accountId, i)); + } + sectionIndex++; + } + + return RefreshIndicator( + onRefresh: _refreshAccounts, + child: ListView(padding: const EdgeInsets.symmetric(vertical: 16.0), children: children), + ); + }, ); }, ); } - Widget _buildCreateAccountView() { - final draft = _draftAccount!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSheetHeader(title: 'New Account', onBack: _closeCreateView), - const SizedBox(height: 40), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Wallet Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), - const SizedBox(height: 12), - _buildCreatedNameField(), - const SizedBox(height: 40), - Text('Wallet Address', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), - const SizedBox(height: 12), - _buildCreateField( - value: AddressFormattingService.formatAddress(draft.accountId), - onCopy: () => context.copyTextWithToaster(draft.accountId), - textStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1.35, - ), - ), - const SizedBox(height: 40), - Text('Wallet Checkphrase', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), - const SizedBox(height: 12), - _buildCreateField( - value: _draftChecksum, - onCopy: () => context.copyTextWithToaster(_draftChecksum), - textStyle: TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w400, - color: context.themeColors.pink, - height: 1.0, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 24), - _buildPrimarySheetButton( - label: 'Create Account', - isLoading: _isSavingCreatedAccount, - onTap: _submitCreatedAccount, - ), - ], - ); - } + Widget _buildAccountListItem(Account account, String? activeAccountId, int index) { + final bool isActive = account.accountId == activeAccountId; + final entrustedAccountsAsync = ref.watch(entrustedAccountsProvider(account)); + final entrustedAccountsData = entrustedAccountsAsync.value ?? []; - Widget _buildAccountRow(Account account, bool isActive) { - final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); - final balanceText = balanceAsync.when( - loading: () => 'Loading...', - error: (_, _) => 'Balance unavailable', - data: (balance) => '${_formattingService.formatBalance(balance)} ${AppConstants.tokenSymbol}', - ); + final entrustedNodes = entrustedAccountsData + .map((entrusted) => TreeNode(data: entrusted)) + .toList(); - return GestureDetector( - onTap: () => _switchAccount(account), - child: Container( - padding: const EdgeInsets.fromLTRB(16, 12, 12, 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final isHighSecurityAsync = ref.watch(isHighSecurityProvider(account)); + final isHighSecurity = isHighSecurityAsync.value ?? false; + + return InkWell( + onTap: () async { + await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); + if (mounted) Navigator.pop(context); + }, + child: Stack( + clipBehavior: Clip.hardEdge, + children: [ + Column( + children: [ + Row( children: [ - Text( - account.name, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 16, - fontWeight: FontWeight.w500, - color: isActive ? context.themeColors.pink : Colors.white, - height: 1.35, + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: context.isTablet ? 20 : 8, vertical: 8), + height: context.themeSize.accountListItemHeight, + decoration: ShapeDecoration( + color: isActive ? context.themeColors.surfaceActive : context.themeColors.surface, + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: context.themeColors.borderLight), + borderRadius: BorderRadius.circular(5), + ), + ), + child: Row( + children: [ + const SizedBox(width: 24), + Expanded( + child: Consumer( + builder: (context, ref, child) { + final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); + + return FutureBuilder( + future: _checksumService.getHumanReadableName(account.accountId), + builder: (context, checksumSnapshot) { + final humanChecksum = checksumSnapshot.data ?? ''; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + account.name, + style: context.themeText.smallParagraph?.copyWith( + color: isActive ? Colors.black : Colors.white, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isHighSecurity) + AccountTag( + text: 'High Security', + color: context.themeColors.accountTagEntrusted, + ), + if (isHighSecurity && entrustedNodes.isNotEmpty) + const SizedBox(width: 6), + if (entrustedNodes.isNotEmpty) + AccountTag( + text: 'Guardian', + color: context.themeColors.accountTagGuardian, + ), + ], + ), + ], + ), + Text( + humanChecksum, + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.checksumDarker + : context.themeColors.checksum, + ), + ), + Row( + children: [ + Text( + context.isTablet + ? account.accountId + // ignore: lines_longer_than_80_chars + : AddressFormattingService.formatAddress(account.accountId), + style: context.themeText.tiny?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textMuted, + ), + ), + ], + ), + const SizedBox(height: 2), + balanceAsync.when( + loading: () => Text( + 'loading balance...', + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.light, + ), + ), + error: (error, _) => Text( + 'error loading', + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + data: (balance) => Text.rich( + TextSpan( + children: [ + TextSpan( + text: _formattingService.formatBalance(balance), + style: context.themeText.detail?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + TextSpan( + text: ' ${AppConstants.tokenSymbol}', + style: context.themeText.tiny?.copyWith( + color: isActive + ? context.themeColors.darkGray + : context.themeColors.textPrimary, + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + }, + ), + ), + ], + ), ), ), - const SizedBox(height: 4), - Text( - balanceText, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 12, - fontWeight: FontWeight.w400, - color: Color(0x80FFFFFF), - height: 1.35, + 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(account.accountId)); + final checksumName = await _checksumService.getHumanReadableName(account.accountId); + + balanceAsync.when( + loading: () { + // Show loading or handle appropriately + }, + error: (error, _) { + // Handle error + }, + data: (balance) async { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: AppConstants.accountSettingsRouteName), + builder: (context) => AccountSettingsScreen( + account: account, + balance: _formattingService.formatBalance(balance, addSymbol: true), + checksumName: checksumName, + isHighSecurity: isHighSecurity, + isEntrustedAccount: false, + ), + ), + ); + // Providers will automatically refresh if needed + }, + ); + }, ), ], ), - ), - const SizedBox(width: 12), - _buildIconActionButton( - size: 40, - radius: 8, - icon: Icons.edit_outlined, - iconSize: 20, - onTap: () => _openEdit(account), - ), - ], - ), - ), - ); - } - - Widget _buildAccountNameField(Account account) { - return Container( - padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _nameController, - readOnly: !_isEditingName || _isSavingName, - style: TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w500, - color: context.themeColors.pink, - height: 1.35, - ), - cursorColor: context.themeColors.pink, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - onSubmitted: (_) { - if (_isEditingName && !_isSavingName) { - _saveEditedName(account); - } - }, - onTap: () { - if (!_isEditingName) { - setState(() => _isEditingName = true); - } - }, - ), - ), - _isSavingName - ? const SizedBox( - width: 40, - height: 40, - child: Center( - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ), - ), - ) - : _buildIconActionButton( - size: 40, - radius: 8, - icon: _isEditingName ? Icons.check : Icons.edit_outlined, - iconSize: 20, - onTap: () { - if (_isEditingName) { - _saveEditedName(account); - } else { - setState(() => _isEditingName = true); - } + if (entrustedNodes.isNotEmpty) + 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), + ], + ), + ), + ), + ), + ), + 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, + ), + ), + ); + }, + ); + }, + ), + ], + ); }, ), - ], - ), - ); - } - - Widget _buildCreatedNameField() { - return Container( - padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _createNameController, - readOnly: !_isEditingCreatedName || _isSavingCreatedAccount, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w400, - color: Colors.white, - ), - cursorColor: Colors.white, - decoration: const InputDecoration( - filled: true, - fillColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - ), + ], ), - _buildIconActionButton( - size: 40, - radius: 8, - icon: _isEditingCreatedName ? Icons.check : Icons.edit_outlined, - iconSize: 20, - onTap: () { - setState(() => _isEditingCreatedName = !_isEditingCreatedName); - }, - ), - ], - ), - ); - } - - Widget _buildCreateField({required String value, required VoidCallback onCopy, required TextStyle textStyle}) { - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), - child: Row( - children: [ - Expanded( - child: Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle), - ), - const SizedBox(width: 8), - _buildIconActionButton(size: 40, radius: 8, icon: Icons.copy_outlined, iconSize: 20, onTap: onCopy), - ], - ), - ); - } - - Widget _buildSheetHeader({required String title, VoidCallback? onBack}) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: 24, - height: 24, - child: onBack == null - ? const SizedBox() - : IconButton( - onPressed: onBack, - icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), - splashRadius: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: 24, minWidth: 24), - ), - ), - Text( - title, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1, - ), - ), - SizedBox( - width: 24, - height: 24, - child: IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white, size: 20), - splashRadius: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: 24, minWidth: 24), - ), - ), - ], - ); - } - - Widget _buildAddressDetails(Account account, String checksum) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), - child: Column( - children: [ - _buildCopyRow( - value: account.accountId, - onCopy: () => context.copyTextWithToaster(account.accountId), - textStyle: const TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1.35, - ), - maxLines: null, - overflow: TextOverflow.visible, - ), - const SizedBox(height: 8), - _buildCopyRow( - value: checksum, - onCopy: () => context.copyTextWithToaster(checksum), - textStyle: TextStyle( - fontFamily: 'Inter', - fontSize: 14, - fontWeight: FontWeight.w400, - color: context.themeColors.pink, - height: 1, + Positioned( + // calculating the middle point + top: (context.themeSize.accountListItemHeight / 2) - (context.themeSize.accountListItemLogoWidth / 2), + left: (context.themeSize.accountListItemLogoWidth / 2) * -1, + child: AccountGradientImage( + accountId: account.accountId, + width: context.themeSize.accountListItemLogoWidth, + height: context.themeSize.accountListItemLogoWidth, ), ), ], ), ); } - - Widget _buildCopyRow({ - required String value, - required VoidCallback onCopy, - required TextStyle textStyle, - int? maxLines = 1, - TextOverflow overflow = TextOverflow.ellipsis, - }) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text(value, maxLines: maxLines, overflow: overflow, style: textStyle), - ), - const SizedBox(width: 8), - _buildIconActionButton(size: 20, radius: 4, icon: Icons.copy_outlined, iconSize: 12, onTap: onCopy), - ], - ); - } - - Widget _buildIconActionButton({ - required double size, - required double radius, - required IconData icon, - required double iconSize, - required VoidCallback onTap, - }) { - return Material( - color: Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(radius), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(radius), - child: SizedBox( - width: size, - height: size, - child: Icon(icon, color: Colors.white, size: iconSize), - ), - ), - ); - } - - Widget _buildPrimarySheetButton({required String label, required VoidCallback onTap, bool isLoading = false}) { - return Material( - color: Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(14), - child: InkWell( - onTap: isLoading ? null : onTap, - borderRadius: BorderRadius.circular(14), - child: Container( - height: 64, - width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), - ), - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ) - : Text( - label, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1, - ), - ), - ), - ), - ); - } - - Future _submitCreatedAccount() async { - final draft = _draftAccount; - if (draft == null) return; - final name = _createNameController.text.trim(); - if (name.isEmpty) { - context.showErrorToaster(message: "Account name can't be empty"); - return; - } - - setState(() => _isSavingCreatedAccount = true); - try { - final accountToSave = draft.copyWith(name: name); - await _accountsService.addAccount(accountToSave); - ref.invalidate(accountsProvider); - ref.invalidate(activeAccountProvider); - if (mounted) { - _closeCreateView(); - } - } catch (_) { - if (mounted) { - context.showErrorToaster(message: 'Failed to create account.'); - } - } finally { - if (mounted) { - setState(() => _isSavingCreatedAccount = false); - } - } - } } diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart new file mode 100644 index 00000000..8a80f926 --- /dev/null +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -0,0 +1,836 @@ +import 'dart:math' as math; + +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/account_gradient_image.dart'; +import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; +import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.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_providers.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; +import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; + +Future showAccountsSheet(BuildContext context) { + return showAppModalBottomSheet(context: context, builder: (_) => const AccountsScreen()); +} + +class AccountsScreen extends ConsumerStatefulWidget { + const AccountsScreen({super.key}); + + @override + ConsumerState createState() => _AccountsScreenState(); +} + +class _AccountsScreenState extends ConsumerState { + final AccountsService _accountsService = AccountsService(); + final NumberFormattingService _formattingService = NumberFormattingService(); + final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _createNameController = TextEditingController(); + + bool _isCreatingAccount = false; + bool _isSavingCreatedAccount = false; + bool _isEditingName = false; + bool _isEditingCreatedName = false; + bool _isSavingName = false; + bool _isCreateViewOpen = false; + String? _editingAccountId; + Account? _draftAccount; + String _draftChecksum = 'Loading...'; + + bool _isHardwareWallet(List accounts) { + return accounts.isNotEmpty && accounts.every((a) => a.accountType == AccountType.keystone); + } + + int _walletIndexForActiveAccount(List accounts, DisplayAccount? activeDisplayAccount) { + if (activeDisplayAccount is RegularAccount) { + return activeDisplayAccount.account.walletIndex; + } + + if (activeDisplayAccount is EntrustedDisplayAccount) { + final parent = accounts.firstWhereOrNull((a) => a.accountId == activeDisplayAccount.account.parentAccountId); + if (parent != null) return parent.walletIndex; + } + + return accounts.isNotEmpty ? accounts.first.walletIndex : 0; + } + + List _displayAccounts(List accounts) { + final sorted = [...accounts]; + sorted.sort((a, b) { + final walletCmp = a.walletIndex.compareTo(b.walletIndex); + if (walletCmp != 0) return walletCmp; + return a.index.compareTo(b.index); + }); + return sorted; + } + + Future _createNewAccount() async { + if (_isCreatingAccount) return; + + setState(() => _isCreatingAccount = true); + try { + final accounts = ref.read(accountsProvider).value ?? []; + final activeDisplayAccount = ref.read(activeAccountProvider).value; + final walletIndex = _walletIndexForActiveAccount(accounts, activeDisplayAccount); + final selectedWalletAccounts = accounts.where((a) => a.walletIndex == walletIndex).toList(); + + if (_isHardwareWallet(selectedWalletAccounts)) { + final created = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => AddHardwareAccountScreen(walletIndex: walletIndex)), + ); + if (created == true) { + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + } + } else { + final draft = await _accountsService.createNewAccount(walletIndex: walletIndex); + final checksum = await _checksumService.getHumanReadableName(draft.accountId); + if (!mounted) return; + _createNameController.text = draft.name; + setState(() { + _draftAccount = draft; + _draftChecksum = checksum; + _isCreateViewOpen = true; + _isEditingCreatedName = false; + }); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Could not add account.'); + } + } finally { + if (mounted) { + setState(() => _isCreatingAccount = false); + } + } + } + + Future _switchAccount(Account account) async { + await ref.read(activeAccountProvider.notifier).setActiveAccount(RegularAccount(account)); + if (mounted) { + Navigator.of(context).pop(); + } + } + + Future _openEdit(Account account) async { + _nameController.text = account.name; + setState(() { + _editingAccountId = account.accountId; + _isEditingName = false; + }); + } + + void _closeEdit() { + setState(() { + _editingAccountId = null; + _isEditingName = false; + _isSavingName = false; + }); + } + + void _closeCreateView() { + setState(() { + _isCreateViewOpen = false; + _isEditingCreatedName = false; + _isSavingCreatedAccount = false; + _draftAccount = null; + _draftChecksum = 'Loading...'; + _createNameController.clear(); + }); + } + + Future _saveEditedName(Account account) async { + final name = _nameController.text.trim(); + if (name.isEmpty) { + context.showErrorToaster(message: "Account name can't be empty"); + return; + } + if (name == account.name) { + setState(() => _isEditingName = false); + return; + } + + setState(() => _isSavingName = true); + try { + await _accountsService.updateAccountName(account, name); + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + if (mounted) { + setState(() { + _isEditingName = false; + }); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Failed to rename account.'); + } + } finally { + if (mounted) { + setState(() => _isSavingName = false); + } + } + } + + @override + void dispose() { + _nameController.dispose(); + _createNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context); + final maxHeight = media.size.height - media.padding.top - 20; + final sheetHeight = math.min(610.0, maxHeight); + + final accountsAsync = ref.watch(accountsProvider); + final activeDisplayAccountAsync = ref.watch(activeAccountProvider); + + final accounts = accountsAsync.value ?? []; + final activeDisplayAccount = activeDisplayAccountAsync.value; + final displayAccounts = _displayAccounts(accounts); + final activeAccountId = activeDisplayAccount?.account.accountId; + final editingAccount = _editingAccountId == null + ? null + : displayAccounts.firstWhereOrNull((a) => a.accountId == _editingAccountId); + + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 0, 14, 20), + child: SizedBox( + height: sheetHeight, + child: Container( + padding: const EdgeInsets.fromLTRB(24, 40, 24, 40), + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + border: Border.all(color: const Color(0xFF3D3D3D)), + borderRadius: BorderRadius.circular(24), + ), + child: _buildContent( + accountsAsync: accountsAsync, + activeDisplayAccountAsync: activeDisplayAccountAsync, + displayAccounts: displayAccounts, + activeAccountId: activeAccountId, + editingAccount: editingAccount, + ), + ), + ), + ), + ); + } + + Widget _buildContent({ + required AsyncValue> accountsAsync, + required AsyncValue activeDisplayAccountAsync, + required List displayAccounts, + required String? activeAccountId, + required Account? editingAccount, + }) { + if (accountsAsync.isLoading || activeDisplayAccountAsync.isLoading) { + return const Center(child: CircularProgressIndicator(color: Colors.white)); + } + + if (accountsAsync.hasError) { + return Center( + child: Text( + 'Failed to load accounts.', + style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), + ), + ); + } + + if (activeDisplayAccountAsync.hasError) { + return Center( + child: Text( + 'Failed to load active account.', + style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), + ), + ); + } + + if (_isCreateViewOpen && _draftAccount != null) { + return _buildCreateAccountView(); + } + + if (editingAccount != null) { + return _buildEditAccountView(editingAccount); + } + + return _buildAccountsListView(displayAccounts, activeAccountId); + } + + Widget _buildAccountsListView(List displayAccounts, String? activeAccountId) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + width: 32.0, + height: 32.0, + child: activeAccountId == null + ? const SizedBox() + : AccountGradientImage(accountId: activeAccountId, width: 32.0, height: 32.0), + ), + const SizedBox(width: 12), + const Text( + 'Accounts', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + ], + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + ], + ), + const SizedBox(height: 40), + Expanded( + child: displayAccounts.isEmpty + ? Center( + child: Text( + 'No accounts found.', + style: context.themeText.smallParagraph?.copyWith(color: Colors.white70), + ), + ) + : ListView.separated( + itemCount: displayAccounts.length, + separatorBuilder: (_, _) => const SizedBox(height: 20), + itemBuilder: (_, index) { + final account = displayAccounts[index]; + return _buildAccountRow(account, account.accountId == activeAccountId); + }, + ), + ), + const SizedBox(height: 24), + _buildPrimarySheetButton(label: 'Add Account', isLoading: _isCreatingAccount, onTap: _createNewAccount), + ], + ); + } + + Widget _buildEditAccountView(Account account) { + if (!_isEditingName && _nameController.text != account.name) { + _nameController.text = account.name; + } + + return FutureBuilder( + future: _checksumService.getHumanReadableName(account.accountId), + builder: (context, snapshot) { + final checksum = snapshot.connectionState == ConnectionState.done ? (snapshot.data ?? '-') : 'Loading...'; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: _closeEdit, + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + const Text( + 'Edit Account', + style: TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 20, minWidth: 20), + ), + ], + ), + const SizedBox(height: 40), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Account Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildAccountNameField(account), + const SizedBox(height: 40), + Text('Address Details', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildAddressDetails(account, checksum), + ], + ), + ), + ), + const SizedBox(height: 24), + _buildPrimarySheetButton( + label: 'Share Account Details', + onTap: () => shareAccountDetails( + context, + account.accountId, + checksum: checksum != 'Loading...' && checksum != '-' ? checksum : null, + ), + ), + ], + ); + }, + ); + } + + Widget _buildCreateAccountView() { + final draft = _draftAccount!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSheetHeader(title: 'New Account', onBack: _closeCreateView), + const SizedBox(height: 40), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Wallet Name', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreatedNameField(), + const SizedBox(height: 40), + Text('Wallet Address', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreateField( + value: AddressFormattingService.formatAddress(draft.accountId), + onCopy: () => context.copyTextWithToaster(draft.accountId), + textStyle: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1.35, + ), + ), + const SizedBox(height: 40), + Text('Wallet Checkphrase', style: context.themeText.smallParagraph?.copyWith(color: Colors.white)), + const SizedBox(height: 12), + _buildCreateField( + value: _draftChecksum, + onCopy: () => context.copyTextWithToaster(_draftChecksum), + textStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: context.themeColors.pink, + height: 1.0, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + _buildPrimarySheetButton( + label: 'Create Account', + isLoading: _isSavingCreatedAccount, + onTap: _submitCreatedAccount, + ), + ], + ); + } + + Widget _buildAccountRow(Account account, bool isActive) { + final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); + final balanceText = balanceAsync.when( + loading: () => 'Loading...', + error: (_, _) => 'Balance unavailable', + data: (balance) => '${_formattingService.formatBalance(balance)} ${AppConstants.tokenSymbol}', + ); + + return GestureDetector( + onTap: () => _switchAccount(account), + child: Container( + padding: const EdgeInsets.fromLTRB(16, 12, 12, 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + account.name, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + color: isActive ? context.themeColors.pink : Colors.white, + height: 1.35, + ), + ), + const SizedBox(height: 4), + Text( + balanceText, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 12, + fontWeight: FontWeight.w400, + color: Color(0x80FFFFFF), + height: 1.35, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + _buildIconActionButton( + size: 40, + radius: 8, + icon: Icons.edit_outlined, + iconSize: 20, + onTap: () => _openEdit(account), + ), + ], + ), + ), + ); + } + + Widget _buildAccountNameField(Account account) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _nameController, + readOnly: !_isEditingName || _isSavingName, + style: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: context.themeColors.pink, + height: 1.35, + ), + cursorColor: context.themeColors.pink, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onSubmitted: (_) { + if (_isEditingName && !_isSavingName) { + _saveEditedName(account); + } + }, + onTap: () { + if (!_isEditingName) { + setState(() => _isEditingName = true); + } + }, + ), + ), + _isSavingName + ? const SizedBox( + width: 40, + height: 40, + child: Center( + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ), + ), + ) + : _buildIconActionButton( + size: 40, + radius: 8, + icon: _isEditingName ? Icons.check : Icons.edit_outlined, + iconSize: 20, + onTap: () { + if (_isEditingName) { + _saveEditedName(account); + } else { + setState(() => _isEditingName = true); + } + }, + ), + ], + ), + ); + } + + Widget _buildCreatedNameField() { + return Container( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _createNameController, + readOnly: !_isEditingCreatedName || _isSavingCreatedAccount, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.white, + ), + cursorColor: Colors.white, + decoration: const InputDecoration( + filled: true, + fillColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + _buildIconActionButton( + size: 40, + radius: 8, + icon: _isEditingCreatedName ? Icons.check : Icons.edit_outlined, + iconSize: 20, + onTap: () { + setState(() => _isEditingCreatedName = !_isEditingCreatedName); + }, + ), + ], + ), + ); + } + + Widget _buildCreateField({required String value, required VoidCallback onCopy, required TextStyle textStyle}) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + child: Row( + children: [ + Expanded( + child: Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle), + ), + const SizedBox(width: 8), + _buildIconActionButton(size: 40, radius: 8, icon: Icons.copy_outlined, iconSize: 20, onTap: onCopy), + ], + ), + ); + } + + Widget _buildSheetHeader({required String title, VoidCallback? onBack}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 24, + height: 24, + child: onBack == null + ? const SizedBox() + : IconButton( + onPressed: onBack, + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 24, minWidth: 24), + ), + ), + Text( + title, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white, size: 20), + splashRadius: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minHeight: 24, minWidth: 24), + ), + ), + ], + ); + } + + Widget _buildAddressDetails(Account account, String checksum) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + child: Column( + children: [ + _buildCopyRow( + value: account.accountId, + onCopy: () => context.copyTextWithToaster(account.accountId), + textStyle: const TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1.35, + ), + maxLines: null, + overflow: TextOverflow.visible, + ), + const SizedBox(height: 8), + _buildCopyRow( + value: checksum, + onCopy: () => context.copyTextWithToaster(checksum), + textStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 14, + fontWeight: FontWeight.w400, + color: context.themeColors.pink, + height: 1, + ), + ), + ], + ), + ); + } + + Widget _buildCopyRow({ + required String value, + required VoidCallback onCopy, + required TextStyle textStyle, + int? maxLines = 1, + TextOverflow overflow = TextOverflow.ellipsis, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text(value, maxLines: maxLines, overflow: overflow, style: textStyle), + ), + const SizedBox(width: 8), + _buildIconActionButton(size: 20, radius: 4, icon: Icons.copy_outlined, iconSize: 12, onTap: onCopy), + ], + ); + } + + Widget _buildIconActionButton({ + required double size, + required double radius, + required IconData icon, + required double iconSize, + required VoidCallback onTap, + }) { + return Material( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(radius), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: size, + height: size, + child: Icon(icon, color: Colors.white, size: iconSize), + ), + ), + ); + } + + Widget _buildPrimarySheetButton({required String label, required VoidCallback onTap, bool isLoading = false}) { + return Material( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: isLoading ? null : onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + height: 64, + width: double.infinity, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + ), + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : Text( + label, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), + ), + ), + ); + } + + Future _submitCreatedAccount() async { + final draft = _draftAccount; + if (draft == null) return; + final name = _createNameController.text.trim(); + if (name.isEmpty) { + context.showErrorToaster(message: "Account name can't be empty"); + return; + } + + setState(() => _isSavingCreatedAccount = true); + try { + final accountToSave = draft.copyWith(name: name); + await _accountsService.addAccount(accountToSave); + ref.invalidate(accountsProvider); + ref.invalidate(activeAccountProvider); + if (mounted) { + _closeCreateView(); + } + } catch (_) { + if (mounted) { + context.showErrorToaster(message: 'Failed to create account.'); + } + } finally { + if (mounted) { + setState(() => _isSavingCreatedAccount = false); + } + } + } +} From 89306706352c15c21c4e14b1488e6272697829b2 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 22:10:32 +0800 Subject: [PATCH 05/20] format --- mobile-app/lib/shared/utils/share_utils.dart | 5 +---- quantus_sdk/lib/src/services/accounts_service.dart | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/mobile-app/lib/shared/utils/share_utils.dart b/mobile-app/lib/shared/utils/share_utils.dart index 276d2edf..a9a0aaf4 100644 --- a/mobile-app/lib/shared/utils/share_utils.dart +++ b/mobile-app/lib/shared/utils/share_utils.dart @@ -3,10 +3,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:share_plus/share_plus.dart'; String buildAccountShareText(String accountId, {String? checksum}) { - final checkphrasePart = checksum != null && - checksum.isNotEmpty && - checksum != 'Loading...' && - checksum != '-' + final checkphrasePart = checksum != null && checksum.isNotEmpty && checksum != 'Loading...' && checksum != '-' ? '\n\nCheckphrase:$checksum' : ''; return 'Hey! These are my Quantus account details:\n\nAddress:\n$accountId$checkphrasePart\n\nTo open in the app or to download click the link below:\n${AppConstants.websiteBaseUrl}/account?id=$accountId'; diff --git a/quantus_sdk/lib/src/services/accounts_service.dart b/quantus_sdk/lib/src/services/accounts_service.dart index 3a91e82d..10214cd2 100644 --- a/quantus_sdk/lib/src/services/accounts_service.dart +++ b/quantus_sdk/lib/src/services/accounts_service.dart @@ -23,9 +23,7 @@ class AccountsService { if (mnemonic == null) { throw Exception('Mnemonic not found. Cannot create new account.'); } - final nextIndex = await _settingsService.getNextFreeAccountIndex( - walletIndex, - ); + final nextIndex = await _settingsService.getNextFreeAccountIndex(walletIndex); final keypair = HdWalletService().keyPairAtIndex(mnemonic, nextIndex); final newAccount = Account( walletIndex: walletIndex, @@ -43,9 +41,7 @@ class AccountsService { } final accounts = await _settingsService.getAccounts(); - final currentAccountIndex = accounts.indexWhere( - (a) => a.accountId == account.accountId, - ); + final currentAccountIndex = accounts.indexWhere((a) => a.accountId == account.accountId); if (currentAccountIndex == -1) { throw Exception('Account not found'); } From 2a4e31c0359deb6d6792d32045d90cb79810ac34 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 22:42:44 +0800 Subject: [PATCH 06/20] rename to accounts sheet --- .../lib/features/main/screens/settings_screen.dart | 2 +- .../main/screens/wallet_main/wallet_main.dart | 2 +- mobile-app/lib/v2/screens/home/accounts_sheet.dart | 12 ++++++------ mobile-app/lib/v2/screens/home/home_screen.dart | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mobile-app/lib/features/main/screens/settings_screen.dart b/mobile-app/lib/features/main/screens/settings_screen.dart index fab4af2c..d83680f2 100644 --- a/mobile-app/lib/features/main/screens/settings_screen.dart +++ b/mobile-app/lib/features/main/screens/settings_screen.dart @@ -8,7 +8,7 @@ import 'package:resonance_network_wallet/features/components/reset_confirmation_ import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; import 'package:resonance_network_wallet/features/components/sphere.dart'; 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/v2/screens/home/accounts_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/authentication_settings_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/notifications_settings_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/select_wallet_for_recovery_phrase_screen.dart'; 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 2e8da690..9d087765 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 @@ -7,7 +7,7 @@ import 'package:resonance_network_wallet/features/components/shared_address_acti import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/features/components/sphere.dart'; 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/v2/screens/home/accounts_sheet.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'; diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index 8a80f926..3e4eb69f 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -16,17 +16,17 @@ import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions. import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; Future showAccountsSheet(BuildContext context) { - return showAppModalBottomSheet(context: context, builder: (_) => const AccountsScreen()); + return showAppModalBottomSheet(context: context, builder: (_) => const AccountsSheet()); } -class AccountsScreen extends ConsumerStatefulWidget { - const AccountsScreen({super.key}); +class AccountsSheet extends ConsumerStatefulWidget { + const AccountsSheet({super.key}); @override - ConsumerState createState() => _AccountsScreenState(); + ConsumerState createState() => _AccountsScreenState(); } -class _AccountsScreenState extends ConsumerState { +class _AccountsScreenState extends ConsumerState { final AccountsService _accountsService = AccountsService(); final NumberFormattingService _formattingService = NumberFormattingService(); final HumanReadableChecksumService _checksumService = HumanReadableChecksumService(); @@ -452,7 +452,7 @@ class _AccountsScreenState extends ConsumerState { ), const SizedBox(height: 24), _buildPrimarySheetButton( - label: 'Create Account', + label: 'Create Account', isLoading: _isSavingCreatedAccount, onTap: _submitCreatedAccount, ), diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 82079678..af952227 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -6,7 +6,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; -import 'package:resonance_network_wallet/features/main/screens/accounts_screen.dart'; +import 'package:resonance_network_wallet/v2/screens/home/accounts_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_screen.dart'; From f74c45d3e69d22d3bd270b74a8c16e1ef8b85349 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 22:49:14 +0800 Subject: [PATCH 07/20] color cleanup --- .../lib/v2/screens/home/accounts_sheet.dart | 36 +++++++++---------- mobile-app/lib/v2/theme/app_colors.dart | 12 +++++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index 3e4eb69f..3c9dfc9c 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -8,7 +8,7 @@ import 'package:resonance_network_wallet/features/components/account_gradient_im import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; -import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.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/wallet_providers.dart'; @@ -211,8 +211,8 @@ class _AccountsScreenState extends ConsumerState { child: Container( padding: const EdgeInsets.fromLTRB(24, 40, 24, 40), decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - border: Border.all(color: const Color(0xFF3D3D3D)), + color: context.colors.sheetBackground, + border: Border.all(color: context.colors.toasterBorder), borderRadius: BorderRadius.circular(24), ), child: _buildContent( @@ -442,7 +442,7 @@ class _AccountsScreenState extends ConsumerState { fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, - color: context.themeColors.pink, + color: context.colors.accentPink, height: 1.0, ), ), @@ -474,7 +474,7 @@ class _AccountsScreenState extends ConsumerState { padding: const EdgeInsets.fromLTRB(16, 12, 12, 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + border: Border.all(color: context.colors.borderSubtle, width: 0.9), ), child: Row( children: [ @@ -488,18 +488,18 @@ class _AccountsScreenState extends ConsumerState { fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, - color: isActive ? context.themeColors.pink : Colors.white, + color: isActive ? context.colors.accentPink : Colors.white, height: 1.35, ), ), const SizedBox(height: 4), Text( balanceText, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w400, - color: Color(0x80FFFFFF), + color: context.colors.textSecondary, height: 1.35, ), ), @@ -523,7 +523,7 @@ class _AccountsScreenState extends ConsumerState { Widget _buildAccountNameField(Account account) { return Container( padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + decoration: BoxDecoration(color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(14)), child: Row( children: [ Expanded( @@ -534,10 +534,10 @@ class _AccountsScreenState extends ConsumerState { fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w500, - color: context.themeColors.pink, + color: context.colors.accentPink, height: 1.35, ), - cursorColor: context.themeColors.pink, + cursorColor: context.colors.accentPink, decoration: const InputDecoration( filled: true, fillColor: Colors.transparent, @@ -593,7 +593,7 @@ class _AccountsScreenState extends ConsumerState { Widget _buildCreatedNameField() { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration(color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(8)), child: Row( children: [ Expanded( @@ -637,7 +637,7 @@ class _AccountsScreenState extends ConsumerState { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration(color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(8)), child: Row( children: [ Expanded( @@ -695,7 +695,7 @@ class _AccountsScreenState extends ConsumerState { Widget _buildAddressDetails(Account account, String checksum) { return Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)), + decoration: BoxDecoration(color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(14)), child: Column( children: [ _buildCopyRow( @@ -719,7 +719,7 @@ class _AccountsScreenState extends ConsumerState { fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, - color: context.themeColors.pink, + color: context.colors.accentPink, height: 1, ), ), @@ -755,7 +755,7 @@ class _AccountsScreenState extends ConsumerState { required VoidCallback onTap, }) { return Material( - color: Colors.white.withValues(alpha: 0.1), + color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(radius), child: InkWell( onTap: onTap, @@ -771,7 +771,7 @@ class _AccountsScreenState extends ConsumerState { Widget _buildPrimarySheetButton({required String label, required VoidCallback onTap, bool isLoading = false}) { return Material( - color: Colors.white.withValues(alpha: 0.1), + color: context.colors.surfaceGlass, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: isLoading ? null : onTap, @@ -782,7 +782,7 @@ class _AccountsScreenState extends ConsumerState { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0x70FFFFFF), width: 0.9), + border: Border.all(color: context.colors.borderSubtle, width: 0.9), ), child: isLoading ? const SizedBox( diff --git a/mobile-app/lib/v2/theme/app_colors.dart b/mobile-app/lib/v2/theme/app_colors.dart index a81e5a57..54c77b27 100644 --- a/mobile-app/lib/v2/theme/app_colors.dart +++ b/mobile-app/lib/v2/theme/app_colors.dart @@ -40,6 +40,8 @@ class AppColorsV2 extends ThemeExtension { final Color skeletonBase; final Color skeletonHighlight; final Color toasterBorder; + final Color sheetBackground; + final Color borderSubtle; // Account tags final Color tagGuardian; @@ -51,6 +53,8 @@ class AppColorsV2 extends ThemeExtension { required this.backgroundAlt, required this.toasterBackground, required this.toasterBorder, + required this.sheetBackground, + required this.borderSubtle, required this.surface, required this.surfaceGlass, required this.surfaceCard, @@ -83,6 +87,8 @@ class AppColorsV2 extends ThemeExtension { backgroundAlt: const Color(0xFF1F1F1F), toasterBackground: const Color(0xFF191919), toasterBorder: const Color(0xFF3D3D3D), + sheetBackground: const Color(0xFF1A1A1A), + borderSubtle: const Color(0x70FFFFFF), surface: const Color(0xFF292929), surfaceGlass: const Color(0x1AFFFFFF), surfaceCard: const Color(0x0FFFFFFF), @@ -133,6 +139,8 @@ class AppColorsV2 extends ThemeExtension { Color? border, Color? toasterBackground, Color? toasterBorder, + Color? sheetBackground, + Color? borderSubtle, Color? buttonDisabled, Color? skeletonBase, Color? skeletonHighlight, @@ -145,6 +153,8 @@ class AppColorsV2 extends ThemeExtension { backgroundAlt: backgroundAlt ?? this.backgroundAlt, toasterBackground: toasterBackground ?? this.toasterBackground, toasterBorder: toasterBorder ?? this.toasterBorder, + sheetBackground: sheetBackground ?? this.sheetBackground, + borderSubtle: borderSubtle ?? this.borderSubtle, surface: surface ?? this.surface, surfaceGlass: surfaceGlass ?? this.surfaceGlass, surfaceCard: surfaceCard ?? this.surfaceCard, @@ -180,6 +190,8 @@ class AppColorsV2 extends ThemeExtension { backgroundAlt: Color.lerp(backgroundAlt, other.backgroundAlt, t) ?? backgroundAlt, toasterBackground: Color.lerp(toasterBackground, other.toasterBackground, t) ?? toasterBackground, toasterBorder: Color.lerp(toasterBorder, other.toasterBorder, t) ?? toasterBorder, + sheetBackground: Color.lerp(sheetBackground, other.sheetBackground, t) ?? sheetBackground, + borderSubtle: Color.lerp(borderSubtle, other.borderSubtle, t) ?? borderSubtle, surface: Color.lerp(surface, other.surface, t) ?? surface, surfaceGlass: Color.lerp(surfaceGlass, other.surfaceGlass, t) ?? surfaceGlass, surfaceCard: Color.lerp(surfaceCard, other.surfaceCard, t) ?? surfaceCard, From 7aafb36f680900905def507bde86c6eefda3bad2 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:25:50 +0800 Subject: [PATCH 08/20] accounts screen buttons --- .../assets/v2/glass_small_button_40.png | Bin 878 -> 0 bytes mobile-app/assets/v2/glass_tiny_button.png | Bin 439 -> 1056 bytes .../lib/v2/components/glass_container.dart | 5 +- .../lib/v2/screens/home/accounts_sheet.dart | 84 +++++++----------- mobile-app/pubspec.yaml | 1 + 5 files changed, 37 insertions(+), 53 deletions(-) delete mode 100644 mobile-app/assets/v2/glass_small_button_40.png diff --git a/mobile-app/assets/v2/glass_small_button_40.png b/mobile-app/assets/v2/glass_small_button_40.png deleted file mode 100644 index 3ce7c65277a9a323bb6549f541ec32b53d8520ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 878 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q{^b&H|6fVg?393lL^>oo1K-6l5$8 za(7}_cTVOdki(Mh=D=+k@y#W@*NvQ`Pj3;+ zv`vltmh*l~d4tH;Ti184H$I%+alyFS{mr???8M{8TI4Rp$6sEz(3Dqnz=U&}5yZk3Dp}+d+^!3NC%$p@1UvKlN=KAb1H}B|AM+#1r z<=u>C7TzAd&F$RmxGy_C&Yia7uc4>=%eLMFTlqs?OxIs|SKC+k&)pvmSKTjuTK4j& zmsL4?+diqKZBw-hr1H<*{kPk%Tsr)fuulx{)3r}tO)sj_=ha%Jsi!_;xnmTNX717@h?|HT4lB)aUUv1Z#nIwDpQ@fTyuugK6Uw*~c4F~@#pDKR5c>lDO ziA!H*URt{`^n0Mm@16f1xjitsSKhYoXYBo1-o59W|Mv2SS+6Mk^yp6R9_5*Df304m zp;k9j!tZ9u-#cq>OIlPs@!nsP{W(r1WX{g=%PW~1!doXyq`njxgN@xNA Dn-7TY diff --git a/mobile-app/assets/v2/glass_tiny_button.png b/mobile-app/assets/v2/glass_tiny_button.png index 027ba8d0893adddc78f133d20e0153916b5d1664..f0bbe660388caabecd0361bc4e0d2e234b97b642 100644 GIT binary patch delta 985 zcmV;~119{p1E2_yReuAuNkl^$CEf@mU;qhNU}IupWn*Px;Xm+K zSot3yu`qyy*jW)0kEIXw{qCpZPp;B5iJdeJ#2l$4jh)1&=U)5TTxYhow|nrx{QNx5 z&d#v2vx9E8i)OQlN~I#oGMNliP}Py;UiEa!h-$Tpudgq>zkk2uiJ(hOS0!oq?$N;f3>gsH3~ zTPPG@3h0(Jb$_X}#Xs~}n(@uejZtJJNP1#oLYxc27Z(?}ySo#;or$1XFD@>Mb8BN` z1N-~?cz%9{$(Wv=hGki>j*pMU(8I3WWYDCSmX=^092|&o)^yOUS65eIQECPiCPM}< z%gf8Kip3%tjRs7GGg4b$Ux&qLgyWQnaFXTul)5_X%i z6IPh%R~n$=W0b;vek;$7HEF`ntc@Y;M{vl8VF1GPL#ZdM@>_W{(uadgSt8ZLl8)aR zcn3nuw5hD)w_fE&_^_W?zCMhUA;=n4|CS+At+1-2@(eB0XvttNx!@wRFO`MLZS$#EOTEvIrv= z0D^|6Uq3yJyKVMwAe&?xZ2S;LB^l*@-jg`tq%F$*zC;P@^rquRB0d~;m13Ol9J|6{ z*PNo((|yj^2uC5Bh)ItlTQR6~e0O_Y9TF zUVqBE6?UGltgL_uwDdt7z29eNo7fve*5t7&@Uf|w5T$DnqBxf$&E@r=s657c6WDi zcz7t3b&@59LQ5*mbYha`{vT$NQ+cP4$5KT;Dp6-W9vM`yy&QVjL2-8)H>^lA9=I=P zD;~-6L2GMk;==@M_U1{NvZ|czp&+zO#uQIaPpH@HzV-hByQ}!E=cJj?00000NkvXX Hu0mjfJDv4( delta 363 zcmZ3$v7LEBXg#Bkr;B4q1>@V>2lJX7L|PwOM{Lzq{rETjl8g7&wHABX({;9;h&ngN zey3xno?T$UL6+GX$0B|%`xg0I=aNm*#pk!5cW^4sdAHY~XP@;k_4_$BVP@y~?0y^m zu-}k;zvl9dx%|m)bKf@QwOi*0^s#O+T-R{#xq4`wfyJgA)4bL7pC`-BsoUIhBW7~X zb>%Z3X3yn1=lfsbWPHzlQN7I*?4vEOE^WNdpucvG7=!WrHUGA)ShIm8%|GEAu>`}r zHFM4_ZS-fTkFu0y$Xh$7FO-9E`>A@#dilJ!pCVJLvZqf~TyyZ6KJ$x%6Q0X1c{}Oj zcdIwn_j-kEH*adKl;q!cZ1>4jo9p4nIz%%Z_TREO>{&5;|FOer{Yg#9wk+>v#ou}? j_f?PgU+|Wfr;Km$bLWN^y?@8d!2krFu6{1-oD!M asset == tinyAsset ? 4 : asset == smallAsset ? 8 : 14; + const GlassContainer({ super.key, required this.child, @@ -44,7 +47,7 @@ class GlassContainer extends StatelessWidget { return GestureDetector( onTap: onTap, child: ClipRRect( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(defaultRadius), child: SizedBox( height: defaultHeight, child: Stack( diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index 3c9dfc9c..778f0842 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -8,6 +8,7 @@ import 'package:resonance_network_wallet/features/components/account_gradient_im import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; +import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; @@ -508,8 +509,6 @@ class _AccountsScreenState extends ConsumerState { ), const SizedBox(width: 12), _buildIconActionButton( - size: 40, - radius: 8, icon: Icons.edit_outlined, iconSize: 20, onTap: () => _openEdit(account), @@ -573,8 +572,6 @@ class _AccountsScreenState extends ConsumerState { ), ) : _buildIconActionButton( - size: 40, - radius: 8, icon: _isEditingName ? Icons.check : Icons.edit_outlined, iconSize: 20, onTap: () { @@ -620,8 +617,6 @@ class _AccountsScreenState extends ConsumerState { ), ), _buildIconActionButton( - size: 40, - radius: 8, icon: _isEditingCreatedName ? Icons.check : Icons.edit_outlined, iconSize: 20, onTap: () { @@ -644,7 +639,7 @@ class _AccountsScreenState extends ConsumerState { child: Text(value, maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle), ), const SizedBox(width: 8), - _buildIconActionButton(size: 40, radius: 8, icon: Icons.copy_outlined, iconSize: 20, onTap: onCopy), + _buildIconActionButton(icon: Icons.copy_outlined, iconSize: 20, onTap: onCopy), ], ), ); @@ -742,66 +737,51 @@ class _AccountsScreenState extends ConsumerState { child: Text(value, maxLines: maxLines, overflow: overflow, style: textStyle), ), const SizedBox(width: 8), - _buildIconActionButton(size: 20, radius: 4, icon: Icons.copy_outlined, iconSize: 12, onTap: onCopy), + _buildIconActionButton(icon: Icons.copy_outlined, isTiny: true, iconSize: 12, onTap: onCopy), ], ); } Widget _buildIconActionButton({ - required double size, - required double radius, required IconData icon, required double iconSize, required VoidCallback onTap, + bool isTiny = false, }) { - return Material( - color: context.colors.surfaceGlass, - borderRadius: BorderRadius.circular(radius), - child: InkWell( + final double size = isTiny ? 20 : 40; + final asset = isTiny ? GlassContainer.tinyAsset : GlassContainer.smallAsset; + return SizedBox( + width: size, + height: size, + child: GlassContainer( + asset: asset, onTap: onTap, - borderRadius: BorderRadius.circular(radius), - child: SizedBox( - width: size, - height: size, - child: Icon(icon, color: Colors.white, size: iconSize), - ), + child: Icon(icon, color: Colors.white, size: iconSize), ), ); } Widget _buildPrimarySheetButton({required String label, required VoidCallback onTap, bool isLoading = false}) { - return Material( - color: context.colors.surfaceGlass, - borderRadius: BorderRadius.circular(14), - child: InkWell( - onTap: isLoading ? null : onTap, - borderRadius: BorderRadius.circular(14), - child: Container( - height: 64, - width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: context.colors.borderSubtle, width: 0.9), - ), - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ) - : Text( - label, - style: const TextStyle( - fontFamily: 'Inter', - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.white, - height: 1, - ), - ), - ), - ), + return GlassContainer( + asset: GlassContainer.wideAsset, + filled: true, + onTap: isLoading ? null : onTap, + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : Text( + label, + style: const TextStyle( + fontFamily: 'Inter', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, + height: 1, + ), + ), ); } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 45e86d73..9e048be6 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -127,6 +127,7 @@ flutter: - assets/v2/auth_wrapper_bracket.png - assets/v2/glass_wide_clear.png - assets/v2/welcome_screen_bg_image.jpg + - assets/v2/glass_tiny_button.png fonts: From 9a12d485cdaf9c0fa240d5f5d582b69c067462e7 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:29:08 +0800 Subject: [PATCH 09/20] format --- mobile-app/lib/v2/components/glass_container.dart | 6 +++++- mobile-app/lib/v2/screens/home/accounts_sheet.dart | 12 ++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/mobile-app/lib/v2/components/glass_container.dart b/mobile-app/lib/v2/components/glass_container.dart index 1a9adcb9..e1f0e4ad 100644 --- a/mobile-app/lib/v2/components/glass_container.dart +++ b/mobile-app/lib/v2/components/glass_container.dart @@ -30,7 +30,11 @@ class GlassContainer extends StatelessWidget { ? 36 : 56; - double get defaultRadius => asset == tinyAsset ? 4 : asset == smallAsset ? 8 : 14; + double get defaultRadius => asset == tinyAsset + ? 4 + : asset == smallAsset + ? 8 + : 14; const GlassContainer({ super.key, diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index 778f0842..b72c891f 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -508,11 +508,7 @@ class _AccountsScreenState extends ConsumerState { ), ), const SizedBox(width: 12), - _buildIconActionButton( - icon: Icons.edit_outlined, - iconSize: 20, - onTap: () => _openEdit(account), - ), + _buildIconActionButton(icon: Icons.edit_outlined, iconSize: 20, onTap: () => _openEdit(account)), ], ), ), @@ -767,11 +763,7 @@ class _AccountsScreenState extends ConsumerState { filled: true, onTap: isLoading ? null : onTap, child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ) + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text( label, style: const TextStyle( From ba7df5b24b7ac6ef27ccba513c94746d889b82eb Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:42:14 +0800 Subject: [PATCH 10/20] reversible coming soon --- .../v2/screens/settings/settings_screen.dart | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 9a26d5e1..d3df0efa 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -202,14 +202,15 @@ class _SettingsScreenV2State extends ConsumerState { ]), const SizedBox(height: 40), _section('Reversible Transactions', colors, text, [ - _toggleItem( - 'Reversible Transactions', - 'Coming Soon', //_reversibleEnabled ? 'Enabled' : 'Disabled', - _reversibleEnabled, - null, - colors, - text, - ), + _comingSoonItem('Reversible Transactions', null, colors, text), + // _toggleItem( + // 'Reversible Transactions', + // 'Coming Soon', //_reversibleEnabled ? 'Enabled' : 'Disabled', + // _reversibleEnabled, + // null, + // colors, + // text, + // ), _divider(colors), _chevronItem('Time Limit', _timeLimitLabel(), colors, text, onTap: () {}), _divider(colors), @@ -285,13 +286,13 @@ class _SettingsScreenV2State extends ConsumerState { ); } - Column _itemContent(String title, AppTextTheme text, AppColorsV2 colors, String subtitle) { + Column _itemContent(String title, AppTextTheme text, AppColorsV2 colors, String? subtitle) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: text.paragraph?.copyWith(color: colors.textPrimary)), - const SizedBox(height: 4), - Text(subtitle, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + if (subtitle != null) const SizedBox(height: 4), + if (subtitle != null) Text(subtitle, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), ], ); } @@ -354,7 +355,7 @@ class _SettingsScreenV2State extends ConsumerState { ); } - Widget _comingSoonItem(String title, String subtitle, AppColorsV2 colors, AppTextTheme text) { + Widget _comingSoonItem(String title, String? subtitle, AppColorsV2 colors, AppTextTheme text) { return Row( children: [ Expanded(child: _itemContent(title, text, colors, subtitle)), From 85d4914aef1417760a4f036566f011ed6df0cb09 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:42:26 +0800 Subject: [PATCH 11/20] hide balance --- mobile-app/lib/v2/screens/home/home_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index af952227..5f28e255 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -206,10 +206,10 @@ class _HomeScreenState extends ConsumerState { ), error: (_, _) => Text('Error loading balance', style: text.detail?.copyWith(color: colors.textError)), ), - if (!isBalanceHidden) ...[ - const SizedBox(height: 6), - Text('≈ \$0.00', style: text.paragraph?.copyWith(color: colors.textSecondary)), - ], + // if (!isBalanceHidden) ...[ + // const SizedBox(height: 6), + // Text('≈ \$0.00', style: text.paragraph?.copyWith(color: colors.textSecondary)), + // ], ], ), ); From d47361c781d08f59533d4ba4b55bdcbb0601baa3 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:42:44 +0800 Subject: [PATCH 12/20] explorer button is a glass button --- .../v2/screens/activity/transaction_detail_sheet.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index c24f2f6b..3166dbff 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -4,6 +4,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; +import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -178,12 +179,9 @@ class _TransactionDetailSheetState extends State<_TransactionDetailSheet> { Widget _explorerButton(AppColorsV2 colors, AppTextTheme text) { return GestureDetector( onTap: _openExplorer, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.white.withValues(alpha: 0.44)), - ), + child: GlassContainer( + asset: GlassContainer.wideAsset, + filled: false, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ From 0b55cb3c4cb5062a969d98c55d1b86f24a686993 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:42:57 +0800 Subject: [PATCH 13/20] hide info button --- mobile-app/lib/v2/screens/activity/activity_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index 5e500c15..4106683f 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -38,7 +38,8 @@ class ActivityScreen extends ConsumerWidget { children: [ const AppBackButton(), Text('Activity', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), - Icon(Icons.info_outline, color: colors.textPrimary, size: 24), + const SizedBox(width: 24), // empty filler + // Icon(Icons.info_outline, color: colors.textPrimary, size: 24), ], ), const SizedBox(height: 48), From 2573947a3ac0de13f6ad30eeb1f37bfeb711eb62 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Feb 2026 23:45:08 +0800 Subject: [PATCH 14/20] comment out reversible code --- mobile-app/lib/v2/screens/settings/settings_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index d3df0efa..5ca3374f 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -34,7 +34,7 @@ class _SettingsScreenV2State extends ConsumerState { bool _biometricEnabled = false; String _biometricDesc = 'Face ID Disabled'; int _autoLockMinutes = 5; - bool _reversibleEnabled = false; + // bool _reversibleEnabled = false; int _reversibleTimeSeconds = 600; bool _hasPinSet = false; @@ -55,7 +55,7 @@ class _SettingsScreenV2State extends ConsumerState { final bioDesc = await _authService.getBiometricDescription(); final timeout = _authService.getAuthTimeoutMinutes(); final revTime = await _settingsService.getReversibleTimeSeconds() ?? 600; - final revEnabled = _settingsService.isReversibleEnabled(); + // final revEnabled = _settingsService.isReversibleEnabled(); if (!mounted) return; setState(() { @@ -63,7 +63,7 @@ class _SettingsScreenV2State extends ConsumerState { _biometricDesc = bioEnabled ? bioDesc : 'Face ID Disabled'; _autoLockMinutes = timeout; _reversibleTimeSeconds = revTime; - _reversibleEnabled = revEnabled; + // _reversibleEnabled = revEnabled; }); } From 82a0ad2e5e95b214e8229f31affcce6805ddd31e Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 11:37:42 +0800 Subject: [PATCH 15/20] swap screen updated - graphics icons - formatting numbers - quan at $1 --- mobile-app/lib/v2/components/token_icon.dart | 104 ++++++ .../lib/v2/screens/swap/deposit_screen.dart | 15 +- .../v2/screens/swap/review_quote_sheet.dart | 22 +- .../lib/v2/screens/swap/swap_screen.dart | 158 +++++++-- .../v2/screens/swap/token_picker_sheet.dart | 251 +++++++++++--- .../lib/src/services/swap_service.dart | 305 ++++++++++++++++-- 6 files changed, 753 insertions(+), 102 deletions(-) create mode 100644 mobile-app/lib/v2/components/token_icon.dart diff --git a/mobile-app/lib/v2/components/token_icon.dart b/mobile-app/lib/v2/components/token_icon.dart new file mode 100644 index 00000000..5773509f --- /dev/null +++ b/mobile-app/lib/v2/components/token_icon.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class TokenIcon extends StatelessWidget { + final SwapToken token; + final double size; + final double networkBadgeSize; + + const TokenIcon({ + super.key, + required this.token, + this.size = 31, + this.networkBadgeSize = 12, + }); + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final swapService = SwapService(); + final iconUrl = token.iconUrl ?? swapService.getTokenIconUrl(token); + final networkIconUrl = token.networkIconUrl ?? swapService.getNetworkIconUrl(token); + + return SizedBox( + width: size, + height: size, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: ClipOval( + child: iconUrl != null + ? Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _fallback(context, token, colors, text), + ) + : _fallback(context, token, colors, text), + ), + ), + Positioned( + right: -2, + bottom: -2, + child: SizedBox( + width: networkBadgeSize, + height: networkBadgeSize, + child: ClipOval( + child: networkIconUrl != null + ? Image.network( + networkIconUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _networkFallback(context, token, colors, text), + ) + : _networkFallback(context, token, colors, text), + ), + ), + ), + ], + ), + ); + } + + Widget _fallback(BuildContext context, SwapToken token, AppColorsV2 colors, AppTextTheme text) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF2F86E8), + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFF71B5FF), width: 1.4), + ), + child: Center( + child: Text( + token.symbol.isNotEmpty ? token.symbol.substring(0, 1) : '?', + style: text.tiny?.copyWith( + color: colors.textPrimary, + fontWeight: FontWeight.w700, + decoration: TextDecoration.none, + ), + ), + ), + ); + } + + Widget _networkFallback(BuildContext context, SwapToken token, AppColorsV2 colors, AppTextTheme text) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + border: Border.all(color: const Color(0xFF3D3D3D)), + ), + child: Center( + child: Text( + token.network.isNotEmpty ? token.network.substring(0, 1) : '?', + style: text.tiny?.copyWith( + color: colors.textPrimary, + fontSize: 8, + fontWeight: FontWeight.w700, + decoration: TextDecoration.none, + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index 662d87c8..08f289d5 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -4,6 +4,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/components/gradient_background.dart'; import 'package:resonance_network_wallet/v2/components/success_check.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -115,8 +116,8 @@ class _DepositScreenState extends State { Text('Deposit Amount', style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), const SizedBox(width: 6), GestureDetector( - onTap: () => context.copyTextWithToaster( - quote.totalAmount.toStringAsFixed(2), + onTap: () => context.copyTextWithToaster( + SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken), message: 'Deposit amount copied to clipboard', ), child: Container( @@ -132,14 +133,10 @@ class _DepositScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - width: 28, - height: 28, - decoration: BoxDecoration(color: colors.accentPink.withValues(alpha: 0.3), shape: BoxShape.circle), - ), + TokenIcon(token: quote.fromToken, size: 28, networkBadgeSize: 11), const SizedBox(width: 8), Text( - quote.totalAmount.toStringAsFixed(2), + SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken), style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w600), ), ], @@ -281,7 +278,7 @@ class _DepositScreenState extends State { Text('Swap Complete', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), const SizedBox(height: 12), Text( - 'Your swap for ${_order.quote.toAmount.toStringAsFixed(2)} QUAN is processing.', + 'Your swap for ${SwapService.formatTokenAmount(_order.quote.toAmount, _order.quote.toToken)} QUAN is processing.', style: text.paragraph?.copyWith(color: colors.textSecondary), textAlign: TextAlign.center, ), diff --git a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart index d1945fd6..60b7d288 100644 --- a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/screens/swap/deposit_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -54,11 +55,16 @@ class _ReviewQuoteContent extends StatelessWidget { const SizedBox(height: 32), _swapVisual(context, colors, text, fromUsd, toUsd), const SizedBox(height: 48), - _feeRow('Total fees', '${quote.networkFee.toStringAsFixed(3)} ${quote.fromToken.symbol}', colors, text), + _feeRow( + 'Total fees', + '${SwapService.formatTokenAmount(quote.networkFee, quote.fromToken)} ${quote.fromToken.symbol}', + colors, + text, + ), Divider(color: colors.separator, height: 32), _feeRow( 'Total Amount', - '${quote.totalAmount.toStringAsFixed(2)} ${quote.fromToken.symbol}', + '${SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken)} ${quote.fromToken.symbol}', colors, text, highlight: true, @@ -89,7 +95,6 @@ class _ReviewQuoteContent extends StatelessWidget { } Widget _tokenCard(SwapToken token, double amount, double usd, double width, AppColorsV2 colors, AppTextTheme text) { - final isQu = token.symbol == 'QUAN'; return Container( width: width, height: 111, @@ -101,14 +106,7 @@ class _ReviewQuoteContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: isQu ? colors.accentGreen.withValues(alpha: 0.3) : colors.accentPink.withValues(alpha: 0.3), - shape: BoxShape.circle, - ), - ), + TokenIcon(token: token, size: 22, networkBadgeSize: 9), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -124,7 +122,7 @@ class _ReviewQuoteContent extends StatelessWidget { ), const SizedBox(height: 6), Text( - amount.toStringAsFixed(2), + SwapService.formatTokenAmount(amount, token), style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w600), ), const SizedBox(height: 0), diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index 0127650f..e06af803 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -4,6 +4,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/components/gradient_background.dart'; import 'package:resonance_network_wallet/v2/screens/swap/refund_address_picker_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/swap/review_quote_sheet.dart'; @@ -34,7 +35,14 @@ class _SwapScreenState extends State { bool _loading = false; double get _rate => _swapService.getRate(_fromToken); - String get _rateLabel => '1 QUAN = ${(1 / _rate).toStringAsFixed(4)} ${_fromToken.symbol}'; + String get _rateLabel { + final val = 1 / _rate; + if (val == 0) return '1 QUAN = 0 ${_fromToken.symbol}'; + final decimals = val >= 100 ? 2 : val >= 1 ? 4 : val >= 0.01 ? 6 : val >= 0.0001 ? 8 : 10; + var formatted = val.toStringAsFixed(decimals).replaceAll(RegExp(r'0+$'), ''); + if (formatted.endsWith('.')) formatted = formatted.substring(0, formatted.length - 1); + return '1 QUAN = $formatted ${_fromToken.symbol}'; + } @override void initState() { @@ -80,7 +88,12 @@ class _SwapScreenState extends State { } void _pickToken() async { - final token = await showTokenPickerSheet(context, _swapService.getFromTokens(), _fromToken); + final token = await showTokenPickerSheet( + context, + current: _fromToken, + loadTokens: ({bool forceRefresh = false}) => _swapService.getFromTokens(limit: 10, forceRefresh: forceRefresh), + ); + if (!mounted) return; if (token != null && token != _fromToken) { setState(() => _fromToken = token); _recalculate(); @@ -176,7 +189,7 @@ class _SwapScreenState extends State { style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.bold), keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( - hintText: '0.00', + hintText: SwapService.formatTokenAmountHint(_fromToken), hintStyle: text.mediumTitle?.copyWith(color: colors.textTertiary, fontWeight: FontWeight.bold), border: InputBorder.none, isDense: true, @@ -198,14 +211,7 @@ class _SwapScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ - Container( - width: 25, - height: 25, - decoration: BoxDecoration( - color: colors.accentPink.withValues(alpha: 0.3), - shape: BoxShape.circle, - ), - ), + TokenIcon(token: _fromToken, size: 25, networkBadgeSize: 10), const SizedBox(width: 8), Expanded( child: Column( @@ -348,7 +354,7 @@ class _SwapScreenState extends State { decoration: BoxDecoration(color: colors.surfaceGlass, borderRadius: BorderRadius.circular(8)), alignment: Alignment.centerLeft, child: Text( - _toAmount > 0 ? _toAmount.toStringAsFixed(2) : '0.00', + SwapService.formatTokenAmount(_toAmount, _swapService.getQuToken()), style: text.mediumTitle?.copyWith( fontWeight: FontWeight.bold, color: _toAmount > 0 ? colors.textPrimary : colors.textTertiary, @@ -449,34 +455,87 @@ class _SwapScreenState extends State { } } -class _QrScanPage extends StatelessWidget { +class _QrScanPage extends StatefulWidget { final ValueChanged onScanned; const _QrScanPage({required this.onScanned}); + @override + State<_QrScanPage> createState() => _QrScanPageState(); +} + +class _QrScanPageState extends State<_QrScanPage> { + final _controller = MobileScannerController(); + bool _scanned = false; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final colors = context.colors; + final frameSize = (MediaQuery.of(context).size.width - 111.5).clamp(220.0, 280.0); + return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ MobileScanner( + controller: _controller, onDetect: (capture) { + if (_scanned) return; final code = capture.barcodes.firstOrNull?.rawValue; - if (code != null) onScanned(code); + if (code != null && code.isNotEmpty) { + _scanned = true; + widget.onScanned(code); + } }, ), + ColoredBox(color: Colors.black.withValues(alpha: 0.24)), + Center(child: _ScanFrame(size: frameSize)), + Positioned( + left: 0, + right: 0, + bottom: 228, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _scanActionButton(icon: Icons.image_outlined, onTap: () {}, colors: colors), + const SizedBox(width: 8), + ValueListenableBuilder( + valueListenable: _controller, + builder: (_, state, __) { + final isOn = state.torchState == TorchState.on; + return _scanActionButton( + icon: isOn ? Icons.flash_on : Icons.flash_off, + onTap: _controller.toggleTorch, + colors: colors, + ); + }, + ), + ], + ), + ), Positioned( - bottom: 60, + bottom: 58, left: 24, right: 24, child: GestureDetector( onTap: () => Navigator.pop(context), child: Container( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration(color: colors.surfaceGlass, borderRadius: BorderRadius.circular(14)), + padding: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + border: Border.all(color: Colors.white.withValues(alpha: 0.44), width: 0.9), + borderRadius: BorderRadius.circular(14), + ), child: Center( - child: Text('Cancel', style: TextStyle(color: colors.textPrimary, fontSize: 16)), + child: Text( + 'Cancel', + style: TextStyle(color: colors.textPrimary, fontSize: 16, fontWeight: FontWeight.w500), + ), ), ), ), @@ -485,4 +544,67 @@ class _QrScanPage extends StatelessWidget { ), ); } + + Widget _scanActionButton({required IconData icon, required VoidCallback onTap, required AppColorsV2 colors}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), + child: Icon(icon, size: 20, color: colors.textPrimary), + ), + ); + } +} + +class _ScanFrame extends StatelessWidget { + final double size; + const _ScanFrame({required this.size}); + + @override + Widget build(BuildContext context) { + final corner = Colors.white.withValues(alpha: 0.92); + return SizedBox( + width: size, + height: size, + child: Stack( + children: [ + _corner(top: true, left: true, color: corner), + _corner(top: true, left: false, color: corner), + _corner(top: false, left: true, color: corner), + _corner(top: false, left: false, color: corner), + ], + ), + ); + } + + Widget _corner({required bool top, required bool left, required Color color}) { + return Positioned( + top: top ? 0 : null, + bottom: top ? null : 0, + left: left ? 0 : null, + right: left ? null : 0, + child: SizedBox( + width: 41, + height: 41, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: top && left ? const Radius.circular(16) : Radius.zero, + topRight: top && !left ? const Radius.circular(16) : Radius.zero, + bottomLeft: !top && left ? const Radius.circular(16) : Radius.zero, + bottomRight: !top && !left ? const Radius.circular(16) : Radius.zero, + ), + border: Border( + top: top ? BorderSide(color: color, width: 1.6) : BorderSide.none, + bottom: !top ? BorderSide(color: color, width: 1.6) : BorderSide.none, + left: left ? BorderSide(color: color, width: 1.6) : BorderSide.none, + right: !left ? BorderSide(color: color, width: 1.6) : BorderSide.none, + ), + ), + ), + ), + ); + } } diff --git a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart index 2ad1da51..7aedcc93 100644 --- a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart @@ -1,92 +1,259 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -Future showTokenPickerSheet(BuildContext context, List tokens, SwapToken current) { - return showModalBottomSheet( +typedef SwapTokenLoader = Future> Function({bool forceRefresh}); + +Future showTokenPickerSheet( + BuildContext context, { + required SwapTokenLoader loadTokens, + required SwapToken current, +}) { + return showGeneralDialog( context: context, - backgroundColor: Colors.transparent, - builder: (_) => _TokenPickerContent(tokens: tokens, current: current), + barrierDismissible: true, + barrierLabel: 'Select Token', + barrierColor: Colors.transparent, + pageBuilder: (_, __, ___) => _TokenPickerContent(loadTokens: loadTokens, current: current), ); } -class _TokenPickerContent extends StatelessWidget { - final List tokens; +class _TokenPickerContent extends StatefulWidget { + final SwapTokenLoader loadTokens; final SwapToken current; - const _TokenPickerContent({required this.tokens, required this.current}); + const _TokenPickerContent({required this.loadTokens, required this.current}); + + @override + State<_TokenPickerContent> createState() => _TokenPickerContentState(); +} + +class _TokenPickerContentState extends State<_TokenPickerContent> { + final _scrollController = ScrollController(); + List _tokens = const []; + bool _loading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadTokens(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Future _loadTokens({bool forceRefresh = false}) async { + setState(() { + _loading = true; + _error = null; + }); + try { + final tokens = await widget.loadTokens(forceRefresh: forceRefresh); + if (!mounted) return; + setState(() { + _tokens = tokens; + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _loading = false; + _error = 'Failed to load tokens'; + }); + } + } @override Widget build(BuildContext context) { final colors = context.colors; final text = context.themeText; + final size = MediaQuery.of(context).size; + final height = size.height; + final cardHeight = (height - 120).clamp(360.0, 506.0); - return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2), - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: colors.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + return DefaultTextStyle( + style: const TextStyle(decoration: TextDecoration.none), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2), + child: ColoredBox( + color: Colors.black.withValues(alpha: 0.5), + child: SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: (size.width - 28).clamp(300.0, 362.0), + height: cardHeight, + margin: const EdgeInsets.fromLTRB(14, 0, 14, 20), + padding: const EdgeInsets.fromLTRB(24, 40, 24, 24), + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + border: Border.all(color: const Color(0xFF3D3D3D)), + borderRadius: BorderRadius.circular(24), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Select Token', + style: text.smallTitle?.copyWith( + color: colors.textPrimary, + fontSize: 20, + decoration: TextDecoration.none, + ), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, color: colors.textPrimary, size: 20), + ), + ], + ), + const SizedBox(height: 24), + Expanded(child: _content(colors, text)), + ], + ), + ), + ), ), + ), + ), + ); + } + + Widget _content(AppColorsV2 colors, AppTextTheme text) { + if (_loading && _tokens.isEmpty) { + return Center(child: CircularProgressIndicator(color: colors.textPrimary, strokeWidth: 2)); + } + if (_error != null && _tokens.isEmpty) { + return Center( child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Select Token', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 18)), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Icon(Icons.close, color: colors.textPrimary, size: 20), + Text( + _error!, + style: text.smallParagraph?.copyWith( + color: colors.textSecondary, + decoration: TextDecoration.none, + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => _loadTokens(forceRefresh: true), + child: Text( + 'Retry', + style: text.paragraph?.copyWith( + color: colors.textPrimary, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, ), - ], + ), ), - const SizedBox(height: 24), - ...tokens.map((token) => _tokenRow(context, token, colors, text)), - const SizedBox(height: 16), ], ), - ), + ); + } + return Column( + children: [ + if (_error != null) ...[ + Row( + children: [ + Expanded( + child: Text( + _error!, + style: text.detail?.copyWith( + color: colors.textSecondary, + decoration: TextDecoration.none, + ), + ), + ), + GestureDetector( + onTap: () => _loadTokens(forceRefresh: true), + child: Text( + 'Retry', + style: text.detail?.copyWith( + color: colors.textPrimary, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + Expanded( + child: RefreshIndicator( + color: colors.textPrimary, + onRefresh: () => _loadTokens(forceRefresh: true), + child: Scrollbar( + controller: _scrollController, + thumbVisibility: true, + radius: const Radius.circular(25), + thickness: 4, + child: ListView.builder( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _tokens.length, + itemBuilder: (_, index) => _tokenRow(context, _tokens[index], colors, text), + ), + ), + ), + ), + ], ); } Widget _tokenRow(BuildContext context, SwapToken token, AppColorsV2 colors, AppTextTheme text) { - final selected = token == current; + final selected = token == widget.current; return GestureDetector( onTap: () => Navigator.pop(context, token), behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: selected ? Border.all(color: Colors.white.withValues(alpha: 0.44), width: 0.9) : null, + ), child: Row( children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration(color: colors.accentPink.withValues(alpha: 0.2), shape: BoxShape.circle), - child: Center( - child: Text(token.symbol[0], style: text.smallParagraph?.copyWith(color: colors.textPrimary)), - ), - ), - const SizedBox(width: 16), + TokenIcon(token: token), + const SizedBox(width: 9), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( token.symbol, - style: text.smallParagraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), + style: text.paragraph?.copyWith( + color: colors.textPrimary, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + const SizedBox(height: 4), + Text( + token.network, + style: text.paragraph?.copyWith( + color: colors.textSecondary, + decoration: TextDecoration.none, + ), ), - Text('${token.name} · ${token.network}', style: text.detail?.copyWith(color: colors.textTertiary)), ], ), ), - if (selected) Icon(Icons.check_circle, color: colors.accentGreen, size: 20), ], ), ), ); } + } diff --git a/quantus_sdk/lib/src/services/swap_service.dart b/quantus_sdk/lib/src/services/swap_service.dart index 0a9997e6..aa3c1ced 100644 --- a/quantus_sdk/lib/src/services/swap_service.dart +++ b/quantus_sdk/lib/src/services/swap_service.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; enum SwapStatus { pending, depositing, processing, complete, failed, expired } @@ -8,11 +11,21 @@ class SwapToken { final String name; final String network; final int decimals; + final String? iconUrl; + final String? networkIconUrl; - const SwapToken({required this.symbol, required this.name, required this.network, this.decimals = 18}); + const SwapToken({ + required this.symbol, + required this.name, + required this.network, + this.decimals = 18, + this.iconUrl, + this.networkIconUrl, + }); @override - bool operator ==(Object other) => other is SwapToken && symbol == other.symbol && network == other.network; + bool operator ==(Object other) => + other is SwapToken && symbol == other.symbol && network == other.network; @override int get hashCode => Object.hash(symbol, network); @@ -75,7 +88,14 @@ class SwapService { static const _refundAddressKey = 'recent_refund_addresses'; static const _maxRefundAddresses = 50; + static const _intentsTokensUrl = 'https://1click.chaindefuser.com/v0/tokens'; + static const _coinGeckoTopUrl = + 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=150&page=1&sparkline=false'; + static const _tokensCacheTtl = Duration(minutes: 10); final _orders = {}; + List? _cachedFromTokens; + DateTime? _cachedFromTokensAt; + Map _liveUsdPriceBySymbol = {}; static const availableTokens = [ SwapToken(symbol: 'USDC', name: 'USD Coin', network: 'Ethereum'), @@ -86,47 +106,262 @@ class SwapService { SwapToken(symbol: 'QUAN', name: 'Quantus', network: 'Quantus'), ]; - static const _quToken = SwapToken(symbol: 'QUAN', name: 'Quantus', network: 'Quantus'); + static const _quToken = SwapToken( + symbol: 'QUAN', + name: 'Quantus', + network: 'Quantus', + ); + + Future> getFromTokens({ + int limit = 10, + bool forceRefresh = false, + }) async { + final now = DateTime.now(); + if (!forceRefresh && + _cachedFromTokens != null && + _cachedFromTokensAt != null && + now.difference(_cachedFromTokensAt!) < _tokensCacheTtl) { + return _cachedFromTokens!.take(limit).toList(); + } + + try { + final intentTokens = await _fetchNearIntentsTokens(); + if (intentTokens.isNotEmpty) { + final ranked = await _rankByCoinGecko(intentTokens); + _cachedFromTokens = ranked; + _cachedFromTokensAt = now; + _liveUsdPriceBySymbol = { + for (final token in ranked) token.symbol.toUpperCase(): token.price, + }; + return ranked.take(limit).toList(); + } + } catch (_) {} - List getFromTokens() => availableTokens.where((t) => t.symbol != 'QUAN').toList(); + final fallback = availableTokens + .where((t) => t.symbol != 'QUAN') + .take(limit) + .toList(); + _cachedFromTokens = fallback; + _cachedFromTokensAt = now; + return fallback; + } SwapToken getQuToken() => _quToken; + static String formatTokenAmount(double amount, SwapToken token) { + if (amount == 0) return '0'; + final decimals = token.decimals.clamp(0, 12); + var s = amount.toStringAsFixed(decimals).replaceAll(RegExp(r'0+$'), ''); + if (s.endsWith('.')) s = s.substring(0, s.length - 1); + return s; + } + + static String formatTokenAmountHint(SwapToken token) { + if (token.decimals == 0) return '0'; + return '0.${'0' * token.decimals.clamp(1, 8)}'; + } + + String? getTokenIconUrl(SwapToken token) { + final cached = _cachedFromTokens + ?.where((t) => t.symbol == token.symbol && t.network == token.network) + .firstOrNull; + if (cached?.iconUrl != null && cached!.iconUrl!.isNotEmpty) return cached.iconUrl; + return _fallbackTokenIconUrl(token.symbol); + } + + String? getNetworkIconUrl(SwapToken token) { + final cached = _cachedFromTokens + ?.where((t) => t.symbol == token.symbol && t.network == token.network) + .firstOrNull; + if (cached?.networkIconUrl != null && cached!.networkIconUrl!.isNotEmpty) return cached.networkIconUrl; + return _networkIconUrl(token.network); + } + double getRate(SwapToken from) { - switch (from.symbol) { + final fromUsd = getUsdPrice(from); + final toUsd = getUsdPrice(_quToken); + if (fromUsd <= 0 || toUsd <= 0) return 1.0; + return fromUsd / toUsd; + } + + double getUsdPrice(SwapToken token) { + final livePrice = _liveUsdPriceBySymbol[token.symbol.toUpperCase()]; + if (livePrice != null && livePrice > 0) return livePrice; + switch (token.symbol.toUpperCase()) { case 'USDC': case 'USDT': - return 10.0; + return 1.0; case 'ETH': - return 25000.0; + return 2500.0; case 'BTC': return 60000.0; case 'SOL': - return 1500.0; - default: + return 150.0; + case 'QUAN': return 1.0; + default: + return 0.0; } } - double getUsdPrice(SwapToken token) { - switch (token.symbol) { + Future> _fetchNearIntentsTokens() async { + final response = await http.get(Uri.parse(_intentsTokensUrl)); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Failed to fetch intents tokens'); + } + final data = jsonDecode(response.body); + if (data is! List) return const []; + + final bySymbol = {}; + for (final item in data) { + if (item is! Map) continue; + final symbolRaw = item['symbol']; + final blockchainRaw = item['blockchain']; + final decimalsRaw = item['decimals']; + final priceRaw = item['price']; + if (symbolRaw is! String || symbolRaw.isEmpty) continue; + if (blockchainRaw is! String || blockchainRaw.isEmpty) continue; + final symbol = symbolRaw.toUpperCase(); + if (symbol == 'QUAN') continue; + final price = (priceRaw as num?)?.toDouble() ?? 0; + if (price <= 0) continue; + final decimals = (decimalsRaw as num?)?.toInt() ?? 18; + final token = _IntentToken( + symbol: symbol, + network: blockchainRaw.toUpperCase(), + decimals: decimals, + price: price, + networkIconUrl: _networkIconUrl(blockchainRaw.toUpperCase()), + ); + final existing = bySymbol[symbol]; + if (existing == null || + _networkPriority(token.network) < + _networkPriority(existing.network)) { + bySymbol[symbol] = token; + } + } + return bySymbol.values.toList(); + } + + Future> _rankByCoinGecko(List<_IntentToken> tokens) async { + try { + final response = await http.get(Uri.parse(_coinGeckoTopUrl)); + if (response.statusCode < 200 || response.statusCode >= 300) { + return _sortByPrice(tokens); + } + final payload = jsonDecode(response.body); + if (payload is! List) return _sortByPrice(tokens); + final rankBySymbol = {}; + final iconBySymbol = {}; + for (var i = 0; i < payload.length; i++) { + final item = payload[i]; + if (item is! Map) continue; + final symbol = (item['symbol'] as String?)?.toUpperCase(); + if (symbol == null || + symbol.isEmpty || + rankBySymbol.containsKey(symbol)) + continue; + rankBySymbol[symbol] = i; + final icon = item['image'] as String?; + if (icon != null && icon.isNotEmpty) iconBySymbol[symbol] = icon; + } + final ranked = [ + for (final token in tokens) + token.copyWith( + iconUrl: + iconBySymbol[token.symbol] ?? + token.iconUrl ?? + _fallbackTokenIconUrl(token.symbol), + ), + ]; + ranked.sort((a, b) { + final ar = rankBySymbol[a.symbol] ?? 99999; + final br = rankBySymbol[b.symbol] ?? 99999; + if (ar != br) return ar.compareTo(br); + return b.price.compareTo(a.price); + }); + return ranked; + } catch (_) { + return _sortByPrice(tokens); + } + } + + List<_IntentToken> _sortByPrice(List<_IntentToken> tokens) { + final sorted = [...tokens]; + sorted.sort((a, b) => b.price.compareTo(a.price)); + return sorted; + } + + int _networkPriority(String network) { + switch (network) { + case 'ETH': + return 0; + case 'BTC': + return 1; + case 'SOL': + return 2; + case 'NEAR': + return 3; + case 'BASE': + return 4; + case 'ARB': + return 5; + default: + return 100; + } + } + + String? _fallbackTokenIconUrl(String symbol) { + switch (symbol) { case 'USDC': + return 'https://assets.coingecko.com/coins/images/6319/large/usdc.png'; case 'USDT': - return 1.0; + return 'https://assets.coingecko.com/coins/images/325/large/Tether.png'; case 'ETH': - return 2500.0; + case 'WETH': + return 'https://assets.coingecko.com/coins/images/279/large/ethereum.png'; case 'BTC': - return 60000.0; + case 'WBTC': + case 'XBTC': + return 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png'; case 'SOL': - return 150.0; - case 'QUAN': - return 0.10; + return 'https://assets.coingecko.com/coins/images/4128/large/solana.png'; + case 'NEAR': + case 'WNEAR': + return 'https://assets.coingecko.com/coins/images/10365/large/near.jpg'; default: - return 0.0; + return null; + } + } + + String? _networkIconUrl(String network) { + switch (network) { + case 'ETH': + case 'BASE': + case 'ARB': + case 'OP': + case 'GNOSIS': + case 'AVAX': + case 'POL': + case 'MONAD': + case 'BSC': + return 'https://assets.coingecko.com/coins/images/279/large/ethereum.png'; + case 'BTC': + return 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png'; + case 'SOL': + return 'https://assets.coingecko.com/coins/images/4128/large/solana.png'; + case 'NEAR': + return 'https://assets.coingecko.com/coins/images/10365/large/near.jpg'; + default: + return null; } } - Future getQuote({required SwapToken fromToken, required double fromAmount, double slippage = 0.01}) async { + Future getQuote({ + required SwapToken fromToken, + required double fromAmount, + double slippage = 0.01, + }) async { await Future.delayed(const Duration(milliseconds: 500)); final rate = getRate(fromToken); final toAmount = fromAmount * rate; @@ -150,7 +385,10 @@ class SwapService { Future createSwap(SwapQuote quote) async { await Future.delayed(const Duration(milliseconds: 300)); final rng = Random(); - final hex = List.generate(40, (_) => rng.nextInt(16).toRadixString(16)).join(); + final hex = List.generate( + 40, + (_) => rng.nextInt(16).toRadixString(16), + ).join(); final order = SwapOrder( orderId: 'swap_${DateTime.now().millisecondsSinceEpoch}', quote: quote, @@ -178,7 +416,9 @@ class SwapService { Future.delayed(const Duration(seconds: 5), () { if (_orders.containsKey(orderId)) { - _orders[orderId] = _orders[orderId]!.copyWith(status: SwapStatus.complete); + _orders[orderId] = _orders[orderId]!.copyWith( + status: SwapStatus.complete, + ); } }); @@ -203,3 +443,26 @@ class SwapService { return prefs.getStringList(key) ?? []; } } + +class _IntentToken extends SwapToken { + final double price; + const _IntentToken({ + required super.symbol, + required super.network, + required super.decimals, + required this.price, + super.iconUrl, + super.networkIconUrl, + }) : super(name: symbol); + + _IntentToken copyWith({String? iconUrl, String? networkIconUrl}) { + return _IntentToken( + symbol: symbol, + network: network, + decimals: decimals, + price: price, + iconUrl: iconUrl ?? this.iconUrl, + networkIconUrl: networkIconUrl ?? this.networkIconUrl, + ); + } +} From ac3e00549fa2c0a6b9ca4250ad23b5a10dcf07a4 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 11:51:54 +0800 Subject: [PATCH 16/20] swap screen flag --- mobile-app/lib/utils/feature_flags.dart | 1 + .../lib/v2/screens/home/home_screen.dart | 184 ++++++++++-------- 2 files changed, 101 insertions(+), 84 deletions(-) diff --git a/mobile-app/lib/utils/feature_flags.dart b/mobile-app/lib/utils/feature_flags.dart index 02547118..84849f17 100644 --- a/mobile-app/lib/utils/feature_flags.dart +++ b/mobile-app/lib/utils/feature_flags.dart @@ -4,4 +4,5 @@ class FeatureFlags { static const bool enableKeystoneHardwareWallet = false; // turn keystone hw wallet on and off static const bool enableHighSecurity = true; // turn keystone hw wallet on and off static const bool enableRemoteNotifications = false; // turn remote notifications on and off + static const bool enableSwap = false; } diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 5f28e255..888dccf6 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -10,6 +10,7 @@ import 'package:resonance_network_wallet/v2/screens/home/accounts_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_screen.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/screens/swap/swap_screen.dart'; import 'package:resonance_network_wallet/providers/account_id_list_cache.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; @@ -171,75 +172,92 @@ class _HomeScreenState extends ConsumerState { } Widget _buildBalance(AsyncValue balanceAsync, bool isBalanceHidden, AppColorsV2 colors, AppTextTheme text) { - const stableHeight = 96.0; - - return SizedBox( - height: stableHeight, - child: Column( - children: [ - balanceAsync.when( - data: (balance) { - final formatted = isBalanceHidden ? '-----' : _fmt.formatBalance(balance); - return Stack( - alignment: Alignment.center, - children: [ - ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), - child: Text( + return Column( + children: [ + balanceAsync.when( + data: (balance) { + final formatted = isBalanceHidden ? '-----' : _fmt.formatBalance(balance); + final usdFormatted = isBalanceHidden ? '-----' : '\$${_fmt.formatBalance(balance)}'; + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Text( + '$formatted ${AppConstants.tokenSymbol}', + style: text.extraLargeTitle?.copyWith(color: colors.textSecondary), + ), + ), + Text( '$formatted ${AppConstants.tokenSymbol}', - style: text.extraLargeTitle?.copyWith(color: colors.textSecondary), + style: text.extraLargeTitle?.copyWith(color: colors.textPrimary), ), - ), - Text( - '$formatted ${AppConstants.tokenSymbol}', - style: text.extraLargeTitle?.copyWith(color: colors.textPrimary), - ), - ], - ); - }, - loading: () => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Skeleton(width: 200, height: 36), - Text(' ${AppConstants.tokenSymbol}', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + ], + ), + const SizedBox(height: 6), + Text( + '≈ $usdFormatted', + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ], - ), - error: (_, _) => Text('Error loading balance', style: text.detail?.copyWith(color: colors.textError)), + ); + }, + loading: () => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Skeleton(width: 200, height: 36), + Text(' ${AppConstants.tokenSymbol}', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + ], ), - // if (!isBalanceHidden) ...[ - // const SizedBox(height: 6), - // Text('≈ \$0.00', style: text.paragraph?.copyWith(color: colors.textSecondary)), - // ], - ], - ), + error: (_, _) => Text('Error loading balance', style: text.detail?.copyWith(color: colors.textError)), + ), + ], ); } Widget _buildActionButtons(AppColorsV2 colors, AppTextTheme text) { + final receiveCard = _actionCard( + iconAsset: 'assets/v2/action_receive.svg', + label: 'Receive', + colors: colors, + text: text, + onTap: () => showReceiveSheetV2(context), + ); + final sendCard = _actionCard( + iconAsset: 'assets/v2/action_send.svg', + label: 'Send', + colors: colors, + text: text, + onTap: () => showSendSheetV2(context), + ); + + if (!FeatureFlags.enableSwap) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 104, child: receiveCard), + const SizedBox(width: 32), + SizedBox(width: 104, child: sendCard), + ], + ); + } + return Row( children: [ - _actionCard( - iconAsset: 'assets/v2/action_receive.svg', - label: 'Receive', - colors: colors, - text: text, - onTap: () => showReceiveSheetV2(context), - ), + Expanded(child: receiveCard), const SizedBox(width: 15), - _actionCard( - iconAsset: 'assets/v2/action_send.svg', - label: 'Send', - colors: colors, - text: text, - onTap: () => showSendSheetV2(context), - ), + Expanded(child: sendCard), const SizedBox(width: 15), - _actionCard( - iconAsset: 'assets/v2/action_swap.svg', - label: 'Swap', - colors: colors, - text: text, - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())), + Expanded( + child: _actionCard( + iconAsset: 'assets/v2/action_swap.svg', + label: 'Swap', + colors: colors, + text: text, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())), + ), ), ], ); @@ -252,34 +270,32 @@ class _HomeScreenState extends ConsumerState { required AppTextTheme text, required VoidCallback onTap, }) { - return Expanded( - child: GestureDetector( - onTap: onTap, - child: SizedBox( - height: 80, - child: Stack( - fit: StackFit.expand, - children: [ - Image.asset(_actionButtonBgAsset, fit: BoxFit.fill), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset(iconAsset, width: 24, height: 24), - const SizedBox(height: 6), - Text( - label, - maxLines: 1, - overflow: TextOverflow.clip, - style: text.paragraph?.copyWith(color: colors.textPrimary, height: 1.0), - ), - ], - ), + return GestureDetector( + onTap: onTap, + child: SizedBox( + height: 80, + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset(_actionButtonBgAsset, fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(iconAsset, width: 24, height: 24), + const SizedBox(height: 6), + Text( + label, + maxLines: 1, + overflow: TextOverflow.clip, + style: text.paragraph?.copyWith(color: colors.textPrimary, height: 1.0), + ), + ], ), - ], - ), + ), + ], ), ), ); From d55c0c63e67d388dd80c2d0e893bcb2b0c8d1fde Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 12:27:00 +0800 Subject: [PATCH 17/20] toast fix and receive screen copy buttons --- mobile-app/lib/v2/components/toaster_helper.dart | 4 ++-- mobile-app/lib/v2/screens/receive/receive_sheet.dart | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mobile-app/lib/v2/components/toaster_helper.dart b/mobile-app/lib/v2/components/toaster_helper.dart index 2ad6419c..c98c21a9 100644 --- a/mobile-app/lib/v2/components/toaster_helper.dart +++ b/mobile-app/lib/v2/components/toaster_helper.dart @@ -9,14 +9,14 @@ Future showToaster( required String message, required IconData iconData, Color? iconColor, - Duration duration = const Duration(seconds: 3), + Duration duration = const Duration(seconds: 2), FlashBehavior behavior = FlashBehavior.floating, }) async { if (!context.mounted) return; await context.showFlash( duration: duration, - persistent: false, + persistent: true, builder: (context, controller) { return FlashBar( controller: controller, diff --git a/mobile-app/lib/v2/screens/receive/receive_sheet.dart b/mobile-app/lib/v2/screens/receive/receive_sheet.dart index cecc5a95..f7c3b7c3 100644 --- a/mobile-app/lib/v2/screens/receive/receive_sheet.dart +++ b/mobile-app/lib/v2/screens/receive/receive_sheet.dart @@ -175,12 +175,16 @@ class _ReceiveSheetState extends State { } Widget _copyButton(AppColorsV2 colors) { - return Container( + return + SizedBox( width: 20, height: 20, - decoration: BoxDecoration(color: colors.surfaceGlass, borderRadius: BorderRadius.circular(4)), + child: GlassContainer( + asset: GlassContainer.tinyAsset, + filled: false, + onTap: _copyAddress, child: Icon(Icons.copy, size: 12, color: colors.textPrimary), - ); + )); } Widget _buildButtons(AppColorsV2 colors, AppTextTheme text) { From 150bc09902d6e9a777b11087dac51cca7420a781 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 12:27:31 +0800 Subject: [PATCH 18/20] format --- mobile-app/lib/v2/components/token_icon.dart | 7 +- .../lib/v2/screens/home/home_screen.dart | 5 +- .../lib/v2/screens/receive/receive_sheet.dart | 14 ++-- .../lib/v2/screens/swap/deposit_screen.dart | 2 +- .../lib/v2/screens/swap/swap_screen.dart | 10 ++- .../v2/screens/swap/token_picker_sheet.dart | 78 ++++++++----------- .../lib/src/services/swap_service.dart | 62 ++++----------- 7 files changed, 66 insertions(+), 112 deletions(-) diff --git a/mobile-app/lib/v2/components/token_icon.dart b/mobile-app/lib/v2/components/token_icon.dart index 5773509f..ce4e7542 100644 --- a/mobile-app/lib/v2/components/token_icon.dart +++ b/mobile-app/lib/v2/components/token_icon.dart @@ -8,12 +8,7 @@ class TokenIcon extends StatelessWidget { final double size; final double networkBadgeSize; - const TokenIcon({ - super.key, - required this.token, - this.size = 31, - this.networkBadgeSize = 12, - }); + const TokenIcon({super.key, required this.token, this.size = 31, this.networkBadgeSize = 12}); @override Widget build(BuildContext context) { diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 888dccf6..0cb59e46 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -197,10 +197,7 @@ class _HomeScreenState extends ConsumerState { ], ), const SizedBox(height: 6), - Text( - '≈ $usdFormatted', - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + Text('≈ $usdFormatted', style: text.paragraph?.copyWith(color: colors.textSecondary)), ], ); }, diff --git a/mobile-app/lib/v2/screens/receive/receive_sheet.dart b/mobile-app/lib/v2/screens/receive/receive_sheet.dart index f7c3b7c3..16fbeb6d 100644 --- a/mobile-app/lib/v2/screens/receive/receive_sheet.dart +++ b/mobile-app/lib/v2/screens/receive/receive_sheet.dart @@ -175,16 +175,16 @@ class _ReceiveSheetState extends State { } Widget _copyButton(AppColorsV2 colors) { - return - SizedBox( + return SizedBox( width: 20, height: 20, child: GlassContainer( - asset: GlassContainer.tinyAsset, - filled: false, - onTap: _copyAddress, - child: Icon(Icons.copy, size: 12, color: colors.textPrimary), - )); + asset: GlassContainer.tinyAsset, + filled: false, + onTap: _copyAddress, + child: Icon(Icons.copy, size: 12, color: colors.textPrimary), + ), + ); } Widget _buildButtons(AppColorsV2 colors, AppTextTheme text) { diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index 08f289d5..6a33d151 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -116,7 +116,7 @@ class _DepositScreenState extends State { Text('Deposit Amount', style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), const SizedBox(width: 6), GestureDetector( - onTap: () => context.copyTextWithToaster( + onTap: () => context.copyTextWithToaster( SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken), message: 'Deposit amount copied to clipboard', ), diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index e06af803..f3799902 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -38,7 +38,15 @@ class _SwapScreenState extends State { String get _rateLabel { final val = 1 / _rate; if (val == 0) return '1 QUAN = 0 ${_fromToken.symbol}'; - final decimals = val >= 100 ? 2 : val >= 1 ? 4 : val >= 0.01 ? 6 : val >= 0.0001 ? 8 : 10; + final decimals = val >= 100 + ? 2 + : val >= 1 + ? 4 + : val >= 0.01 + ? 6 + : val >= 0.0001 + ? 8 + : 10; var formatted = val.toStringAsFixed(decimals).replaceAll(RegExp(r'0+$'), ''); if (formatted.endsWith('.')) formatted = formatted.substring(0, formatted.length - 1); return '1 QUAN = $formatted ${_fromToken.symbol}'; diff --git a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart index 7aedcc93..990620f3 100644 --- a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart @@ -87,44 +87,44 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { child: Align( alignment: Alignment.bottomCenter, child: Container( - width: (size.width - 28).clamp(300.0, 362.0), - height: cardHeight, - margin: const EdgeInsets.fromLTRB(14, 0, 14, 20), - padding: const EdgeInsets.fromLTRB(24, 40, 24, 24), - decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), - border: Border.all(color: const Color(0xFF3D3D3D)), - borderRadius: BorderRadius.circular(24), - ), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - 'Select Token', - style: text.smallTitle?.copyWith( - color: colors.textPrimary, - fontSize: 20, - decoration: TextDecoration.none, + width: (size.width - 28).clamp(300.0, 362.0), + height: cardHeight, + margin: const EdgeInsets.fromLTRB(14, 0, 14, 20), + padding: const EdgeInsets.fromLTRB(24, 40, 24, 24), + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + border: Border.all(color: const Color(0xFF3D3D3D)), + borderRadius: BorderRadius.circular(24), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Select Token', + style: text.smallTitle?.copyWith( + color: colors.textPrimary, + fontSize: 20, + decoration: TextDecoration.none, + ), ), ), - ), - GestureDetector( - onTap: () => Navigator.pop(context), - child: Icon(Icons.close, color: colors.textPrimary, size: 20), - ), - ], - ), - const SizedBox(height: 24), - Expanded(child: _content(colors, text)), - ], + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, color: colors.textPrimary, size: 20), + ), + ], + ), + const SizedBox(height: 24), + Expanded(child: _content(colors, text)), + ], + ), ), ), ), ), ), - ), ); } @@ -139,10 +139,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { children: [ Text( _error!, - style: text.smallParagraph?.copyWith( - color: colors.textSecondary, - decoration: TextDecoration.none, - ), + style: text.smallParagraph?.copyWith(color: colors.textSecondary, decoration: TextDecoration.none), ), const SizedBox(height: 12), GestureDetector( @@ -168,10 +165,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { Expanded( child: Text( _error!, - style: text.detail?.copyWith( - color: colors.textSecondary, - decoration: TextDecoration.none, - ), + style: text.detail?.copyWith(color: colors.textSecondary, decoration: TextDecoration.none), ), ), GestureDetector( @@ -242,10 +236,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { const SizedBox(height: 4), Text( token.network, - style: text.paragraph?.copyWith( - color: colors.textSecondary, - decoration: TextDecoration.none, - ), + style: text.paragraph?.copyWith(color: colors.textSecondary, decoration: TextDecoration.none), ), ], ), @@ -255,5 +246,4 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { ), ); } - } diff --git a/quantus_sdk/lib/src/services/swap_service.dart b/quantus_sdk/lib/src/services/swap_service.dart index aa3c1ced..461dbb45 100644 --- a/quantus_sdk/lib/src/services/swap_service.dart +++ b/quantus_sdk/lib/src/services/swap_service.dart @@ -24,8 +24,7 @@ class SwapToken { }); @override - bool operator ==(Object other) => - other is SwapToken && symbol == other.symbol && network == other.network; + bool operator ==(Object other) => other is SwapToken && symbol == other.symbol && network == other.network; @override int get hashCode => Object.hash(symbol, network); @@ -106,16 +105,9 @@ class SwapService { SwapToken(symbol: 'QUAN', name: 'Quantus', network: 'Quantus'), ]; - static const _quToken = SwapToken( - symbol: 'QUAN', - name: 'Quantus', - network: 'Quantus', - ); + static const _quToken = SwapToken(symbol: 'QUAN', name: 'Quantus', network: 'Quantus'); - Future> getFromTokens({ - int limit = 10, - bool forceRefresh = false, - }) async { + Future> getFromTokens({int limit = 10, bool forceRefresh = false}) async { final now = DateTime.now(); if (!forceRefresh && _cachedFromTokens != null && @@ -130,17 +122,12 @@ class SwapService { final ranked = await _rankByCoinGecko(intentTokens); _cachedFromTokens = ranked; _cachedFromTokensAt = now; - _liveUsdPriceBySymbol = { - for (final token in ranked) token.symbol.toUpperCase(): token.price, - }; + _liveUsdPriceBySymbol = {for (final token in ranked) token.symbol.toUpperCase(): token.price}; return ranked.take(limit).toList(); } } catch (_) {} - final fallback = availableTokens - .where((t) => t.symbol != 'QUAN') - .take(limit) - .toList(); + final fallback = availableTokens.where((t) => t.symbol != 'QUAN').take(limit).toList(); _cachedFromTokens = fallback; _cachedFromTokensAt = now; return fallback; @@ -162,17 +149,13 @@ class SwapService { } String? getTokenIconUrl(SwapToken token) { - final cached = _cachedFromTokens - ?.where((t) => t.symbol == token.symbol && t.network == token.network) - .firstOrNull; + final cached = _cachedFromTokens?.where((t) => t.symbol == token.symbol && t.network == token.network).firstOrNull; if (cached?.iconUrl != null && cached!.iconUrl!.isNotEmpty) return cached.iconUrl; return _fallbackTokenIconUrl(token.symbol); } String? getNetworkIconUrl(SwapToken token) { - final cached = _cachedFromTokens - ?.where((t) => t.symbol == token.symbol && t.network == token.network) - .firstOrNull; + final cached = _cachedFromTokens?.where((t) => t.symbol == token.symbol && t.network == token.network).firstOrNull; if (cached?.networkIconUrl != null && cached!.networkIconUrl!.isNotEmpty) return cached.networkIconUrl; return _networkIconUrl(token.network); } @@ -234,9 +217,7 @@ class SwapService { networkIconUrl: _networkIconUrl(blockchainRaw.toUpperCase()), ); final existing = bySymbol[symbol]; - if (existing == null || - _networkPriority(token.network) < - _networkPriority(existing.network)) { + if (existing == null || _networkPriority(token.network) < _networkPriority(existing.network)) { bySymbol[symbol] = token; } } @@ -257,22 +238,14 @@ class SwapService { final item = payload[i]; if (item is! Map) continue; final symbol = (item['symbol'] as String?)?.toUpperCase(); - if (symbol == null || - symbol.isEmpty || - rankBySymbol.containsKey(symbol)) - continue; + if (symbol == null || symbol.isEmpty || rankBySymbol.containsKey(symbol)) continue; rankBySymbol[symbol] = i; final icon = item['image'] as String?; if (icon != null && icon.isNotEmpty) iconBySymbol[symbol] = icon; } final ranked = [ for (final token in tokens) - token.copyWith( - iconUrl: - iconBySymbol[token.symbol] ?? - token.iconUrl ?? - _fallbackTokenIconUrl(token.symbol), - ), + token.copyWith(iconUrl: iconBySymbol[token.symbol] ?? token.iconUrl ?? _fallbackTokenIconUrl(token.symbol)), ]; ranked.sort((a, b) { final ar = rankBySymbol[a.symbol] ?? 99999; @@ -357,11 +330,7 @@ class SwapService { } } - Future getQuote({ - required SwapToken fromToken, - required double fromAmount, - double slippage = 0.01, - }) async { + Future getQuote({required SwapToken fromToken, required double fromAmount, double slippage = 0.01}) async { await Future.delayed(const Duration(milliseconds: 500)); final rate = getRate(fromToken); final toAmount = fromAmount * rate; @@ -385,10 +354,7 @@ class SwapService { Future createSwap(SwapQuote quote) async { await Future.delayed(const Duration(milliseconds: 300)); final rng = Random(); - final hex = List.generate( - 40, - (_) => rng.nextInt(16).toRadixString(16), - ).join(); + final hex = List.generate(40, (_) => rng.nextInt(16).toRadixString(16)).join(); final order = SwapOrder( orderId: 'swap_${DateTime.now().millisecondsSinceEpoch}', quote: quote, @@ -416,9 +382,7 @@ class SwapService { Future.delayed(const Duration(seconds: 5), () { if (_orders.containsKey(orderId)) { - _orders[orderId] = _orders[orderId]!.copyWith( - status: SwapStatus.complete, - ); + _orders[orderId] = _orders[orderId]!.copyWith(status: SwapStatus.complete); } }); From 28b57d314a88bf55e6b2731e1d887010ce4a5b10 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 13:08:18 +0800 Subject: [PATCH 19/20] update scanner screen to spec --- .../lib/v2/components/qr_scanner_page.dart | 199 ++++++++++++++++++ .../lib/v2/screens/send/send_sheet.dart | 64 +----- .../lib/v2/screens/swap/swap_screen.dart | 172 +-------------- mobile-app/pubspec.lock | 100 ++++++++- mobile-app/pubspec.yaml | 1 + 5 files changed, 308 insertions(+), 228 deletions(-) create mode 100644 mobile-app/lib/v2/components/qr_scanner_page.dart diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart new file mode 100644 index 00000000..b823177d --- /dev/null +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class QrScannerPage extends StatefulWidget { + const QrScannerPage({super.key}); + + @override + State createState() => _QrScannerPageState(); +} + +class _QrScannerPageState extends State { + final _controller = MobileScannerController(); + bool _scanned = false; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _onDetect(BarcodeCapture capture) { + if (_scanned) return; + final code = capture.barcodes.firstOrNull?.rawValue; + if (code != null && code.isNotEmpty) { + _scanned = true; + Navigator.pop(context, code); + } + } + + Future _pickImage() async { + final image = await ImagePicker().pickImage(source: ImageSource.gallery); + if (image == null || !mounted) return; + final capture = await _controller.analyzeImage(image.path); + if (!mounted) return; + if (capture != null) { + _onDetect(capture); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No QR code found in image')), + ); + } + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final screen = MediaQuery.of(context).size; + final frameSize = (screen.width - 112).clamp(220.0, 280.0); + + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + MobileScanner(controller: _controller, onDetect: _onDetect), + CustomPaint( + size: Size(screen.width, screen.height), + painter: _OverlayPainter(frameSize: frameSize, screenSize: screen), + ), + Center(child: _ScanFrame(size: frameSize)), + Positioned( + left: 0, + right: 0, + top: screen.height / 2 + frameSize / 2 + 24, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _actionButton(icon: Icons.image_outlined, onTap: _pickImage, colors: colors), + const SizedBox(width: 8), + ValueListenableBuilder( + valueListenable: _controller, + builder: (_, state, __) { + final isOn = state.torchState == TorchState.on; + return _actionButton( + icon: isOn ? Icons.flash_on : Icons.flash_off, + onTap: _controller.toggleTorch, + colors: colors, + ); + }, + ), + ], + ), + ), + Positioned( + bottom: 58, + left: 24, + right: 24, + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: GlassContainer( + asset: GlassContainer.wideAsset, + child: Text( + 'Cancel', + textAlign: TextAlign.center, + style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _actionButton({required IconData icon, required VoidCallback onTap, required AppColorsV2 colors}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 20, color: colors.textPrimary), + ), + ); + } +} + +class _OverlayPainter extends CustomPainter { + final double frameSize; + final Size screenSize; + + _OverlayPainter({required this.frameSize, required this.screenSize}); + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + final frameRect = Rect.fromCenter( + center: Offset(size.width / 2, size.height / 2), + width: frameSize, + height: frameSize, + ); + final path = Path() + ..addRect(rect) + ..addRRect(RRect.fromRectAndRadius(frameRect, const Radius.circular(16))); + path.fillType = PathFillType.evenOdd; + canvas.drawPath(path, Paint()..color = Colors.black.withValues(alpha: 0.6)); + } + + @override + bool shouldRepaint(_OverlayPainter old) => frameSize != old.frameSize; +} + +class _ScanFrame extends StatelessWidget { + final double size; + const _ScanFrame({required this.size}); + + @override + Widget build(BuildContext context) { + final color = Colors.white.withValues(alpha: 0.92); + return SizedBox( + width: size, + height: size, + child: Stack( + children: [ + _corner(top: true, left: true, color: color), + _corner(top: true, left: false, color: color), + _corner(top: false, left: true, color: color), + _corner(top: false, left: false, color: color), + ], + ), + ); + } + + Widget _corner({required bool top, required bool left, required Color color}) { + return Positioned( + top: top ? 0 : null, + bottom: top ? null : 0, + left: left ? 0 : null, + right: left ? null : 0, + child: SizedBox( + width: 41, + height: 41, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: top && left ? const Radius.circular(16) : Radius.zero, + topRight: top && !left ? const Radius.circular(16) : Radius.zero, + bottomLeft: !top && left ? const Radius.circular(16) : Radius.zero, + bottomRight: !top && !left ? const Radius.circular(16) : Radius.zero, + ), + border: Border( + top: top ? BorderSide(color: color, width: 1.6) : BorderSide.none, + bottom: !top ? BorderSide(color: color, width: 1.6) : BorderSide.none, + left: left ? BorderSide(color: color, width: 1.6) : BorderSide.none, + right: !left ? BorderSide(color: color, width: 1.6) : BorderSide.none, + ), + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index d11a066f..cc6f8cf4 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/main/screens/send/send_providers.dart'; import 'package:resonance_network_wallet/features/main/screens/send/send_screen_logic.dart'; @@ -117,7 +117,7 @@ class _SendSheetState extends ConsumerState { Future _scanQr() async { final address = await Navigator.push( context, - MaterialPageRoute(fullscreenDialog: true, builder: (_) => const _QrScanPage()), + MaterialPageRoute(fullscreenDialog: true, builder: (_) => const QrScannerPage()), ); if (address != null && mounted) { _recipientController.text = address; @@ -511,63 +511,3 @@ void showSendSheetV2(BuildContext context, {String? address}) { ); } -class _QrScanPage extends StatefulWidget { - const _QrScanPage(); - - @override - State<_QrScanPage> createState() => _QrScanPageState(); -} - -class _QrScanPageState extends State<_QrScanPage> { - final _controller = MobileScannerController(); - bool _scanned = false; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - MobileScanner( - controller: _controller, - onDetect: (capture) { - if (_scanned) return; - for (final barcode in capture.barcodes) { - final v = barcode.rawValue; - if (v != null && v.isNotEmpty) { - _scanned = true; - Navigator.pop(context, v); - return; - } - } - }, - ), - Positioned( - bottom: 60, - left: 0, - right: 0, - child: Center( - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), - decoration: BoxDecoration(color: const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(14)), - child: const Text( - 'Cancel', - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), - ), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index f3799902..5ab798c8 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; @@ -108,18 +108,15 @@ class _SwapScreenState extends State { } } - void _scanQr() { - Navigator.push( + void _scanQr() async { + final address = await Navigator.push( context, - MaterialPageRoute( - builder: (_) => _QrScanPage( - onScanned: (v) { - _addressController.text = v; - Navigator.pop(context); - }, - ), - ), + MaterialPageRoute(builder: (_) => const QrScannerPage()), ); + if (address != null && mounted) { + _addressController.text = address; + setState(() {}); + } } @override @@ -463,156 +460,3 @@ class _SwapScreenState extends State { } } -class _QrScanPage extends StatefulWidget { - final ValueChanged onScanned; - const _QrScanPage({required this.onScanned}); - - @override - State<_QrScanPage> createState() => _QrScanPageState(); -} - -class _QrScanPageState extends State<_QrScanPage> { - final _controller = MobileScannerController(); - bool _scanned = false; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final colors = context.colors; - final frameSize = (MediaQuery.of(context).size.width - 111.5).clamp(220.0, 280.0); - - return Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - MobileScanner( - controller: _controller, - onDetect: (capture) { - if (_scanned) return; - final code = capture.barcodes.firstOrNull?.rawValue; - if (code != null && code.isNotEmpty) { - _scanned = true; - widget.onScanned(code); - } - }, - ), - ColoredBox(color: Colors.black.withValues(alpha: 0.24)), - Center(child: _ScanFrame(size: frameSize)), - Positioned( - left: 0, - right: 0, - bottom: 228, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _scanActionButton(icon: Icons.image_outlined, onTap: () {}, colors: colors), - const SizedBox(width: 8), - ValueListenableBuilder( - valueListenable: _controller, - builder: (_, state, __) { - final isOn = state.torchState == TorchState.on; - return _scanActionButton( - icon: isOn ? Icons.flash_on : Icons.flash_off, - onTap: _controller.toggleTorch, - colors: colors, - ); - }, - ), - ], - ), - ), - Positioned( - bottom: 58, - left: 24, - right: 24, - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 20), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), - border: Border.all(color: Colors.white.withValues(alpha: 0.44), width: 0.9), - borderRadius: BorderRadius.circular(14), - ), - child: Center( - child: Text( - 'Cancel', - style: TextStyle(color: colors.textPrimary, fontSize: 16, fontWeight: FontWeight.w500), - ), - ), - ), - ), - ), - ], - ), - ); - } - - Widget _scanActionButton({required IconData icon, required VoidCallback onTap, required AppColorsV2 colors}) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), - child: Icon(icon, size: 20, color: colors.textPrimary), - ), - ); - } -} - -class _ScanFrame extends StatelessWidget { - final double size; - const _ScanFrame({required this.size}); - - @override - Widget build(BuildContext context) { - final corner = Colors.white.withValues(alpha: 0.92); - return SizedBox( - width: size, - height: size, - child: Stack( - children: [ - _corner(top: true, left: true, color: corner), - _corner(top: true, left: false, color: corner), - _corner(top: false, left: true, color: corner), - _corner(top: false, left: false, color: corner), - ], - ), - ); - } - - Widget _corner({required bool top, required bool left, required Color color}) { - return Positioned( - top: top ? 0 : null, - bottom: top ? null : 0, - left: left ? 0 : null, - right: left ? null : 0, - child: SizedBox( - width: 41, - height: 41, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: top && left ? const Radius.circular(16) : Radius.zero, - topRight: top && !left ? const Radius.circular(16) : Radius.zero, - bottomLeft: !top && left ? const Radius.circular(16) : Radius.zero, - bottomRight: !top && !left ? const Radius.circular(16) : Radius.zero, - ), - border: Border( - top: top ? BorderSide(color: color, width: 1.6) : BorderSide.none, - bottom: !top ? BorderSide(color: color, width: 1.6) : BorderSide.none, - left: left ? BorderSide(color: color, width: 1.6) : BorderSide.none, - right: !left ? BorderSide(color: color, width: 1.6) : BorderSide.none, - ), - ), - ), - ), - ); - } -} diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index b6cd5527..f604a6d3 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -433,6 +433,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" firebase_core: dependency: "direct main" description: @@ -809,6 +841,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" + url: "https://pub.dev" + source: hosted + version: "0.8.13+13" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: "direct main" description: @@ -1869,5 +1965,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 9e048be6..2c09fe2b 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: glass_kit: ^4.0.2 firebase_core: ^4.4.0 firebase_messaging: ^16.1.1 + image_picker: ^1.2.1 dev_dependencies: flutter_test: From 004ff9d4d1c20f0fab60e2d22008639d2e6fdee3 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Thu, 19 Feb 2026 13:18:51 +0800 Subject: [PATCH 20/20] format and linter --- mobile-app/lib/v2/components/qr_scanner_page.dart | 11 +++-------- mobile-app/lib/v2/components/token_icon.dart | 4 ++-- mobile-app/lib/v2/screens/send/send_sheet.dart | 1 - mobile-app/lib/v2/screens/swap/swap_screen.dart | 6 +----- .../lib/v2/screens/swap/token_picker_sheet.dart | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart index b823177d..472d3ddd 100644 --- a/mobile-app/lib/v2/components/qr_scanner_page.dart +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -39,9 +39,7 @@ class _QrScannerPageState extends State { if (capture != null) { _onDetect(capture); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No QR code found in image')), - ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No QR code found in image'))); } } @@ -73,7 +71,7 @@ class _QrScannerPageState extends State { const SizedBox(width: 8), ValueListenableBuilder( valueListenable: _controller, - builder: (_, state, __) { + builder: (_, state, _) { final isOn = state.torchState == TorchState.on; return _actionButton( icon: isOn ? Icons.flash_on : Icons.flash_off, @@ -112,10 +110,7 @@ class _QrScannerPageState extends State { child: Container( width: 40, height: 40, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 20, color: colors.textPrimary), ), ); diff --git a/mobile-app/lib/v2/components/token_icon.dart b/mobile-app/lib/v2/components/token_icon.dart index ce4e7542..885ccdec 100644 --- a/mobile-app/lib/v2/components/token_icon.dart +++ b/mobile-app/lib/v2/components/token_icon.dart @@ -30,7 +30,7 @@ class TokenIcon extends StatelessWidget { ? Image.network( iconUrl, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _fallback(context, token, colors, text), + errorBuilder: (_, _, _) => _fallback(context, token, colors, text), ) : _fallback(context, token, colors, text), ), @@ -46,7 +46,7 @@ class TokenIcon extends StatelessWidget { ? Image.network( networkIconUrl, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _networkFallback(context, token, colors, text), + errorBuilder: (_, _, _) => _networkFallback(context, token, colors, text), ) : _networkFallback(context, token, colors, text), ), diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index cc6f8cf4..fc7a5305 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -510,4 +510,3 @@ void showSendSheetV2(BuildContext context, {String? address}) { ), ); } - diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index 5ab798c8..e8873193 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -109,10 +109,7 @@ class _SwapScreenState extends State { } void _scanQr() async { - final address = await Navigator.push( - context, - MaterialPageRoute(builder: (_) => const QrScannerPage()), - ); + final address = await Navigator.push(context, MaterialPageRoute(builder: (_) => const QrScannerPage())); if (address != null && mounted) { _addressController.text = address; setState(() {}); @@ -459,4 +456,3 @@ class _SwapScreenState extends State { ); } } - diff --git a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart index 990620f3..89dd9f54 100644 --- a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart @@ -17,7 +17,7 @@ Future showTokenPickerSheet( barrierDismissible: true, barrierLabel: 'Select Token', barrierColor: Colors.transparent, - pageBuilder: (_, __, ___) => _TokenPickerContent(loadTokens: loadTokens, current: current), + pageBuilder: (_, _, _) => _TokenPickerContent(loadTokens: loadTokens, current: current), ); }