Files
arbiter/docs/superpowers/specs/2026-03-28-grant-grid-view-design.md
2026-03-29 00:37:58 +01:00

5.6 KiB

Grant Grid View — Design Spec

Date: 2026-03-28

Overview

Add a "Grants" dashboard tab to the Flutter user-agent app that displays all EVM grants as a card-based grid. Each card shows a compact summary (type, chain, wallet address, client name) with a revoke action. The tab integrates into the existing AdaptiveScaffold navigation alongside Wallets, Clients, and About.

Scope

  • New walletAccessListProvider for fetching wallet access entries with their DB row IDs
  • New EvmGrantsScreen as a dashboard tab
  • Grant card widget with enriched display (type, chain, wallet, client)
  • Revoke action wired to existing executeRevokeEvmGrant mutation
  • Dashboard tab bar and router updated
  • New token-transfer accent color added to Palette

Out of scope: Fixing grant creation (separate task).


Data Layer

walletAccessListProvider

File: useragent/lib/providers/sdk_clients/wallet_access_list.dart

  • @riverpod class, watches connectionManagerProvider.future
  • Returns List<SdkClientWalletAccess>? (null when not connected)
  • Each entry: .id (wallet_access_id), .access.walletId, .access.sdkClientId
  • Exposes a refresh() method following the same pattern as EvmGrants.refresh()

Enrichment at render time (Approach A)

The EvmGrantsScreen watches four providers:

  1. evmGrantsProvider — the grant list
  2. walletAccessListProvider — to resolve wallet_access_id → (wallet_id, sdk_client_id)
  3. evmProvider — to resolve wallet_id → wallet address
  4. sdkClientsProvider — to resolve sdk_client_id → client name

All lookups are in-memory Maps built inside the build method; no extra model class needed.

Fallbacks:

  • Wallet address not found → "Access #N" where N is the wallet_access_id
  • Client name not found → "Client #N" where N is the sdk_client_id

Route Structure

/dashboard
  /evm          ← existing (Wallets tab)
  /clients      ← existing (Clients tab)
  /grants       ← NEW (Grants tab)
  /about        ← existing

/evm-grants/create  ← existing push route (unchanged)

Changes to router.dart

Add inside dashboard children:

AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),

Changes to dashboard.dart

Add to routes list:

const EvmGrantsRoute()

Add NavigationDestination:

NavigationDestination(
  icon: Icon(Icons.policy_outlined),
  selectedIcon: Icon(Icons.policy),
  label: 'Grants',
),

Screen: EvmGrantsScreen

File: useragent/lib/screens/dashboard/evm/grants/grants.dart

Scaffold
└─ SafeArea
   └─ RefreshIndicator.adaptive (refreshes evmGrantsProvider + walletAccessListProvider)
      └─ ListView (BouncingScrollPhysics + AlwaysScrollableScrollPhysics)
         ├─ PageHeader
         │    title: 'EVM Grants'
         │    isBusy: evmGrantsProvider.isLoading
         │    actions: [CreateGrantButton, RefreshButton]
         ├─ SizedBox(height: 1.8.h)
         └─ <content>

State handling

Matches the pattern from EvmScreen and ClientsScreen:

State Display
Loading (no data yet) _StatePanel with spinner, "Loading grants"
Error _StatePanel with coral icon, error message, Retry button
No connection _StatePanel, "No active server connection"
Empty list _StatePanel, "No grants yet", with Create Grant shortcut
Data Column of _GrantCard widgets

Header actions

CreateGrantButton: FilledButton.icon with Icons.add_rounded, pushes CreateEvmGrantRoute() via context.router.push(...).

RefreshButton: OutlinedButton.icon with Icons.refresh, calls ref.read(evmGrantsProvider.notifier).refresh().


Grant Card: _GrantCard

Layout:

Container (rounded 24, Palette.cream bg, Palette.line border)
└─ IntrinsicHeight > Row
   ├─ Accent strip (0.8.w wide, full height, rounded left)
   └─ Padding > Column
        ├─ Row 1: TypeBadge + ChainChip + Spacer + RevokeButton
        └─ Row 2: WalletText + "·" + ClientText

Accent color by grant type:

  • Ether transfer → Palette.coral
  • Token transfer → Palette.token (new entry in Palette — indigo, e.g. Color(0xFF5C6BC0))

TypeBadge: Small pill container with accent color background at 15% opacity, accent-colored text. Label: 'Ether' or 'Token'.

ChainChip: Small container: 'Chain ${grant.shared.chainId}', muted ink color.

WalletText: Short hex address (0xabc...def) from wallet lookup, bodySmall, monospace font family.

ClientText: Client name from sdkClientsProvider lookup, or fallback string. bodySmall, muted ink.

RevokeButton:

  • OutlinedButton with Icons.block_rounded icon, label 'Revoke'
  • foregroundColor: Palette.coral, side: BorderSide(color: Palette.coral.withValues(alpha: 0.4))
  • Disabled (replaced with CircularProgressIndicator) while revokeEvmGrantMutation is pending — note: this is a single global mutation, so all revoke buttons disable while any revoke is in flight
  • On press: calls executeRevokeEvmGrant(ref, grantId: grant.id); shows SnackBar on error

Adaptive Sizing

All sizing uses sizer units (1.h, 1.w, etc.). No hardcoded pixel values.


Files to Create / Modify

File Action
lib/theme/palette.dart Modify — add Palette.token color
lib/providers/sdk_clients/wallet_access_list.dart Create
lib/screens/dashboard/evm/grants/grants.dart Create
lib/router.dart Modify — add grants route to dashboard children
lib/screens/dashboard.dart Modify — add tab to routes list and NavigationDestinations