import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/providers/evm/evm_grants.dart'; import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart'; import 'package:arbiter/router.gr.dart'; import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart'; import 'package:arbiter/theme/palette.dart'; import 'package:arbiter/widgets/page_header.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sizer/sizer.dart'; String _formatError(Object error) { final message = error.toString(); if (message.startsWith('Exception: ')) { return message.substring('Exception: '.length); } return message; } // ─── State panel ────────────────────────────────────────────────────────────── class _StatePanel extends StatelessWidget { const _StatePanel({ required this.icon, required this.title, required this.body, this.actionLabel, this.onAction, this.busy = false, }); final IconData icon; final String title; final String body; final String? actionLabel; final Future Function()? onAction; final bool busy; @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.8.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (busy) SizedBox( width: 2.8.h, height: 2.8.h, child: const CircularProgressIndicator(strokeWidth: 2.5), ) else Icon(icon, size: 34, color: Palette.coral), SizedBox(height: 1.8.h), Text( title, style: theme.textTheme.headlineSmall?.copyWith( color: Palette.ink, fontWeight: FontWeight.w800, ), ), SizedBox(height: 1.h), Text( body, style: theme.textTheme.bodyLarge?.copyWith( color: Palette.ink.withValues(alpha: 0.72), height: 1.5, ), ), if (actionLabel != null && onAction != null) ...[ SizedBox(height: 2.h), OutlinedButton.icon( onPressed: () => onAction!(), icon: const Icon(Icons.refresh), label: Text(actionLabel!), ), ], ], ), ), ); } } // ─── Grant list ─────────────────────────────────────────────────────────────── class _GrantList extends StatelessWidget { const _GrantList({required this.grants}); final List grants; @override Widget build(BuildContext context) { return Column( children: [ for (var i = 0; i < grants.length; i++) Padding( padding: EdgeInsets.only( bottom: i == grants.length - 1 ? 0 : 1.8.h, ), child: GrantCard(grant: grants[i]), ), ], ); } } // ─── Screen ─────────────────────────────────────────────────────────────────── @RoutePage() class EvmGrantsScreen extends ConsumerWidget { const EvmGrantsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // Screen watches only the grant list for top-level state decisions final grantsAsync = ref.watch(evmGrantsProvider); Future refresh() async { ref.invalidate(walletAccessListProvider); ref.invalidate(evmGrantsProvider); } void showMessage(String message) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), behavior: SnackBarBehavior.floating), ); } Future safeRefresh() async { try { await refresh(); } catch (e) { showMessage(_formatError(e)); } } final grantsState = grantsAsync.asData?.value; final grants = grantsState?.grants; final content = switch (grantsAsync) { AsyncLoading() when grantsState == null => const _StatePanel( icon: Icons.hourglass_top, title: 'Loading grants', body: 'Pulling grant registry from Arbiter.', busy: true, ), AsyncError(:final error) => _StatePanel( icon: Icons.sync_problem, title: 'Grant registry unavailable', body: _formatError(error), actionLabel: 'Retry', onAction: safeRefresh, ), AsyncData(:final value) when value == null => _StatePanel( icon: Icons.portable_wifi_off, title: 'No active server connection', body: 'Reconnect to Arbiter to list EVM grants.', actionLabel: 'Refresh', onAction: safeRefresh, ), _ when grants != null && grants.isEmpty => _StatePanel( icon: Icons.policy_outlined, title: 'No grants yet', body: 'Create a grant to allow SDK clients to sign transactions.', actionLabel: 'Create grant', onAction: () async => context.router.push(const CreateEvmGrantRoute()), ), _ => _GrantList(grants: grants ?? const []), }; return Scaffold( body: SafeArea( child: RefreshIndicator.adaptive( color: Palette.ink, backgroundColor: Colors.white, onRefresh: safeRefresh, child: ListView( physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), ), padding: EdgeInsets.fromLTRB(2.4.w, 2.4.h, 2.4.w, 3.2.h), children: [ PageHeader( title: 'EVM Grants', isBusy: grantsAsync.isLoading, actions: [ FilledButton.icon( onPressed: () => context.router.push(const CreateEvmGrantRoute()), icon: const Icon(Icons.add_rounded), label: const Text('Create grant'), ), SizedBox(width: 1.w), OutlinedButton.icon( onPressed: safeRefresh, 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'), ), ], ), SizedBox(height: 1.8.h), content, ], ), ), ), ); } }