From 1abb5fa00622b861503cb22d1c03d52be897e8c4 Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 26 Mar 2026 20:46:15 +0100 Subject: [PATCH] refactor(useragent::evm::table): broke down into more widgets --- useragent/lib/providers/evm/evm.dart | 25 +- useragent/lib/providers/evm/evm.g.dart | 2 +- useragent/lib/screens/dashboard/evm/evm.dart | 295 +----------------- .../screens/dashboard/evm/wallets/header.dart | 98 ++++++ .../screens/dashboard/evm/wallets/table.dart | 209 +++++++++++++ 5 files changed, 337 insertions(+), 292 deletions(-) create mode 100644 useragent/lib/screens/dashboard/evm/wallets/header.dart create mode 100644 useragent/lib/screens/dashboard/evm/wallets/table.dart diff --git a/useragent/lib/providers/evm/evm.dart b/useragent/lib/providers/evm/evm.dart index 7cb89f3..f32386f 100644 --- a/useragent/lib/providers/evm/evm.dart +++ b/useragent/lib/providers/evm/evm.dart @@ -1,6 +1,8 @@ -import 'package:arbiter/features/connection/evm.dart'; +import 'package:arbiter/features/connection/evm.dart' as evm; import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/providers/connection/connection_manager.dart'; +import 'package:hooks_riverpod/experimental/mutation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'evm.g.dart'; @@ -14,7 +16,7 @@ class Evm extends _$Evm { return null; } - return listEvmWallets(connection); + return evm.listEvmWallets(connection); } Future refreshWallets() async { @@ -25,16 +27,21 @@ class Evm extends _$Evm { } state = const AsyncLoading(); - state = await AsyncValue.guard(() => listEvmWallets(connection)); + state = await AsyncValue.guard(() => evm.listEvmWallets(connection)); } +} - Future createWallet() async { - final connection = await ref.read(connectionManagerProvider.future); +final createEvmWallet = Mutation(); + +Future executeCreateEvmWallet(MutationTarget target) async { + return await createEvmWallet.run(target, (tsx) async { + final connection = await tsx.get(connectionManagerProvider.future); if (connection == null) { throw Exception('Not connected to the server.'); } - await createEvmWallet(connection); - state = await AsyncValue.guard(() => listEvmWallets(connection)); - } -} + await evm.createEvmWallet(connection); + + await tsx.get(evmProvider.notifier).refreshWallets(); + }); +} \ No newline at end of file diff --git a/useragent/lib/providers/evm/evm.g.dart b/useragent/lib/providers/evm/evm.g.dart index 8c5bb02..ab490d7 100644 --- a/useragent/lib/providers/evm/evm.g.dart +++ b/useragent/lib/providers/evm/evm.g.dart @@ -33,7 +33,7 @@ final class EvmProvider Evm create() => Evm(); } -String _$evmHash() => r'f5d05bfa7b820d0b96026a47ca47702a3793af5d'; +String _$evmHash() => r'ca2c9736065c5dc7cc45d8485000dd85dfbfa572'; abstract class _$Evm extends $AsyncNotifier?> { FutureOr?> build(); diff --git a/useragent/lib/screens/dashboard/evm/evm.dart b/useragent/lib/screens/dashboard/evm/evm.dart index ea407a9..743b369 100644 --- a/useragent/lib/screens/dashboard/evm/evm.dart +++ b/useragent/lib/screens/dashboard/evm/evm.dart @@ -1,13 +1,11 @@ -import 'dart:math' as math; - import 'package:arbiter/proto/evm.pb.dart'; +import 'package:arbiter/screens/dashboard/evm/wallets/header.dart'; +import 'package:arbiter/screens/dashboard/evm/wallets/table.dart'; import 'package:arbiter/theme/palette.dart'; -import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/widgets/page_header.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sizer/sizer.dart'; @@ -17,13 +15,10 @@ class EvmScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final walletsAsync = ref.watch(evmProvider); - final isCreating = useState(false); + final evm = ref.watch(evmProvider); - final wallets = walletsAsync.asData?.value; + final wallets = evm.asData?.value; final loadedWallets = wallets ?? const []; - final isConnected = - ref.watch(connectionManagerProvider).asData?.value != null; void showMessage(String message) { if (!context.mounted) return; @@ -35,28 +30,12 @@ class EvmScreen extends HookConsumerWidget { Future refreshWallets() async { try { await ref.read(evmProvider.notifier).refreshWallets(); - } catch (error) { - showMessage(_formatError(error)); + } catch (e) { + showMessage('Failed to refresh wallets: ${_formatError(e)}'); } } - Future createWallet() async { - if (isCreating.value) { - return; - } - - isCreating.value = true; - try { - await ref.read(evmProvider.notifier).createWallet(); - showMessage('Wallet created.'); - } catch (error) { - showMessage(_formatError(error)); - } finally { - isCreating.value = false; - } - } - - final content = switch (walletsAsync) { + final content = switch (evm) { AsyncLoading() when wallets == null => const _StatePanel( icon: Icons.hourglass_top, title: 'Loading wallets', @@ -70,22 +49,14 @@ class EvmScreen extends HookConsumerWidget { actionLabel: 'Retry', onAction: refreshWallets, ), - _ when !isConnected => _StatePanel( + AsyncData(:final value) when value == null => _StatePanel( icon: Icons.portable_wifi_off, title: 'No active server connection', body: 'Reconnect to Arbiter to list or create EVM wallets.', actionLabel: 'Refresh', - onAction: refreshWallets, + onAction: () => refreshWallets(), ), - _ when loadedWallets.isEmpty => _StatePanel( - icon: Icons.account_balance_wallet_outlined, - title: 'No wallets yet', - body: - 'Create the first vault-backed wallet to start building your EVM registry.', - actionLabel: isCreating.value ? 'Creating...' : 'Create wallet', - onAction: isCreating.value ? null : createWallet, - ), - _ => _WalletTable(wallets: loadedWallets), + _ => WalletTable(wallets: loadedWallets), }; return Scaffold( @@ -102,47 +73,11 @@ class EvmScreen extends HookConsumerWidget { children: [ PageHeader( title: 'EVM Wallet Vault', - isBusy: walletsAsync.isLoading, + isBusy: evm.isLoading, actions: [ - FilledButton.icon( - onPressed: isCreating.value ? null : () => createWallet(), - style: FilledButton.styleFrom( - backgroundColor: Palette.ink, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: 1.4.w, - vertical: 1.2.h, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - icon: isCreating.value - ? SizedBox( - width: 1.6.h, - height: 1.6.h, - child: CircularProgressIndicator(strokeWidth: 2.2), - ) - : const Icon(Icons.add_circle_outline, size: 18), - label: Text(isCreating.value ? 'Creating...' : 'Create'), - ), + const CreateWalletButton(), SizedBox(width: 1.w), - OutlinedButton.icon( - onPressed: () => refreshWallets(), - style: OutlinedButton.styleFrom( - foregroundColor: Palette.ink, - side: BorderSide(color: Palette.line), - padding: EdgeInsets.symmetric( - horizontal: 1.4.w, - vertical: 1.2.h, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - ), - icon: const Icon(Icons.refresh, size: 18), - label: const Text('Refresh'), - ), + const RefreshWalletButton(), ], ), SizedBox(height: 1.8.h), @@ -155,197 +90,6 @@ class EvmScreen extends HookConsumerWidget { } } -double get _accentStripWidth => 0.8.w; -double get _cellHorizontalPadding => 1.8.w; -double get _walletColumnWidth => 18.w; -double get _columnGap => 1.8.w; -double get _tableMinWidth => 72.w; - -class _WalletTable extends StatelessWidget { - const _WalletTable({required this.wallets}); - - final List wallets; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Palette.cream.withValues(alpha: 0.92), - border: Border.all(color: Palette.line), - ), - child: Padding( - padding: EdgeInsets.all(2.h), - child: LayoutBuilder( - builder: (context, constraints) { - final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Managed wallets', - style: theme.textTheme.titleLarge?.copyWith( - color: Palette.ink, - fontWeight: FontWeight.w800, - ), - ), - SizedBox(height: 0.6.h), - Text( - 'Every address here is generated and held by Arbiter.', - style: theme.textTheme.bodyMedium?.copyWith( - color: Palette.ink.withValues(alpha: 0.70), - height: 1.4, - ), - ), - SizedBox(height: 1.6.h), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: tableWidth, - child: Column( - children: [ - const _WalletTableHeader(), - SizedBox(height: 1.h), - for (var i = 0; i < wallets.length; i++) - Padding( - padding: EdgeInsets.only( - bottom: i == wallets.length - 1 ? 0 : 1.h, - ), - child: _WalletTableRow( - wallet: wallets[i], - index: i, - ), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -class _WalletTableHeader extends StatelessWidget { - const _WalletTableHeader(); - - @override - Widget build(BuildContext context) { - final style = Theme.of(context).textTheme.labelLarge?.copyWith( - color: Palette.ink.withValues(alpha: 0.72), - fontWeight: FontWeight.w800, - letterSpacing: 0.3, - ); - - return Container( - padding: EdgeInsets.symmetric(vertical: 1.4.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Palette.ink.withValues(alpha: 0.04), - ), - child: Row( - children: [ - SizedBox(width: _accentStripWidth + _cellHorizontalPadding), - SizedBox( - width: _walletColumnWidth, - child: Text('Wallet', style: style), - ), - SizedBox(width: _columnGap), - Expanded(child: Text('Address', style: style)), - SizedBox(width: _cellHorizontalPadding), - ], - ), - ); - } -} - -class _WalletTableRow extends StatelessWidget { - const _WalletTableRow({required this.wallet, required this.index}); - - final WalletEntry wallet; - final int index; - - @override - Widget build(BuildContext context) { - final accent = _accentColor(wallet.address); - final address = _hexAddress(wallet.address); - final rowHeight = 5.h; - final walletStyle = Theme.of( - context, - ).textTheme.bodyLarge?.copyWith(color: Palette.ink); - final addressStyle = Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: Palette.ink); - - return Container( - height: rowHeight, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - color: accent.withValues(alpha: 0.10), - border: Border.all(color: accent.withValues(alpha: 0.28)), - ), - child: Row( - children: [ - Container( - width: _accentStripWidth, - height: rowHeight, - decoration: BoxDecoration( - color: accent, - borderRadius: const BorderRadius.horizontal( - left: Radius.circular(18), - ), - ), - ), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: _cellHorizontalPadding), - child: Row( - children: [ - SizedBox( - width: _walletColumnWidth, - child: Row( - children: [ - Container( - width: 1.2.h, - height: 1.2.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: accent, - ), - ), - SizedBox(width: 1.w), - Text( - 'Wallet ${(index + 1).toString().padLeft(2, '0')}', - style: walletStyle, - ), - ], - ), - ), - SizedBox(width: _columnGap), - Expanded( - child: Text( - address, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: addressStyle, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} - class _StatePanel extends StatelessWidget { const _StatePanel({ required this.icon, @@ -417,19 +161,6 @@ class _StatePanel extends StatelessWidget { } } -String _hexAddress(List bytes) { - final hex = bytes - .map((byte) => byte.toRadixString(16).padLeft(2, '0')) - .join(); - return '0x$hex'; -} - -Color _accentColor(List bytes) { - final seed = bytes.fold(0, (value, byte) => value + byte); - final hue = (seed * 17) % 360; - return HSLColor.fromAHSL(1, hue.toDouble(), 0.68, 0.54).toColor(); -} - String _formatError(Object error) { final message = error.toString(); if (message.startsWith('Exception: ')) { diff --git a/useragent/lib/screens/dashboard/evm/wallets/header.dart b/useragent/lib/screens/dashboard/evm/wallets/header.dart new file mode 100644 index 0000000..646d5ea --- /dev/null +++ b/useragent/lib/screens/dashboard/evm/wallets/header.dart @@ -0,0 +1,98 @@ +import 'package:arbiter/providers/evm/evm.dart'; +import 'package:arbiter/theme/palette.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/experimental/mutation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:sizer/sizer.dart'; + + +class CreateWalletButton extends ConsumerWidget { + const CreateWalletButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final createWallet = ref.watch(createEvmWallet); + final isCreating = createWallet is MutationPending; + + Future handleCreateWallet() async { + try { + await executeCreateEvmWallet(ref); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('New wallet created successfully.'), + behavior: SnackBarBehavior.floating, + ), + ); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to create wallet: ${_formatError(e)}'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + return FilledButton.icon( + onPressed: isCreating ? null : () => handleCreateWallet(), + style: FilledButton.styleFrom( + backgroundColor: Palette.ink, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 1.4.w, vertical: 1.2.h), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + icon: isCreating + ? SizedBox( + width: 1.6.h, + height: 1.6.h, + child: CircularProgressIndicator(strokeWidth: 2.2), + ) + : const Icon(Icons.add_circle_outline, size: 18), + label: Text(isCreating ? 'Creating...' : 'Create'), + ); + } +} + +class RefreshWalletButton extends ConsumerWidget { + const RefreshWalletButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Future handleRefreshWallets() async { + try { + await ref.read(evmProvider.notifier).refreshWallets(); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to refresh wallets: ${_formatError(e)}'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + + return OutlinedButton.icon( + onPressed: () => handleRefreshWallets(), + style: OutlinedButton.styleFrom( + foregroundColor: Palette.ink, + side: BorderSide(color: Palette.line), + padding: EdgeInsets.symmetric(horizontal: 1.4.w, vertical: 1.2.h), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + icon: const Icon(Icons.refresh, size: 18), + label: const Text('Refresh'), + ); + } +} + + +String _formatError(Object error) { + final message = error.toString(); + if (message.startsWith('Exception: ')) { + return message.substring('Exception: '.length); + } + return message; +} diff --git a/useragent/lib/screens/dashboard/evm/wallets/table.dart b/useragent/lib/screens/dashboard/evm/wallets/table.dart new file mode 100644 index 0000000..1093dfd --- /dev/null +++ b/useragent/lib/screens/dashboard/evm/wallets/table.dart @@ -0,0 +1,209 @@ +import 'dart:math' as math; +import 'package:arbiter/proto/evm.pb.dart'; +import 'package:arbiter/theme/palette.dart'; +import 'package:flutter/material.dart'; +import 'package:sizer/sizer.dart'; + +double get _accentStripWidth => 0.8.w; +double get _cellHorizontalPadding => 1.8.w; +double get _walletColumnWidth => 18.w; +double get _columnGap => 1.8.w; +double get _tableMinWidth => 72.w; + +String _hexAddress(List bytes) { + final hex = bytes + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(); + return '0x$hex'; +} + +Color _accentColor(List bytes) { + final seed = bytes.fold(0, (value, byte) => value + byte); + final hue = (seed * 17) % 360; + return HSLColor.fromAHSL(1, hue.toDouble(), 0.68, 0.54).toColor(); +} + +class WalletTable extends StatelessWidget { + const WalletTable({super.key, required this.wallets}); + + final List wallets; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Palette.cream.withValues(alpha: 0.92), + border: Border.all(color: Palette.line), + ), + child: Padding( + padding: EdgeInsets.all(2.h), + child: LayoutBuilder( + builder: (context, constraints) { + final tableWidth = math.max(_tableMinWidth, constraints.maxWidth); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Managed wallets', + style: theme.textTheme.titleLarge?.copyWith( + color: Palette.ink, + fontWeight: FontWeight.w800, + ), + ), + SizedBox(height: 0.6.h), + Text( + 'Every address here is generated and held by Arbiter.', + style: theme.textTheme.bodyMedium?.copyWith( + color: Palette.ink.withValues(alpha: 0.70), + height: 1.4, + ), + ), + SizedBox(height: 1.6.h), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: tableWidth, + child: Column( + children: [ + const _WalletTableHeader(), + SizedBox(height: 1.h), + for (var i = 0; i < wallets.length; i++) + Padding( + padding: EdgeInsets.only( + bottom: i == wallets.length - 1 ? 0 : 1.h, + ), + child: _WalletTableRow( + wallet: wallets[i], + index: i, + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _WalletTableHeader extends StatelessWidget { + const _WalletTableHeader(); + + @override + Widget build(BuildContext context) { + final style = Theme.of(context).textTheme.labelLarge?.copyWith( + color: Palette.ink.withValues(alpha: 0.72), + fontWeight: FontWeight.w800, + letterSpacing: 0.3, + ); + + return Container( + padding: EdgeInsets.symmetric(vertical: 1.4.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Palette.ink.withValues(alpha: 0.04), + ), + child: Row( + children: [ + SizedBox(width: _accentStripWidth + _cellHorizontalPadding), + SizedBox( + width: _walletColumnWidth, + child: Text('Wallet', style: style), + ), + SizedBox(width: _columnGap), + Expanded(child: Text('Address', style: style)), + SizedBox(width: _cellHorizontalPadding), + ], + ), + ); + } +} + +class _WalletTableRow extends StatelessWidget { + const _WalletTableRow({required this.wallet, required this.index}); + + final WalletEntry wallet; + final int index; + + @override + Widget build(BuildContext context) { + final accent = _accentColor(wallet.address); + final address = _hexAddress(wallet.address); + final rowHeight = 5.h; + final walletStyle = Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: Palette.ink); + final addressStyle = Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Palette.ink); + + return Container( + height: rowHeight, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: accent.withValues(alpha: 0.10), + border: Border.all(color: accent.withValues(alpha: 0.28)), + ), + child: Row( + children: [ + Container( + width: _accentStripWidth, + height: rowHeight, + decoration: BoxDecoration( + color: accent, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(18), + ), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: _cellHorizontalPadding), + child: Row( + children: [ + SizedBox( + width: _walletColumnWidth, + child: Row( + children: [ + Container( + width: 1.2.h, + height: 1.2.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: accent, + ), + ), + SizedBox(width: 1.w), + Text( + 'Wallet ${(index + 1).toString().padLeft(2, '0')}', + style: walletStyle, + ), + ], + ), + ), + SizedBox(width: _columnGap), + Expanded( + child: Text( + address, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: addressStyle, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +}