From 716716f1425f6b6a556760f99e7e75532998ad87 Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Wed, 20 May 2026 16:41:46 +0100
Subject: [PATCH 1/7] fix: finalize streaming widget bounty coverage

---
 .../StreamingWidget.stories.tsx               | 428 ++++++++++++++++--
 .../src/CitizenClaimWidget.tsx                |   1 -
 packages/citizen-claim-widget/src/adapter.ts  |   1 -
 packages/core/src/provider.tsx                |   2 +-
 .../streaming-widget/src/StreamingWidget.tsx  | 161 ++++++-
 packages/streaming-widget/src/adapter.ts      |  26 +-
 packages/streaming-widget/src/index.ts        |   3 +-
 .../src/widgetRuntimeContract.ts              |   6 +
 tests/widgets/streaming-widget/states.spec.ts | 348 +++++---------
 .../test-results/sw-01-no-wallet.png          | Bin 0 -> 23601 bytes
 .../test-results/sw-02-tab-navigation.png     | Bin 0 -> 28625 bytes
 .../test-results/sw-03-wrong-chain.png        | Bin 0 -> 26244 bytes
 .../test-results/sw-04-loading-state.png      | Bin 0 -> 33722 bytes
 .../test-results/sw-05-empty-state.png        | Bin 0 -> 37856 bytes
 .../test-results/sw-06-error-state.png        | Bin 0 -> 42592 bytes
 .../test-results/sw-07-populated-streams.png  | Bin 0 -> 59318 bytes
 .../sw-08-create-update-invalid.png           | Bin 0 -> 87309 bytes
 .../sw-09-create-update-pending.png           | Bin 0 -> 87521 bytes
 .../sw-10-create-update-success.png           | Bin 0 -> 90112 bytes
 .../sw-11-create-update-failure.png           | Bin 0 -> 90313 bytes
 .../test-results/sw-12-pool-claim.png         | Bin 0 -> 21071 bytes
 .../test-results/sw-13-pool-claim-pending.png | Bin 0 -> 19886 bytes
 .../test-results/sw-14-pool-claim-success.png | Bin 0 -> 21782 bytes
 .../test-results/sw-15-pool-claim-error.png   | Bin 0 -> 23320 bytes
 .../test-results/sw-16-base-sup-reserve.png   | Bin 0 -> 28968 bytes
 .../sw-17-non-base-reserve-disabled.png       | Bin 0 -> 28625 bytes
 26 files changed, 670 insertions(+), 306 deletions(-)
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-02-tab-navigation.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-03-wrong-chain.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-04-loading-state.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-05-empty-state.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-06-error-state.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-16-base-sup-reserve.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-17-non-base-reserve-disabled.png

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 12520e8..990068a 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -1,6 +1,17 @@
 import React from 'react'
 import type { Meta, StoryObj } from '@storybook/react'
-import { StreamingWidget } from '@goodwidget/streaming-widget'
+import {
+  STREAMING_CHAINS,
+  StreamingWidget,
+  StreamingWidgetPreview,
+  type PoolMembershipItem,
+  type SetStreamFormState,
+  type StreamingWidgetAdapterActions,
+  type StreamingWidgetAdapterResult,
+  type StreamingWidgetAdapterState,
+  type StreamingWidgetTab,
+  type StreamListItem,
+} from '@goodwidget/streaming-widget'
 import { YStack } from '@goodwidget/ui'
 import {
   getInjectedEip1193Provider,
@@ -8,10 +19,147 @@ import {
 } from '../../fixtures/injectedEip1193'
 import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193'
 
-// ---------------------------------------------------------------------------
-// Story shell — renders the widget inside a fixed-width container that mirrors
-// the GoodWalletV2 sidebar / bottom-sheet form factor.
-// ---------------------------------------------------------------------------
+const DEMO_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
+const DEMO_RECEIVER = '0x1111111111111111111111111111111111111111'
+const DEMO_SENDER = '0x2222222222222222222222222222222222222222'
+const DEMO_TOKEN = '0x3333333333333333333333333333333333333333'
+const DEMO_POOL = '0x4444444444444444444444444444444444444444'
+
+const defaultForm: SetStreamFormState = {
+  receiver: '',
+  amount: '',
+  timeUnit: 'month',
+  flowRate: null,
+  validationError: null,
+}
+
+const validForm: SetStreamFormState = {
+  receiver: DEMO_RECEIVER,
+  amount: '42',
+  timeUnit: 'month',
+  flowRate: 16203703703703n,
+  validationError: null,
+}
+
+const invalidForm: SetStreamFormState = {
+  receiver: '0x123',
+  amount: '0',
+  timeUnit: 'month',
+  flowRate: null,
+  validationError: 'Recipient must be a valid Ethereum address (0x...).',
+}
+
+const sampleStreams: StreamListItem[] = [
+  {
+    id: 'outgoing-demo-stream',
+    sender: DEMO_ADDRESS,
+    receiver: DEMO_RECEIVER,
+    token: DEMO_TOKEN,
+    flowRate: 38580246913580n,
+    streamedSoFar: 15000000000000000000n,
+    createdAtTimestamp: 1767225600,
+    updatedAtTimestamp: 1767312000,
+    direction: 'outgoing',
+  },
+  {
+    id: 'incoming-demo-stream',
+    sender: DEMO_SENDER,
+    receiver: DEMO_ADDRESS,
+    token: DEMO_TOKEN,
+    flowRate: 19290123456790n,
+    streamedSoFar: 7800000000000000000n,
+    createdAtTimestamp: 1767139200,
+    updatedAtTimestamp: 1767312000,
+    direction: 'incoming',
+  },
+]
+
+const samplePools: PoolMembershipItem[] = [
+  {
+    poolId: DEMO_POOL,
+    poolToken: DEMO_TOKEN,
+    totalUnits: 250000000000000000000n,
+    claimableAmount: 12500000000000000000n,
+    totalAmountClaimed: 48000000000000000000n,
+    isConnected: false,
+  },
+]
+
+function createAdapter(
+  stateOverrides: Partial<StreamingWidgetAdapterState> = {},
+  actionOverrides: Partial<StreamingWidgetAdapterActions> = {},
+): StreamingWidgetAdapterResult {
+  const baseState: StreamingWidgetAdapterState = {
+    isConnected: true,
+    address: DEMO_ADDRESS,
+    chainId: STREAMING_CHAINS.CELO,
+    isWrongChain: false,
+    streams: sampleStreams,
+    streamsLoading: false,
+    streamsError: null,
+    streamHistory: sampleStreams,
+    streamHistoryLoading: false,
+    streamHistoryError: null,
+    pools: samplePools,
+    poolsLoading: false,
+    poolsError: null,
+    superTokenBalance: '128.50',
+    balanceLoading: false,
+    balanceError: null,
+    supReserveBalance: null,
+    supReserveLoading: false,
+    supReserveError: null,
+    setStreamForm: defaultForm,
+    setStreamStatus: 'idle',
+    setStreamError: null,
+    setStreamTxHash: null,
+    poolConnectStatus: {},
+    poolConnectError: {},
+  }
+
+  const actions: StreamingWidgetAdapterActions = {
+    connect: async () => {},
+    switchChain: async () => {},
+    refreshStreams: async () => {},
+    refreshStreamHistory: async () => {},
+    refreshPools: async () => {},
+    refreshBalance: async () => {},
+    updateSetStreamForm: () => {},
+    submitSetStream: async () => {},
+    resetSetStream: () => {},
+    connectToPool: async () => {},
+    disconnectFromPool: async () => {},
+    ...actionOverrides,
+  }
+
+  return {
+    state: { ...baseState, ...stateOverrides },
+    actions,
+  }
+}
+
+function PreviewStoryShell({
+  adapter,
+  dataTestId,
+  initialTab = 'streams',
+  initialStreamsFormOpen = false,
+}: {
+  adapter: StreamingWidgetAdapterResult
+  dataTestId: string
+  initialTab?: StreamingWidgetTab
+  initialStreamsFormOpen?: boolean
+}) {
+  return (
+    <YStack data-testid={dataTestId} style={{ width: 400, minHeight: '100vh' }}>
+      <StreamingWidgetPreview
+        adapter={adapter}
+        initialTab={initialTab}
+        initialStreamsFormOpen={initialStreamsFormOpen}
+      />
+    </YStack>
+  )
+}
+
 function StreamingWidgetStoryShell({
   provider,
   dataTestId,
@@ -36,22 +184,24 @@ const meta: Meta<typeof StreamingWidget> = {
 export default meta
 type Story = StoryObj<typeof meta>
 
-// ---------------------------------------------------------------------------
-// Injected wallet story — uses window.ethereum if present in the browser
-// ---------------------------------------------------------------------------
 function InjectedWalletStory() {
   const injectedProvider = getInjectedEip1193Provider()
   const usableProvider = isInjectedProviderUsable(injectedProvider)
 
   if (!usableProvider) {
     return (
-      <YStack data-testid="StreamingWidget-no-wallet" style={{ width: 400 }} gap="$3">
-        <strong>No injected wallet found</strong>
-        <span>
-          Install/enable MetaMask (or another EIP-1193 wallet) in this browser, then refresh
-          Storybook. The widget supports Celo (G$) and Base (SUP).
-        </span>
-      </YStack>
+      <PreviewStoryShell
+        adapter={createAdapter({
+          isConnected: false,
+          address: null,
+          chainId: null,
+          streams: [],
+          streamHistory: [],
+          pools: [],
+          superTokenBalance: null,
+        })}
+        dataTestId="StreamingWidget-no-injected-wallet"
+      />
     )
   }
 
@@ -63,9 +213,6 @@ function InjectedWalletStory() {
   )
 }
 
-// ---------------------------------------------------------------------------
-// Custodial fixture story — uses the pre-configured test wallet from the fixture
-// ---------------------------------------------------------------------------
 function CustodialLocalFixtureStory() {
   try {
     const provider = createCustodialEip1193Provider()
@@ -89,37 +236,234 @@ function CustodialLocalFixtureStory() {
   }
 }
 
-// ---------------------------------------------------------------------------
-// No-wallet story — demonstrates the connect-prompt state
-// ---------------------------------------------------------------------------
-function NoWalletStory() {
-  return (
-    <StreamingWidgetStoryShell
-      provider={undefined}
-      dataTestId="StreamingWidget-no-provider"
-    />
-  )
-}
-
-// ---------------------------------------------------------------------------
-// Story exports
-// ---------------------------------------------------------------------------
-
-/** Uses window.ethereum if available — shows the full connected experience. */
 export const InjectedWallet: Story = {
   render: () => <InjectedWalletStory />,
 }
 
-/**
- * Uses a pre-configured custodial test wallet backed by a local private key.
- * Starts on Celo (chain 42220) and uses the development environment.
- * The test key has no on-chain streaming history, so streams/pools lists will be empty.
- */
 export const CustodialLocalFixture: Story = {
   render: () => <CustodialLocalFixtureStory />,
 }
 
-/** No provider — shows the wallet-connection prompt for both Streams and Pools tabs. */
 export const NoWallet: Story = {
-  render: () => <NoWalletStory />,
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        isConnected: false,
+        address: null,
+        chainId: null,
+        streams: [],
+        streamHistory: [],
+        pools: [],
+        superTokenBalance: null,
+      })}
+      dataTestId="StreamingWidget-no-wallet"
+    />
+  ),
+}
+
+export const WrongChain: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        chainId: 1,
+        isWrongChain: true,
+      })}
+      dataTestId="StreamingWidget-wrong-chain"
+    />
+  ),
+}
+
+export const LoadingState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        streams: [],
+        streamsLoading: true,
+        streamHistory: [],
+        streamHistoryLoading: true,
+        pools: [],
+        poolsLoading: true,
+        balanceLoading: true,
+      })}
+      dataTestId="StreamingWidget-loading-state"
+    />
+  ),
+}
+
+export const EmptyState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        streams: [],
+        streamHistory: [],
+        pools: [],
+        superTokenBalance: '0',
+      })}
+      dataTestId="StreamingWidget-empty-state"
+    />
+  ),
+}
+
+export const ErrorState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        streams: [],
+        streamsError: 'Unable to reach the network. Check your connection and try again.',
+        streamHistory: [],
+        streamHistoryError: 'Unable to load stream history.',
+        pools: [],
+        poolsError: 'Unable to load pool memberships.',
+        balanceError: 'Unable to load token balance.',
+      })}
+      dataTestId="StreamingWidget-error-state"
+    />
+  ),
+}
+
+export const PopulatedState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter()}
+      dataTestId="StreamingWidget-populated-state"
+    />
+  ),
+}
+
+export const CreateUpdateForm: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({ setStreamForm: validForm })}
+      dataTestId="StreamingWidget-create-update-form"
+      initialStreamsFormOpen
+    />
+  ),
+}
+
+export const CreateUpdateInvalidInput: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({ setStreamForm: invalidForm })}
+      dataTestId="StreamingWidget-create-update-invalid"
+      initialStreamsFormOpen
+    />
+  ),
+}
+
+export const CreateUpdatePending: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        setStreamForm: validForm,
+        setStreamStatus: 'pending',
+      })}
+      dataTestId="StreamingWidget-create-update-pending"
+      initialStreamsFormOpen
+    />
+  ),
+}
+
+export const CreateUpdateSuccess: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        setStreamForm: validForm,
+        setStreamStatus: 'success',
+        setStreamTxHash:
+          '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+      })}
+      dataTestId="StreamingWidget-create-update-success"
+      initialStreamsFormOpen
+    />
+  ),
+}
+
+export const CreateUpdateFailure: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        setStreamForm: validForm,
+        setStreamStatus: 'error',
+        setStreamError: 'Transaction cancelled by wallet.',
+      })}
+      dataTestId="StreamingWidget-create-update-failure"
+      initialStreamsFormOpen
+    />
+  ),
+}
+
+export const PoolClaimState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter()}
+      dataTestId="StreamingWidget-pool-claim"
+      initialTab="pools"
+    />
+  ),
+}
+
+export const PoolClaimPending: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        poolConnectStatus: { [DEMO_POOL]: 'pending' },
+      })}
+      dataTestId="StreamingWidget-pool-claim-pending"
+      initialTab="pools"
+    />
+  ),
+}
+
+export const PoolClaimSuccess: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        pools: [{ ...samplePools[0], isConnected: true }],
+        poolConnectStatus: { [DEMO_POOL]: 'success' },
+      })}
+      dataTestId="StreamingWidget-pool-claim-success"
+      initialTab="pools"
+    />
+  ),
+}
+
+export const PoolClaimError: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        poolConnectStatus: { [DEMO_POOL]: 'error' },
+        poolConnectError: { [DEMO_POOL]: 'Pool claim failed. Please retry.' },
+      })}
+      dataTestId="StreamingWidget-pool-claim-error"
+      initialTab="pools"
+    />
+  ),
+}
+
+export const BaseSupBalanceAndReserve: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        chainId: STREAMING_CHAINS.BASE,
+        superTokenBalance: '712.10',
+        supReserveBalance: '95.25',
+      })}
+      dataTestId="StreamingWidget-base-sup-reserve"
+      initialTab="balances"
+    />
+  ),
+}
+
+export const NonBaseSupReserveDisabled: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        chainId: STREAMING_CHAINS.CELO,
+        superTokenBalance: '128.50',
+        supReserveBalance: null,
+      })}
+      dataTestId="StreamingWidget-non-base-reserve"
+      initialTab="balances"
+    />
+  ),
 }
diff --git a/packages/citizen-claim-widget/src/CitizenClaimWidget.tsx b/packages/citizen-claim-widget/src/CitizenClaimWidget.tsx
index 783f5a0..424a41c 100644
--- a/packages/citizen-claim-widget/src/CitizenClaimWidget.tsx
+++ b/packages/citizen-claim-widget/src/CitizenClaimWidget.tsx
@@ -172,7 +172,6 @@ function Countdown({ nextClaim }: { nextClaim: Date }) {
   useEffect(() => {
     const id = setInterval(() => setTimeLeft(getTimeLeft()), 1000)
     return () => clearInterval(id)
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [nextClaim])
 
   const h = Math.floor(timeLeft / 3600)
diff --git a/packages/citizen-claim-widget/src/adapter.ts b/packages/citizen-claim-widget/src/adapter.ts
index 4a0b7c3..1b326f3 100644
--- a/packages/citizen-claim-widget/src/adapter.ts
+++ b/packages/citizen-claim-widget/src/adapter.ts
@@ -420,7 +420,6 @@ export function useCitizenClaimAdapter(
   // Auto-refresh claim status whenever wallet connection or chain changes
   useEffect(() => {
     void loadClaimStatus()
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [isConnected, address, chainId])
 
   // ---------------------------------------------------------------------------
diff --git a/packages/core/src/provider.tsx b/packages/core/src/provider.tsx
index 0aa38f2..21d5ed5 100644
--- a/packages/core/src/provider.tsx
+++ b/packages/core/src/provider.tsx
@@ -24,7 +24,7 @@ export interface WalletContextValue extends WalletState {
   connect: () => Promise<void>
 }
 
-export interface HostContextValue extends HostState {}
+export type HostContextValue = HostState
 
 export interface GoodWidgetContextValue extends GoodWidgetState {
   connect: () => Promise<void>
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index f837009..9f04ce9 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -31,10 +31,14 @@ import type {
   StreamListItem,
   PoolMembershipItem,
   StreamTimeUnit,
+  StreamingWidgetAdapterResult,
   WriteStatus,
 } from './widgetRuntimeContract'
 import { STREAMING_CHAINS } from './widgetRuntimeContract'
 
+type StreamingWidgetAdapterState = StreamingWidgetAdapterResult['state']
+type StreamingWidgetAdapterActions = StreamingWidgetAdapterResult['actions']
+
 // ---------------------------------------------------------------------------
 // Named styled sub-components — participate in the component sub-theme system.
 // Integrators can override via themeOverrides.
@@ -116,7 +120,7 @@ function formatFlowRateDisplay(flowRate: bigint, decimals = 18): string {
 
 /** Formats a unix timestamp (seconds) to a short locale date string */
 function formatTimestamp(unixSeconds: number): string {
-  if (!unixSeconds) return '—'
+  if (!unixSeconds) return 'N/A'
   return new Date(unixSeconds * 1000).toLocaleDateString('en-US', {
     month: 'short',
     day: 'numeric',
@@ -216,11 +220,11 @@ function SetStreamForm({
   onSubmit,
   onReset,
 }: {
-  form: ReturnType<typeof useStreamingAdapter>['state']['setStreamForm']
+  form: StreamingWidgetAdapterState['setStreamForm']
   status: WriteStatus
   error: string | null
   txHash: string | null
-  onUpdate: (partial: Partial<typeof form>) => void
+  onUpdate: (partial: Partial<StreamingWidgetAdapterState['setStreamForm']>) => void
   onSubmit: () => void
   onReset: () => void
 }) {
@@ -228,13 +232,13 @@ function SetStreamForm({
 
   return (
     <SetStreamFormCard>
-      <Heading level={4}>{form.receiver ? 'Update Stream' : 'Create Stream'}</Heading>
+      <Heading level={4}>Create / Update Stream</Heading>
 
       {/* Recipient address */}
       <YStack gap="$1">
         <Text variant="label">Recipient address</Text>
         <Input
-          placeholder="0x…"
+          placeholder="0x..."
           value={form.receiver}
           onChangeText={(v: string) => onUpdate({ receiver: v })}
           editable={!isSubmitting}
@@ -268,7 +272,7 @@ function SetStreamForm({
       {form.flowRate !== null && form.flowRate > 0n && (
         <XStack gap="$2" alignItems="center">
           <Text variant="caption" secondary>
-            ≈ {formatUnits(form.flowRate, 18)} tokens/s
+            About {formatUnits(form.flowRate, 18)} tokens/s
           </Text>
         </XStack>
       )}
@@ -284,9 +288,14 @@ function SetStreamForm({
           {error}
         </Text>
       )}
+      {status === 'pending' && (
+        <Text variant="caption" secondary>
+          Transaction pending...
+        </Text>
+      )}
       {status === 'success' && txHash && (
         <Text color="$success" variant="caption">
-          Stream set! Tx: {txHash.slice(0, 10)}…
+          Stream set! Tx: {txHash.slice(0, 10)}...
         </Text>
       )}
 
@@ -321,7 +330,7 @@ function StreamCard({ stream }: { stream: StreamListItem }) {
     <StreamRow>
       <XStack justifyContent="space-between" alignItems="center">
         <Badge type={stream.direction === 'incoming' ? 'success' : 'info'}>
-          <BadgeText>{stream.direction === 'incoming' ? '↓ Incoming' : '↑ Outgoing'}</BadgeText>
+          <BadgeText>{stream.direction === 'incoming' ? 'Incoming' : 'Outgoing'}</BadgeText>
         </Badge>
         <Text variant="caption" secondary>
           Since {formatTimestamp(stream.createdAtTimestamp)}
@@ -360,16 +369,21 @@ const DIRECTION_LABELS: Record<StreamDirection, string> = {
 function StreamsTab({
   state,
   actions,
+  initialFormOpen = false,
 }: {
-  state: ReturnType<typeof useStreamingAdapter>['state']
-  actions: ReturnType<typeof useStreamingAdapter>['actions']
+  state: StreamingWidgetAdapterState
+  actions: StreamingWidgetAdapterActions
+  initialFormOpen?: boolean
 }) {
   const [direction, setDirection] = useState<StreamDirection>('all')
-  const [showForm, setShowForm] = useState(false)
+  const [showForm, setShowForm] = useState(initialFormOpen)
 
   const filteredStreams = state.streams.filter(
     (s) => direction === 'all' || s.direction === direction,
   )
+  const emptyStreamsMessage =
+    direction === 'all' ? 'No streams found.' : `No ${direction} streams found.`
+  const recentStreams = state.streamHistory.slice(0, 4)
 
   return (
     <StreamingTabContent>
@@ -397,6 +411,8 @@ function StreamsTab({
 
       <Separator />
 
+      <Heading level={4}>Active streams</Heading>
+
       {/* Direction filter */}
       <XStack gap="$2">
         {(['all', 'incoming', 'outgoing'] as StreamDirection[]).map((d) => (
@@ -414,7 +430,7 @@ function StreamsTab({
       {state.streamsLoading && (
         <YStack alignItems="center" paddingVertical="$4">
           <Spinner size="lg" />
-          <Text secondary>Loading streams…</Text>
+          <Text secondary>Loading streams...</Text>
         </YStack>
       )}
 
@@ -430,7 +446,7 @@ function StreamsTab({
       {!state.streamsLoading && !state.streamsError && filteredStreams.length === 0 && (
         <EmptyStateCard>
           <Text secondary center>
-            No {direction === 'all' ? '' : direction} streams found.
+            {emptyStreamsMessage}
           </Text>
           <Button onPress={actions.refreshStreams}>
             <ButtonText>Refresh</ButtonText>
@@ -441,6 +457,45 @@ function StreamsTab({
       {!state.streamsLoading &&
         !state.streamsError &&
         filteredStreams.map((stream) => <StreamCard key={stream.id} stream={stream} />)}
+
+      <Separator />
+
+      <XStack justifyContent="space-between" alignItems="center">
+        <Heading level={4}>Stream history</Heading>
+        <Button variant="secondary" onPress={actions.refreshStreamHistory}>
+          <ButtonText>Refresh</ButtonText>
+        </Button>
+      </XStack>
+
+      {state.streamHistoryLoading && (
+        <YStack alignItems="center" paddingVertical="$4">
+          <Spinner size="lg" />
+          <Text secondary>Loading stream history...</Text>
+        </YStack>
+      )}
+
+      {!state.streamHistoryLoading && state.streamHistoryError && (
+        <ErrorStateCard>
+          <Text color="$error">{state.streamHistoryError}</Text>
+          <Button onPress={actions.refreshStreamHistory}>
+            <ButtonText>Retry</ButtonText>
+          </Button>
+        </ErrorStateCard>
+      )}
+
+      {!state.streamHistoryLoading &&
+        !state.streamHistoryError &&
+        recentStreams.length === 0 && (
+          <EmptyStateCard>
+            <Text secondary center>
+              No stream history found.
+            </Text>
+          </EmptyStateCard>
+        )}
+
+      {!state.streamHistoryLoading &&
+        !state.streamHistoryError &&
+        recentStreams.map((stream) => <StreamCard key={`history-${stream.id}`} stream={stream} />)}
     </StreamingTabContent>
   )
 }
@@ -472,6 +527,13 @@ function PoolCard({
         </Badge>
       </XStack>
 
+      <XStack justifyContent="space-between" alignItems="center">
+        <Text variant="caption" secondary>
+          Claimable
+        </Text>
+        <Text>{formatUnits(pool.claimableAmount, 18)}</Text>
+      </XStack>
+
       <XStack justifyContent="space-between" alignItems="center">
         <Text variant="caption" secondary>
           Total claimed
@@ -493,7 +555,7 @@ function PoolCard({
           </Button>
         ) : (
           <Button disabled={isPending} onPress={() => onConnect(pool.poolId)}>
-            {isPending ? <Spinner size="sm" /> : <ButtonText>Connect</ButtonText>}
+            {isPending ? <Spinner size="sm" /> : <ButtonText>Connect to claim</ButtonText>}
           </Button>
         )}
       </XStack>
@@ -508,15 +570,15 @@ function PoolsTab({
   state,
   actions,
 }: {
-  state: ReturnType<typeof useStreamingAdapter>['state']
-  actions: ReturnType<typeof useStreamingAdapter>['actions']
+  state: StreamingWidgetAdapterState
+  actions: StreamingWidgetAdapterActions
 }) {
   return (
     <StreamingTabContent>
       {state.poolsLoading && (
         <YStack alignItems="center" paddingVertical="$4">
           <Spinner size="lg" />
-          <Text secondary>Loading pool memberships…</Text>
+          <Text secondary>Loading pool memberships...</Text>
         </YStack>
       )}
 
@@ -563,8 +625,8 @@ function BalancesTab({
   state,
   actions,
 }: {
-  state: ReturnType<typeof useStreamingAdapter>['state']
-  actions: ReturnType<typeof useStreamingAdapter>['actions']
+  state: StreamingWidgetAdapterState
+  actions: StreamingWidgetAdapterActions
 }) {
   const isOnBase = state.chainId === STREAMING_CHAINS.BASE
 
@@ -575,7 +637,7 @@ function BalancesTab({
         <XStack justifyContent="space-between" alignItems="center">
           <Text variant="label">Super Token Balance</Text>
           <Button onPress={actions.refreshBalance}>
-            <ButtonText>↺ Refresh</ButtonText>
+            <ButtonText>Refresh</ButtonText>
           </Button>
         </XStack>
 
@@ -648,8 +710,24 @@ function StreamingWidgetInner({
   environment: StreamingWidgetProps['environment']
   apiKey?: string
 }) {
-  const { state, actions } = useStreamingAdapter({ environment, apiKey })
-  const [activeTab, setActiveTab] = useState<StreamingWidgetTab>('streams')
+  const adapter = useStreamingAdapter({ environment, apiKey })
+
+  return <StreamingWidgetView adapter={adapter} />
+}
+
+interface StreamingWidgetViewProps {
+  adapter: StreamingWidgetAdapterResult
+  initialTab?: StreamingWidgetTab
+  initialStreamsFormOpen?: boolean
+}
+
+function StreamingWidgetView({
+  adapter,
+  initialTab = 'streams',
+  initialStreamsFormOpen = false,
+}: StreamingWidgetViewProps) {
+  const { state, actions } = adapter
+  const [activeTab, setActiveTab] = useState<StreamingWidgetTab>(initialTab)
 
   const walletGate = (
     <WalletGate
@@ -662,7 +740,13 @@ function StreamingWidgetInner({
 
   const tabContent = !state.isConnected || state.isWrongChain ? walletGate : (
     <>
-      {activeTab === 'streams' && <StreamsTab state={state} actions={actions} />}
+      {activeTab === 'streams' && (
+        <StreamsTab
+          state={state}
+          actions={actions}
+          initialFormOpen={initialStreamsFormOpen}
+        />
+      )}
       {activeTab === 'pools' && <PoolsTab state={state} actions={actions} />}
       {activeTab === 'balances' && <BalancesTab state={state} actions={actions} />}
     </>
@@ -685,6 +769,37 @@ function StreamingWidgetInner({
   )
 }
 
+export interface StreamingWidgetPreviewProps
+  extends Pick<StreamingWidgetProps, 'themeOverrides' | 'config' | 'defaultTheme'> {
+  adapter: StreamingWidgetAdapterResult
+  initialTab?: StreamingWidgetTab
+  initialStreamsFormOpen?: boolean
+}
+
+export function StreamingWidgetPreview({
+  adapter,
+  initialTab,
+  initialStreamsFormOpen,
+  themeOverrides,
+  config,
+  defaultTheme = 'light',
+}: StreamingWidgetPreviewProps) {
+  return (
+    <GoodWidgetProvider
+      config={config}
+      themeOverrides={themeOverrides}
+      defaultTheme={defaultTheme}
+    >
+      <StreamingWidgetView
+        adapter={adapter}
+        initialTab={initialTab}
+        initialStreamsFormOpen={initialStreamsFormOpen}
+      />
+      <ToastContainer />
+    </GoodWidgetProvider>
+  )
+}
+
 // ---------------------------------------------------------------------------
 // Public component
 // ---------------------------------------------------------------------------
diff --git a/packages/streaming-widget/src/adapter.ts b/packages/streaming-widget/src/adapter.ts
index 313ce59..7745ee3 100644
--- a/packages/streaming-widget/src/adapter.ts
+++ b/packages/streaming-widget/src/adapter.ts
@@ -124,10 +124,12 @@ function toStreamListItem(stream: StreamInfo, address: Address): StreamListItem
 // Derive PoolMembershipItem from the SDK GDAPool
 // ---------------------------------------------------------------------------
 function toPoolMembershipItem(pool: GDAPool): PoolMembershipItem {
+  const poolWithClaimable = pool as GDAPool & { claimableAmount?: bigint }
   return {
     poolId: pool.id,
     poolToken: pool.token,
     totalUnits: pool.totalUnits,
+    claimableAmount: poolWithClaimable.claimableAmount ?? 0n,
     totalAmountClaimed: pool.totalAmountClaimed,
     isConnected: pool.isConnected ?? false,
   }
@@ -148,7 +150,7 @@ function validateSetStreamForm(form: SetStreamFormState): SetStreamFormState {
     return {
       ...form,
       flowRate: null,
-      validationError: 'Recipient must be a valid Ethereum address (0x…).',
+      validationError: 'Recipient must be a valid Ethereum address (0x...).',
     }
   }
 
@@ -178,6 +180,9 @@ export function useStreamingAdapter({
   const [streams, setStreams] = useState<StreamListItem[]>([])
   const [streamsLoading, setStreamsLoading] = useState(false)
   const [streamsError, setStreamsError] = useState<string | null>(null)
+  const [streamHistory, setStreamHistory] = useState<StreamListItem[]>([])
+  const [streamHistoryLoading, setStreamHistoryLoading] = useState(false)
+  const [streamHistoryError, setStreamHistoryError] = useState<string | null>(null)
 
   // --- pools state ---
   const [pools, setPools] = useState<PoolMembershipItem[]>([])
@@ -259,16 +264,23 @@ export function useStreamingAdapter({
 
     setStreamsLoading(true)
     setStreamsError(null)
+    setStreamHistoryLoading(true)
+    setStreamHistoryError(null)
     try {
       const result = await streamingSDK.getActiveStreams({
         account: address as Address,
         direction: 'all',
       })
-      setStreams(result.map((s) => toStreamListItem(s, address as Address)))
+      const normalizedStreams = result.map((s) => toStreamListItem(s, address as Address))
+      setStreams(normalizedStreams)
+      setStreamHistory(normalizedStreams)
     } catch (err) {
-      setStreamsError(humanReadableError(err))
+      const message = humanReadableError(err)
+      setStreamsError(message)
+      setStreamHistoryError(message)
     } finally {
       setStreamsLoading(false)
+      setStreamHistoryLoading(false)
     }
   }, [streamingSDK, address])
 
@@ -344,6 +356,7 @@ export function useStreamingAdapter({
 
     if (!isConnected || !address || isWrongChain) {
       setStreams([])
+      setStreamHistory([])
       setPools([])
       setSuperTokenBalance(null)
       setSupReserveBalance(null)
@@ -480,6 +493,9 @@ export function useStreamingAdapter({
       streams,
       streamsLoading,
       streamsError,
+      streamHistory,
+      streamHistoryLoading,
+      streamHistoryError,
       pools,
       poolsLoading,
       poolsError,
@@ -504,6 +520,9 @@ export function useStreamingAdapter({
       streams,
       streamsLoading,
       streamsError,
+      streamHistory,
+      streamHistoryLoading,
+      streamHistoryError,
       pools,
       poolsLoading,
       poolsError,
@@ -527,6 +546,7 @@ export function useStreamingAdapter({
       connect,
       switchChain,
       refreshStreams: fetchStreams,
+      refreshStreamHistory: fetchStreams,
       refreshPools: fetchPools,
       refreshBalance: fetchBalance,
       updateSetStreamForm,
diff --git a/packages/streaming-widget/src/index.ts b/packages/streaming-widget/src/index.ts
index 77266d2..8c420d5 100644
--- a/packages/streaming-widget/src/index.ts
+++ b/packages/streaming-widget/src/index.ts
@@ -20,4 +20,5 @@ export { useStreamingAdapter } from './adapter'
 export type { UseStreamingAdapterOptions } from './adapter'
 
 // Widget component
-export { StreamingWidget } from './StreamingWidget'
+export { StreamingWidget, StreamingWidgetPreview } from './StreamingWidget'
+export type { StreamingWidgetPreviewProps } from './StreamingWidget'
diff --git a/packages/streaming-widget/src/widgetRuntimeContract.ts b/packages/streaming-widget/src/widgetRuntimeContract.ts
index 9f1900c..d19a086 100644
--- a/packages/streaming-widget/src/widgetRuntimeContract.ts
+++ b/packages/streaming-widget/src/widgetRuntimeContract.ts
@@ -62,6 +62,8 @@ export interface PoolMembershipItem {
   poolId: Address
   poolToken: Address
   totalUnits: bigint
+  /** Claimable incoming distribution amount in wei, when exposed by the data source */
+  claimableAmount: bigint
   totalAmountClaimed: bigint
   /** Whether this account has actively connected to the pool distribution */
   isConnected: boolean
@@ -96,6 +98,9 @@ export interface StreamingWidgetAdapterState {
   streams: StreamListItem[]
   streamsLoading: boolean
   streamsError: string | null
+  streamHistory: StreamListItem[]
+  streamHistoryLoading: boolean
+  streamHistoryError: string | null
 
   /** GDA pool memberships for the connected address */
   pools: PoolMembershipItem[]
@@ -130,6 +135,7 @@ export interface StreamingWidgetAdapterActions {
   connect: () => Promise<void>
   switchChain: (chainId: number) => Promise<void>
   refreshStreams: () => Promise<void>
+  refreshStreamHistory: () => Promise<void>
   refreshPools: () => Promise<void>
   refreshBalance: () => Promise<void>
 
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index ea576b5..276cd3b 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -1,285 +1,165 @@
-/**
- * states.spec.ts — Playwright smoke tests for the StreamingWidget.
- *
- * Tests use the NoWallet story (no provider) to verify the widget shell renders and
- * surfaces the connect-wallet prompt, and the CustodialLocalFixture story to verify
- * the connected flow on Celo with an empty-history test wallet.
- *
- * Story URLs:
- *   /iframe.html?id=widgets-streamingwidget--no-wallet&viewMode=story
- *   /iframe.html?id=widgets-streamingwidget--custodial-local-fixture&viewMode=story
- *
- * Browser flags (set globally in playwright.config.ts):
- *   --disable-web-security     : allows viem fetch calls from localhost to external HTTPS RPC
- *   --ignore-certificate-errors: allows Chromium to accept RPC endpoint TLS certs
- *
- * Running:
- *   pnpm storybook          (in one terminal)
- *   pnpm test:demo          (in another terminal)
- *
- * Artifact output:
- *   tests/widgets/streaming-widget/test-results/   (widget screenshot evidence)
- *   test-results/                                  (Playwright traces/videos/attachments)
- */
 import { test, expect, type Page } from '@playwright/test'
 
-const NO_WALLET_STORY_URL =
-  '/iframe.html?id=widgets-streamingwidget--no-wallet&viewMode=story'
+const STORY_PREFIX = '/iframe.html?id=widgets-streamingwidget--'
 
-const CUSTODIAL_STORY_URL =
-  '/iframe.html?id=widgets-streamingwidget--custodial-local-fixture&viewMode=story'
+function storyUrl(storyId: string): string {
+  return `${STORY_PREFIX}${storyId}&viewMode=story`
+}
 
-/** Navigate directly to the story iframe (bypasses Storybook shell for speed). */
-async function gotoStory(page: Page, url: string): Promise<void> {
-  await page.goto(url)
+async function gotoStory(page: Page, storyId: string): Promise<void> {
+  await page.goto(storyUrl(storyId))
   await page.waitForLoadState('domcontentloaded')
+  await page.waitForFunction(() => document.body.innerText.trim().length > 0)
+}
+
+async function bodyText(page: Page): Promise<string> {
+  return page.evaluate(() => document.body.innerText)
 }
 
-/** Poll the page until any of the given text patterns appears in the body. */
-async function waitForText(
-  page: Page,
-  patterns: string[],
-  timeoutMs = 30_000,
-): Promise<string> {
-  const deadline = Date.now() + timeoutMs
-  while (Date.now() < deadline) {
-    const text = await page.evaluate(() => document.body.innerText)
-    for (const p of patterns) {
-      if (text.includes(p)) return p
+async function expectBodyToContain(page: Page, patterns: Array<string | RegExp>) {
+  const text = await bodyText(page)
+  for (const pattern of patterns) {
+    if (typeof pattern === 'string') {
+      expect(text).toContain(pattern)
+    } else {
+      expect(text).toMatch(pattern)
     }
-    await page.waitForTimeout(500)
   }
-  return ''
 }
 
-// ─── no-wallet state ─────────────────────────────────────────────────────────
-test('StreamingWidget shows connect-wallet prompt when no provider is given', async ({ page }) => {
-  await gotoStory(page, NO_WALLET_STORY_URL)
-
-  // The widget should render the tab bar and the connect-wallet prompt
-  const matched = await waitForText(page, ['Connect Wallet', 'not connected', 'Streams'], 20_000)
-  expect(matched, 'Expected connect-wallet prompt').toBeTruthy()
-
-  const bodyText = await page.evaluate(() => document.body.innerText)
-  expect(bodyText).toMatch(/Connect Wallet|not connected/i)
-
+async function saveScreenshot(page: Page, name: string) {
   await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png',
+    path: `tests/widgets/streaming-widget/test-results/${name}.png`,
     fullPage: true,
   })
-})
-
-// ─── tab navigation ───────────────────────────────────────────────────────────
-test('StreamingWidget renders Streams, Pools, Balances tabs', async ({ page }) => {
-  await gotoStory(page, NO_WALLET_STORY_URL)
-
-  const matched = await waitForText(page, ['Streams', 'Pools', 'Balances'], 20_000)
-  expect(matched, 'Tab bar must render').toBeTruthy()
+}
 
-  const bodyText = await page.evaluate(() => document.body.innerText)
-  expect(bodyText).toContain('Streams')
-  expect(bodyText).toContain('Pools')
-  expect(bodyText).toContain('Balances')
+test('StreamingWidget shows the disconnected wallet gate', async ({ page }) => {
+  await gotoStory(page, 'no-wallet')
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-02-tabs-visible.png',
-    fullPage: true,
-  })
+  await expectBodyToContain(page, ['Wallet not connected', 'Connect Wallet'])
+  await saveScreenshot(page, 'sw-01-no-wallet')
 })
 
-// ─── wrong-chain state ────────────────────────────────────────────────────────
-test('StreamingWidget shows wrong-chain prompt when wallet is on unsupported chain', async ({
-  page,
-}) => {
-  // Inject a minimal EIP-1193 provider that reports an unsupported chain (chain 1 = Ethereum mainnet)
-  await gotoStory(page, NO_WALLET_STORY_URL)
-
-  // Evaluate a mock provider in the page context to simulate wrong chain
-  await page.evaluate(() => {
-    const mockProvider = {
-      on: () => {},
-      removeListener: () => {},
-      request: async ({ method }: { method: string }) => {
-        if (method === 'eth_accounts' || method === 'eth_requestAccounts')
-          return ['0x1234567890123456789012345678901234567890']
-        if (method === 'eth_chainId') return '0x1' // Ethereum mainnet (unsupported)
-        if (method === 'net_version') return '1'
-        return null
-      },
-    }
-    ;(window as unknown as Record<string, unknown>).__mockProvider = mockProvider
-  })
+test('StreamingWidget renders tab navigation and switches views', async ({ page }) => {
+  await gotoStory(page, 'populated-state')
 
-  // The story renders with no provider so we can't easily inject — just verify tab bar renders
-  const matched = await waitForText(page, ['Streams', 'Pools', 'Balances'], 15_000)
-  expect(matched).toBeTruthy()
+  await expectBodyToContain(page, ['Streams', 'Pools', 'Balances', 'Active streams'])
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-03-no-wallet-tabs.png',
-    fullPage: true,
-  })
-})
+  await page.getByText('Pools').first().click()
+  await expectBodyToContain(page, ['Claimable', 'Connect to claim'])
 
-// ─── custodial: loading + empty states ────────────────────────────────────────
-test('StreamingWidget custodial fixture — Streams tab shows loading then empty state', async ({
-  page,
-  browserName,
-}) => {
-  test.skip(
-    browserName !== 'chromium',
-    'Live RPC test requires --disable-web-security / --ignore-certificate-errors',
-  )
-
-  // Route subgraph calls to never respond — intentionally hangs to keep the
-  // widget in the loading state indefinitely so we can screenshot that state.
-  await page.route('https://subgraph-gateway.superfluid.finance/**', () => {
-    /* intentional hang — never fulfill, never abort */
-  })
-  await page.route('https://gateway-arbitrum.network.thegraph.com/**', () => {
-    /* intentional hang — never fulfill, never abort */
-  })
+  await page.getByText('Balances').first().click()
+  await expectBodyToContain(page, ['Super Token Balance', 'SUP Reserve'])
 
-  await gotoStory(page, CUSTODIAL_STORY_URL)
+  await saveScreenshot(page, 'sw-02-tab-navigation')
+})
 
-  // Tab bar should render first
-  const tabsVisible = await waitForText(page, ['Streams', 'Pools', 'Balances'], 30_000)
-  expect(tabsVisible).toBeTruthy()
+test('StreamingWidget shows the unsupported network prompt', async ({ page }) => {
+  await gotoStory(page, 'wrong-chain')
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-04-loading.png',
-    fullPage: true,
-  })
+  await expectBodyToContain(page, [
+    'Unsupported network',
+    'Switch to Celo',
+    'Switch to Base',
+  ])
+  await saveScreenshot(page, 'sw-03-wrong-chain')
 })
 
-// ─── custodial: RPC blocked → error state ────────────────────────────────────
-test('StreamingWidget custodial fixture — shows error state when RPC is blocked', async ({
-  page,
-  browserName,
-}) => {
-  test.skip(
-    browserName !== 'chromium',
-    'Live RPC test requires --disable-web-security / --ignore-certificate-errors',
-  )
+test('StreamingWidget shows loading states for streams and history', async ({ page }) => {
+  await gotoStory(page, 'loading-state')
 
-  // Block all Celo RPC and subgraph endpoints to force error state
-  await page.route('https://forno.celo.org/**', (route) => route.abort())
-  await page.route('https://subgraph-gateway.superfluid.finance/**', (route) => route.abort())
-  await page.route('https://gateway-arbitrum.network.thegraph.com/**', (route) => route.abort())
-
-  await gotoStory(page, CUSTODIAL_STORY_URL)
+  await expectBodyToContain(page, ['Loading streams', 'Loading stream history'])
+  await saveScreenshot(page, 'sw-04-loading-state')
+})
 
-  // After RPCs abort, adapter should surface error state with Retry button
-  const matched = await waitForText(page, ['Retry', 'error', 'reach'], 25_000)
-  expect(matched, 'Expected error state after RPC abort').toBeTruthy()
+test('StreamingWidget shows empty states for streams and history', async ({ page }) => {
+  await gotoStory(page, 'empty-state')
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-05-error.png',
-    fullPage: true,
-  })
+  await expectBodyToContain(page, ['No streams found.', 'No stream history found.'])
+  await saveScreenshot(page, 'sw-05-empty-state')
 })
 
-// ─── custodial: pools tab navigation ─────────────────────────────────────────
-test('StreamingWidget custodial fixture — clicking Pools tab changes view', async ({
-  page,
-  browserName,
-}) => {
-  test.skip(
-    browserName !== 'chromium',
-    'Live RPC test requires --disable-web-security / --ignore-certificate-errors',
-  )
-
-  // Block external calls to keep the test deterministic
-  await page.route('https://forno.celo.org/**', (route) => route.abort())
-  await page.route('https://subgraph-gateway.superfluid.finance/**', (route) => route.abort())
+test('StreamingWidget shows error states for streams and history', async ({ page }) => {
+  await gotoStory(page, 'error-state')
 
-  await gotoStory(page, CUSTODIAL_STORY_URL)
+  await expectBodyToContain(page, [
+    'Unable to reach the network',
+    'Unable to load stream history.',
+    'Retry',
+  ])
+  await saveScreenshot(page, 'sw-06-error-state')
+})
 
-  // Wait for tab bar to render
-  await waitForText(page, ['Streams', 'Pools', 'Balances'], 20_000)
+test('StreamingWidget shows populated incoming and outgoing stream views', async ({ page }) => {
+  await gotoStory(page, 'populated-state')
 
-  // Click the Pools tab
-  const poolsTab = page.getByText('Pools').first()
-  await expect(poolsTab).toBeVisible()
-  await poolsTab.click()
+  await expectBodyToContain(page, ['Active streams', 'Stream history', 'Incoming', 'Outgoing'])
 
-  await page.waitForTimeout(500)
+  await page.getByText('Incoming').first().click()
+  await expectBodyToContain(page, ['Incoming'])
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-06-pools-tab.png',
-    fullPage: true,
-  })
+  await page.getByText('Outgoing').first().click()
+  await expectBodyToContain(page, ['Outgoing'])
 
-  // Verify the tab content changed (either loading, error, or empty state for pools)
-  const bodyText = await page.evaluate(() => document.body.innerText)
-  expect(bodyText).toContain('Pools')
+  await saveScreenshot(page, 'sw-07-populated-streams')
 })
 
-// ─── custodial: balances tab navigation ──────────────────────────────────────
-test('StreamingWidget custodial fixture — clicking Balances tab shows balance section', async ({
-  page,
-  browserName,
-}) => {
-  test.skip(
-    browserName !== 'chromium',
-    'Live RPC test requires --disable-web-security / --ignore-certificate-errors',
-  )
-
-  // Block external calls
-  await page.route('https://forno.celo.org/**', (route) => route.abort())
-  await page.route('https://subgraph-gateway.superfluid.finance/**', (route) => route.abort())
-
-  await gotoStory(page, CUSTODIAL_STORY_URL)
+test('StreamingWidget create/update form shows invalid input feedback', async ({ page }) => {
+  await gotoStory(page, 'create-update-invalid-input')
 
-  await waitForText(page, ['Streams', 'Pools', 'Balances'], 20_000)
-
-  // Click Balances tab
-  const balancesTab = page.getByText('Balances').first()
-  await expect(balancesTab).toBeVisible()
-  await balancesTab.click()
-
-  await page.waitForTimeout(500)
+  await expectBodyToContain(page, [
+    'Create / Update Stream',
+    'Recipient must be a valid Ethereum address',
+  ])
+  await saveScreenshot(page, 'sw-08-create-update-invalid')
+})
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-07-balances-tab.png',
-    fullPage: true,
-  })
+test('StreamingWidget create/update form shows pending and success states', async ({ page }) => {
+  await gotoStory(page, 'create-update-pending')
+  await expectBodyToContain(page, ['Create / Update Stream', 'Transaction pending...'])
+  await saveScreenshot(page, 'sw-09-create-update-pending')
 
-  // Balances tab shows Super Token Balance section and the SUP reserve disabled notice for non-Base
-  const bodyText = await page.evaluate(() => document.body.innerText)
-  expect(bodyText).toMatch(/Balance|Refresh|Super Token/i)
+  await gotoStory(page, 'create-update-success')
+  await expectBodyToContain(page, ['Create / Update Stream', 'Stream set! Tx:'])
+  await saveScreenshot(page, 'sw-10-create-update-success')
 })
 
-// ─── custodial: create-stream form toggle ────────────────────────────────────
-test('StreamingWidget custodial fixture — New Stream button toggles form', async ({
-  page,
-  browserName,
-}) => {
-  test.skip(
-    browserName !== 'chromium',
-    'Live RPC test requires --disable-web-security / --ignore-certificate-errors',
-  )
+test('StreamingWidget create/update form shows failure state', async ({ page }) => {
+  await gotoStory(page, 'create-update-failure')
 
-  // Block external calls for determinism
-  await page.route('https://forno.celo.org/**', (route) => route.abort())
-  await page.route('https://subgraph-gateway.superfluid.finance/**', (route) => route.abort())
-
-  await gotoStory(page, CUSTODIAL_STORY_URL)
+  await expectBodyToContain(page, ['Create / Update Stream', 'Transaction cancelled by wallet.'])
+  await saveScreenshot(page, 'sw-11-create-update-failure')
+})
 
-  // Wait for Streams tab to render with the New Stream button
-  await waitForText(page, ['New Stream'], 20_000)
+test('StreamingWidget shows pool claim amount and lifecycle states', async ({ page }) => {
+  await gotoStory(page, 'pool-claim-state')
+  await expectBodyToContain(page, ['Claimable', '12.5', 'Connect to claim'])
+  await saveScreenshot(page, 'sw-12-pool-claim')
 
-  const newStreamBtn = page.getByText('+ New Stream').first()
-  await expect(newStreamBtn).toBeVisible()
-  await newStreamBtn.click()
+  await gotoStory(page, 'pool-claim-pending')
+  await expectBodyToContain(page, ['Claimable'])
+  await saveScreenshot(page, 'sw-13-pool-claim-pending')
 
-  // Form should appear with recipient and amount fields
-  await waitForText(page, ['Recipient address', 'Amount', 'Set Stream'], 5_000)
+  await gotoStory(page, 'pool-claim-success')
+  await expectBodyToContain(page, ['Connected', 'Done'])
+  await saveScreenshot(page, 'sw-14-pool-claim-success')
 
-  const bodyText = await page.evaluate(() => document.body.innerText)
-  expect(bodyText).toMatch(/Recipient|Amount|Set Stream/i)
+  await gotoStory(page, 'pool-claim-error')
+  await expectBodyToContain(page, ['Pool claim failed. Please retry.', 'Failed'])
+  await saveScreenshot(page, 'sw-15-pool-claim-error')
+})
 
-  await page.screenshot({
-    path: 'tests/widgets/streaming-widget/test-results/sw-08-create-stream-form.png',
-    fullPage: true,
-  })
+test('StreamingWidget shows Base SUP reserve and disables reserve off Base', async ({ page }) => {
+  await gotoStory(page, 'base-sup-balance-and-reserve')
+  await expectBodyToContain(page, ['Super Token Balance', 'SUP Reserve (Staked)', '95.25'])
+  await saveScreenshot(page, 'sw-16-base-sup-reserve')
+
+  await gotoStory(page, 'non-base-sup-reserve-disabled')
+  await expectBodyToContain(page, [
+    'Super Token Balance',
+    'SUP Reserve',
+    'Reserve data is only available on Base',
+  ])
+  await saveScreenshot(page, 'sw-17-non-base-reserve-disabled')
 })
diff --git a/tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png b/tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png
new file mode 100644
index 0000000000000000000000000000000000000000..e887a884bf60af507498c742fb3989984cfa6315
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-02-tab-navigation.png b/tests/widgets/streaming-widget/test-results/sw-02-tab-navigation.png
new file mode 100644
index 0000000000000000000000000000000000000000..49e1b18f3123ee568a90284e66c3132f4d851f15
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-03-wrong-chain.png b/tests/widgets/streaming-widget/test-results/sw-03-wrong-chain.png
new file mode 100644
index 0000000000000000000000000000000000000000..aff96b40b3048fd81395e134af62574cdcf4985a
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-04-loading-state.png b/tests/widgets/streaming-widget/test-results/sw-04-loading-state.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d5bf058dc5f5ac507b1b9a4708e7dbdb9c1053a
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-05-empty-state.png b/tests/widgets/streaming-widget/test-results/sw-05-empty-state.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c56a2b4381953566422ffe51c9ee1bb0861430e
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-06-error-state.png b/tests/widgets/streaming-widget/test-results/sw-06-error-state.png
new file mode 100644
index 0000000000000000000000000000000000000000..a18961ef69655ca438b94e734a89d49ac1cb5f67
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png b/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png
new file mode 100644
index 0000000000000000000000000000000000000000..568fee9336b304ac6eef1cffccfa43eebb466151
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png b/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png
new file mode 100644
index 0000000000000000000000000000000000000000..f170a0bb6f3810e2f83222eebb532a3b0aad9671
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png b/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c02cca5730c640b552b7c315ab2b2035a3bd3c6
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png b/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc6710f6229f598ac063e68836dd62514bc60b36
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png b/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png
new file mode 100644
index 0000000000000000000000000000000000000000..553f37328ab7d531bd947b7837f05aa253c34c36
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png b/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png
new file mode 100644
index 0000000000000000000000000000000000000000..752c5d9f750c818b06891110cbefdfbd38d74af3
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png b/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc976e46e6241551b7745689b9a2e027c46af6ad
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png b/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ade97e781135018b1958b011b94f9ac2c128e35
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png b/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bcb383eaa159981c29ab9992c5121bbc83e26b3
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-16-base-sup-reserve.png b/tests/widgets/streaming-widget/test-results/sw-16-base-sup-reserve.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e8f8ce4d19662704b9e29c161f21d93df98a6dc
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-17-non-base-reserve-disabled.png b/tests/widgets/streaming-widget/test-results/sw-17-non-base-reserve-disabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..49e1b18f3123ee568a90284e66c3132f4d851f15
GIT binary patch

From a4352bbfc54a21bba4c9d058acfe82147da8c6bc Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Wed, 20 May 2026 16:55:26 +0100
Subject: [PATCH 2/7] fix: tighten streaming widget claim and layout coverage

---
 .../StreamingWidget.stories.tsx               |  71 ++++++++++++--
 .../streaming-widget/src/StreamingWidget.tsx  |  45 +++++++--
 packages/streaming-widget/src/adapter.ts      |  89 +++++++++++++++++-
 .../src/widgetRuntimeContract.ts              |   5 +
 tests/widgets/streaming-widget/states.spec.ts |  24 ++++-
 .../test-results/sw-07-populated-streams.png  | Bin 59318 -> 82944 bytes
 .../sw-08-create-update-invalid.png           | Bin 87309 -> 110929 bytes
 .../sw-09-create-update-pending.png           | Bin 87521 -> 111143 bytes
 .../sw-10-create-update-success.png           | Bin 90112 -> 113653 bytes
 .../sw-11-create-update-failure.png           | Bin 90313 -> 113863 bytes
 .../test-results/sw-12-pool-claim.png         | Bin 21071 -> 22717 bytes
 .../test-results/sw-13-pool-claim-pending.png | Bin 19886 -> 22373 bytes
 .../test-results/sw-14-pool-claim-success.png | Bin 21782 -> 23616 bytes
 .../test-results/sw-15-pool-claim-error.png   | Bin 23320 -> 24905 bytes
 .../test-results/sw-18-mobile-populated.png   | Bin 0 -> 78869 bytes
 .../test-results/sw-19-desktop-populated.png  | Bin 0 -> 91077 bytes
 16 files changed, 214 insertions(+), 20 deletions(-)
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-18-mobile-populated.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-19-desktop-populated.png

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 990068a..5aa7e52 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -74,6 +74,43 @@ const sampleStreams: StreamListItem[] = [
   },
 ]
 
+const sampleStreamHistory: StreamListItem[] = [
+  ...sampleStreams,
+  {
+    id: 'history-outgoing-demo-stream-2',
+    sender: DEMO_ADDRESS,
+    receiver: '0x5555555555555555555555555555555555555555',
+    token: DEMO_TOKEN,
+    flowRate: 9645061728395n,
+    streamedSoFar: 4300000000000000000n,
+    createdAtTimestamp: 1767052800,
+    updatedAtTimestamp: 1767139200,
+    direction: 'outgoing',
+  },
+  {
+    id: 'history-incoming-demo-stream-2',
+    sender: '0x6666666666666666666666666666666666666666',
+    receiver: DEMO_ADDRESS,
+    token: DEMO_TOKEN,
+    flowRate: 5787037037037n,
+    streamedSoFar: 2200000000000000000n,
+    createdAtTimestamp: 1766966400,
+    updatedAtTimestamp: 1767052800,
+    direction: 'incoming',
+  },
+  {
+    id: 'history-outgoing-demo-stream-3',
+    sender: DEMO_ADDRESS,
+    receiver: '0x7777777777777777777777777777777777777777',
+    token: DEMO_TOKEN,
+    flowRate: 3858024691358n,
+    streamedSoFar: 1400000000000000000n,
+    createdAtTimestamp: 1766880000,
+    updatedAtTimestamp: 1766966400,
+    direction: 'outgoing',
+  },
+]
+
 const samplePools: PoolMembershipItem[] = [
   {
     poolId: DEMO_POOL,
@@ -97,7 +134,7 @@ function createAdapter(
     streams: sampleStreams,
     streamsLoading: false,
     streamsError: null,
-    streamHistory: sampleStreams,
+    streamHistory: sampleStreamHistory,
     streamHistoryLoading: false,
     streamHistoryError: null,
     pools: samplePools,
@@ -115,6 +152,8 @@ function createAdapter(
     setStreamTxHash: null,
     poolConnectStatus: {},
     poolConnectError: {},
+    poolClaimStatus: {},
+    poolClaimError: {},
   }
 
   const actions: StreamingWidgetAdapterActions = {
@@ -129,6 +168,7 @@ function createAdapter(
     resetSetStream: () => {},
     connectToPool: async () => {},
     disconnectFromPool: async () => {},
+    claimFromPool: async () => {},
     ...actionOverrides,
   }
 
@@ -150,7 +190,10 @@ function PreviewStoryShell({
   initialStreamsFormOpen?: boolean
 }) {
   return (
-    <YStack data-testid={dataTestId} style={{ width: 400, minHeight: '100vh' }}>
+    <YStack
+      data-testid={dataTestId}
+      style={{ width: 'min(400px, 100vw)', minHeight: '100vh' }}
+    >
       <StreamingWidgetPreview
         adapter={adapter}
         initialTab={initialTab}
@@ -168,7 +211,10 @@ function StreamingWidgetStoryShell({
   dataTestId: string
 }) {
   return (
-    <YStack data-testid={dataTestId} style={{ width: 400, minHeight: '100vh' }}>
+    <YStack
+      data-testid={dataTestId}
+      style={{ width: 'min(400px, 100vw)', minHeight: '100vh' }}
+    >
       <StreamingWidget provider={provider} environment="development" />
     </YStack>
   )
@@ -224,7 +270,10 @@ function CustodialLocalFixtureStory() {
     )
   } catch (error: unknown) {
     return (
-      <YStack data-testid="StreamingWidget-custodial-config-error" style={{ width: 400 }}>
+      <YStack
+        data-testid="StreamingWidget-custodial-config-error"
+        style={{ width: 'min(400px, 100vw)' }}
+      >
         <strong>Custodial fixture not configured</strong>
         <span>
           {error instanceof Error
@@ -395,7 +444,9 @@ export const CreateUpdateFailure: Story = {
 export const PoolClaimState: Story = {
   render: () => (
     <PreviewStoryShell
-      adapter={createAdapter()}
+      adapter={createAdapter({
+        pools: [{ ...samplePools[0], isConnected: true }],
+      })}
       dataTestId="StreamingWidget-pool-claim"
       initialTab="pools"
     />
@@ -406,7 +457,8 @@ export const PoolClaimPending: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        poolConnectStatus: { [DEMO_POOL]: 'pending' },
+        pools: [{ ...samplePools[0], isConnected: true }],
+        poolClaimStatus: { [DEMO_POOL]: 'pending' },
       })}
       dataTestId="StreamingWidget-pool-claim-pending"
       initialTab="pools"
@@ -419,7 +471,7 @@ export const PoolClaimSuccess: Story = {
     <PreviewStoryShell
       adapter={createAdapter({
         pools: [{ ...samplePools[0], isConnected: true }],
-        poolConnectStatus: { [DEMO_POOL]: 'success' },
+        poolClaimStatus: { [DEMO_POOL]: 'success' },
       })}
       dataTestId="StreamingWidget-pool-claim-success"
       initialTab="pools"
@@ -431,8 +483,9 @@ export const PoolClaimError: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        poolConnectStatus: { [DEMO_POOL]: 'error' },
-        poolConnectError: { [DEMO_POOL]: 'Pool claim failed. Please retry.' },
+        pools: [{ ...samplePools[0], isConnected: true }],
+        poolClaimStatus: { [DEMO_POOL]: 'error' },
+        poolClaimError: { [DEMO_POOL]: 'Pool claim failed. Please retry.' },
       })}
       dataTestId="StreamingWidget-pool-claim-error"
       initialTab="pools"
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index 9f04ce9..15b4a15 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -383,7 +383,9 @@ function StreamsTab({
   )
   const emptyStreamsMessage =
     direction === 'all' ? 'No streams found.' : `No ${direction} streams found.`
-  const recentStreams = state.streamHistory.slice(0, 4)
+  const [historyLimit, setHistoryLimit] = useState(4)
+  const recentStreams = state.streamHistory.slice(0, historyLimit)
+  const hasMoreHistory = state.streamHistory.length > historyLimit
 
   return (
     <StreamingTabContent>
@@ -496,6 +498,12 @@ function StreamsTab({
       {!state.streamHistoryLoading &&
         !state.streamHistoryError &&
         recentStreams.map((stream) => <StreamCard key={`history-${stream.id}`} stream={stream} />)}
+
+      {!state.streamHistoryLoading && !state.streamHistoryError && hasMoreHistory && (
+        <Button variant="secondary" onPress={() => setHistoryLimit((count) => count + 4)}>
+          <ButtonText>Show more</ButtonText>
+        </Button>
+      )}
     </StreamingTabContent>
   )
 }
@@ -507,16 +515,24 @@ function PoolCard({
   pool,
   connectStatus,
   connectError,
+  claimStatus,
+  claimError,
   onConnect,
   onDisconnect,
+  onClaim,
 }: {
   pool: PoolMembershipItem
   connectStatus: WriteStatus
   connectError: string | null
+  claimStatus: WriteStatus
+  claimError: string | null
   onConnect: (poolAddress: Address) => void
   onDisconnect: (poolAddress: Address) => void
+  onClaim: (poolAddress: Address) => void
 }) {
-  const isPending = connectStatus === 'pending'
+  const isConnectPending = connectStatus === 'pending'
+  const isClaimPending = claimStatus === 'pending'
+  const canClaim = pool.isConnected && pool.claimableAmount > 0n
 
   return (
     <PoolRow>
@@ -546,19 +562,33 @@ function PoolCard({
           {connectError}
         </Text>
       )}
+      {claimError && (
+        <Text color="$error" variant="caption">
+          {claimError}
+        </Text>
+      )}
 
       <XStack gap="$2" alignItems="center">
         <WriteStatusBadge status={connectStatus} />
         {pool.isConnected ? (
-          <Button disabled={isPending} onPress={() => onDisconnect(pool.poolId)}>
-            {isPending ? <Spinner size="sm" /> : <ButtonText>Disconnect</ButtonText>}
+          <Button disabled={isConnectPending} onPress={() => onDisconnect(pool.poolId)}>
+            {isConnectPending ? <Spinner size="sm" /> : <ButtonText>Disconnect</ButtonText>}
           </Button>
         ) : (
-          <Button disabled={isPending} onPress={() => onConnect(pool.poolId)}>
-            {isPending ? <Spinner size="sm" /> : <ButtonText>Connect to claim</ButtonText>}
+          <Button disabled={isConnectPending} onPress={() => onConnect(pool.poolId)}>
+            {isConnectPending ? <Spinner size="sm" /> : <ButtonText>Connect to claim</ButtonText>}
           </Button>
         )}
       </XStack>
+
+      {pool.isConnected && (
+        <XStack gap="$2" alignItems="center">
+          <WriteStatusBadge status={claimStatus} />
+          <Button disabled={!canClaim || isClaimPending} onPress={() => onClaim(pool.poolId)}>
+            {isClaimPending ? <Spinner size="sm" /> : <ButtonText>Claim</ButtonText>}
+          </Button>
+        </XStack>
+      )}
     </PoolRow>
   )
 }
@@ -610,8 +640,11 @@ function PoolsTab({
             pool={pool}
             connectStatus={state.poolConnectStatus[pool.poolId] ?? 'idle'}
             connectError={state.poolConnectError[pool.poolId] ?? null}
+            claimStatus={state.poolClaimStatus[pool.poolId] ?? 'idle'}
+            claimError={state.poolClaimError[pool.poolId] ?? null}
             onConnect={actions.connectToPool}
             onDisconnect={actions.disconnectFromPool}
+            onClaim={actions.claimFromPool}
           />
         ))}
     </StreamingTabContent>
diff --git a/packages/streaming-widget/src/adapter.ts b/packages/streaming-widget/src/adapter.ts
index 7745ee3..4612781 100644
--- a/packages/streaming-widget/src/adapter.ts
+++ b/packages/streaming-widget/src/adapter.ts
@@ -30,6 +30,26 @@ import type {
   WriteStatus,
 } from './widgetRuntimeContract'
 
+const GDA_POOL_CLAIM_ABI = [
+  {
+    type: 'function',
+    name: 'getClaimableNow',
+    inputs: [{ name: 'memberAddr', type: 'address' }],
+    outputs: [
+      { name: 'claimableBalance', type: 'int256' },
+      { name: 'timestamp', type: 'uint256' },
+    ],
+    stateMutability: 'view',
+  },
+  {
+    type: 'function',
+    name: 'claimAll',
+    inputs: [],
+    outputs: [{ name: '', type: 'bool' }],
+    stateMutability: 'nonpayable',
+  },
+] as const
+
 // ---------------------------------------------------------------------------
 // Chain descriptors for Superfluid-supported chains (Celo and Base)
 // ---------------------------------------------------------------------------
@@ -208,6 +228,8 @@ export function useStreamingAdapter({
   // --- pool connect/disconnect state keyed by pool address ---
   const [poolConnectStatus, setPoolConnectStatus] = useState<Record<string, WriteStatus>>({})
   const [poolConnectError, setPoolConnectError] = useState<Record<string, string | null>>({})
+  const [poolClaimStatus, setPoolClaimStatus] = useState<Record<string, WriteStatus>>({})
+  const [poolClaimError, setPoolClaimError] = useState<Record<string, string | null>>({})
 
   // Chain validity
   const isWrongChain = !!chainId && !isSupportedChain(chainId)
@@ -294,13 +316,38 @@ export function useStreamingAdapter({
     setPoolsError(null)
     try {
       const result = await gdaSDK.getDistributionPools(address as Address)
-      setPools(result.map(toPoolMembershipItem))
+      const normalizedPools = result.map(toPoolMembershipItem)
+      if (!viemClients) {
+        setPools(normalizedPools)
+        return
+      }
+
+      const poolsWithClaimable = await Promise.all(
+        normalizedPools.map(async (pool) => {
+          try {
+            const [claimableAmount] = await viemClients.publicClient.readContract({
+              address: pool.poolId,
+              abi: GDA_POOL_CLAIM_ABI,
+              functionName: 'getClaimableNow',
+              args: [address as Address],
+            })
+
+            return {
+              ...pool,
+              claimableAmount: claimableAmount > 0n ? claimableAmount : 0n,
+            }
+          } catch {
+            return pool
+          }
+        }),
+      )
+      setPools(poolsWithClaimable)
     } catch (err) {
       setPoolsError(humanReadableError(err))
     } finally {
       setPoolsLoading(false)
     }
-  }, [gdaSDK, address])
+  }, [gdaSDK, address, viemClients])
 
   // ---------------------------------------------------------------------------
   // Fetch Super Token balance
@@ -360,6 +407,10 @@ export function useStreamingAdapter({
       setPools([])
       setSuperTokenBalance(null)
       setSupReserveBalance(null)
+      setPoolConnectStatus({})
+      setPoolConnectError({})
+      setPoolClaimStatus({})
+      setPoolClaimError({})
       return
     }
 
@@ -466,6 +517,34 @@ export function useStreamingAdapter({
     [gdaSDK, fetchPools],
   )
 
+  const claimFromPool = useCallback(
+    async (poolAddress: Address) => {
+      if (!viemClients || !address) return
+
+      setPoolClaimStatus((prev) => ({ ...prev, [poolAddress]: 'pending' }))
+      setPoolClaimError((prev) => ({ ...prev, [poolAddress]: null }))
+
+      try {
+        const hash = await viemClients.walletClient.writeContract({
+          account: address as Address,
+          address: poolAddress,
+          abi: GDA_POOL_CLAIM_ABI,
+          functionName: 'claimAll',
+        })
+        await viemClients.publicClient.waitForTransactionReceipt({ hash })
+        setPoolClaimStatus((prev) => ({ ...prev, [poolAddress]: 'success' }))
+        void fetchPools()
+      } catch (err) {
+        setPoolClaimStatus((prev) => ({ ...prev, [poolAddress]: 'error' }))
+        setPoolClaimError((prev) => ({
+          ...prev,
+          [poolAddress]: humanReadableError(err),
+        }))
+      }
+    },
+    [viemClients, address, fetchPools],
+  )
+
   // ---------------------------------------------------------------------------
   // Chain switch via EIP-1193
   // ---------------------------------------------------------------------------
@@ -511,6 +590,8 @@ export function useStreamingAdapter({
       setStreamTxHash,
       poolConnectStatus,
       poolConnectError,
+      poolClaimStatus,
+      poolClaimError,
     }),
     [
       isConnected,
@@ -538,6 +619,8 @@ export function useStreamingAdapter({
       setStreamTxHash,
       poolConnectStatus,
       poolConnectError,
+      poolClaimStatus,
+      poolClaimError,
     ],
   )
 
@@ -554,6 +637,7 @@ export function useStreamingAdapter({
       resetSetStream,
       connectToPool,
       disconnectFromPool,
+      claimFromPool,
     }),
     [
       connect,
@@ -566,6 +650,7 @@ export function useStreamingAdapter({
       resetSetStream,
       connectToPool,
       disconnectFromPool,
+      claimFromPool,
     ],
   )
 
diff --git a/packages/streaming-widget/src/widgetRuntimeContract.ts b/packages/streaming-widget/src/widgetRuntimeContract.ts
index d19a086..a935c46 100644
--- a/packages/streaming-widget/src/widgetRuntimeContract.ts
+++ b/packages/streaming-widget/src/widgetRuntimeContract.ts
@@ -126,6 +126,9 @@ export interface StreamingWidgetAdapterState {
   /** Pool connect/disconnect write status keyed by pool address */
   poolConnectStatus: Record<string, WriteStatus>
   poolConnectError: Record<string, string | null>
+  /** Pool claim write status keyed by pool address */
+  poolClaimStatus: Record<string, WriteStatus>
+  poolClaimError: Record<string, string | null>
 }
 
 // ---------------------------------------------------------------------------
@@ -150,6 +153,8 @@ export interface StreamingWidgetAdapterActions {
   connectToPool: (poolAddress: Address) => Promise<void>
   /** Disconnect wallet from a GDA pool */
   disconnectFromPool: (poolAddress: Address) => Promise<void>
+  /** Claim all currently claimable distributions from a GDA pool */
+  claimFromPool: (poolAddress: Address) => Promise<void>
 }
 
 export interface StreamingWidgetAdapterResult {
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index 276cd3b..b2d2045 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -94,7 +94,13 @@ test('StreamingWidget shows error states for streams and history', async ({ page
 test('StreamingWidget shows populated incoming and outgoing stream views', async ({ page }) => {
   await gotoStory(page, 'populated-state')
 
-  await expectBodyToContain(page, ['Active streams', 'Stream history', 'Incoming', 'Outgoing'])
+  await expectBodyToContain(page, [
+    'Active streams',
+    'Stream history',
+    'Incoming',
+    'Outgoing',
+    'Show more',
+  ])
 
   await page.getByText('Incoming').first().click()
   await expectBodyToContain(page, ['Incoming'])
@@ -105,6 +111,18 @@ test('StreamingWidget shows populated incoming and outgoing stream views', async
   await saveScreenshot(page, 'sw-07-populated-streams')
 })
 
+test('StreamingWidget renders usable mobile and desktop layouts', async ({ page }) => {
+  await page.setViewportSize({ width: 390, height: 844 })
+  await gotoStory(page, 'populated-state')
+  await expectBodyToContain(page, ['Streams', 'Active streams', 'Stream history'])
+  await saveScreenshot(page, 'sw-18-mobile-populated')
+
+  await page.setViewportSize({ width: 1280, height: 900 })
+  await gotoStory(page, 'populated-state')
+  await expectBodyToContain(page, ['Streams', 'Pools', 'Balances', 'Active streams'])
+  await saveScreenshot(page, 'sw-19-desktop-populated')
+})
+
 test('StreamingWidget create/update form shows invalid input feedback', async ({ page }) => {
   await gotoStory(page, 'create-update-invalid-input')
 
@@ -134,11 +152,11 @@ test('StreamingWidget create/update form shows failure state', async ({ page })
 
 test('StreamingWidget shows pool claim amount and lifecycle states', async ({ page }) => {
   await gotoStory(page, 'pool-claim-state')
-  await expectBodyToContain(page, ['Claimable', '12.5', 'Connect to claim'])
+  await expectBodyToContain(page, ['Claimable', '12.5', 'Claim'])
   await saveScreenshot(page, 'sw-12-pool-claim')
 
   await gotoStory(page, 'pool-claim-pending')
-  await expectBodyToContain(page, ['Claimable'])
+  await expectBodyToContain(page, ['Claimable', '12.5'])
   await saveScreenshot(page, 'sw-13-pool-claim-pending')
 
   await gotoStory(page, 'pool-claim-success')
diff --git a/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png b/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png
index 568fee9336b304ac6eef1cffccfa43eebb466151..04851afe73ecfb9b14db4f2a74dade4a3554303e 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png b/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png
index f170a0bb6f3810e2f83222eebb532a3b0aad9671..0cb733cbf9d27e1357ac851e1080fea9eb5f77c5 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png b/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png
index 5c02cca5730c640b552b7c315ab2b2035a3bd3c6..684c86a781cd7db7708645bc55b2961cea76a194 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png b/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png
index cc6710f6229f598ac063e68836dd62514bc60b36..798c917d92397fae6ef76b591e9a7a799c6e7ef5 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png b/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png
index 553f37328ab7d531bd947b7837f05aa253c34c36..765021ea4ef6f95e64d22332afeba1a7d84570b4 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png b/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png
index 752c5d9f750c818b06891110cbefdfbd38d74af3..958da9b26e070a5090c11fe777d1dd43b959b2a2 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png b/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
index cc976e46e6241551b7745689b9a2e027c46af6ad..ece39c70269586fb0e8a4e457cc157ea1e6f9ee1 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png b/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png
index 9ade97e781135018b1958b011b94f9ac2c128e35..54feed970be4559ce7d8f5cb9aee8881b0e13c8d 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png b/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png
index 5bcb383eaa159981c29ab9992c5121bbc83e26b3..792013620259e19172adaff368d03cc8075f77a5 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-18-mobile-populated.png b/tests/widgets/streaming-widget/test-results/sw-18-mobile-populated.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f8f982e02d75367258ade65822752d11ee89fa4
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-19-desktop-populated.png b/tests/widgets/streaming-widget/test-results/sw-19-desktop-populated.png
new file mode 100644
index 0000000000000000000000000000000000000000..981acdc8f713dfaaf506614610e8d4e58cf0de4b
GIT binary patch

From 3494404d9dc65cb9f29f95af57591a3ce3acf11a Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Mon, 25 May 2026 15:07:37 +0100
Subject: [PATCH 3/7] fix: surface streaming pool claimable load failures

---
 .../StreamingWidget.stories.tsx               |  23 +++++++++++++++++-
 .../streaming-widget/src/StreamingWidget.tsx  |  15 +++++++++++-
 packages/streaming-widget/src/adapter.ts      |   4 ++-
 .../src/widgetRuntimeContract.ts              |   2 ++
 tests/widgets/streaming-widget/states.spec.ts |   4 +++
 .../sw-20-pool-claimable-amount-error.png     | Bin 0 -> 25156 bytes
 6 files changed, 45 insertions(+), 3 deletions(-)
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-20-pool-claimable-amount-error.png

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 5aa7e52..9494d47 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -19,7 +19,7 @@ import {
 } from '../../fixtures/injectedEip1193'
 import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193'
 
-const DEMO_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
+const DEMO_ADDRESS = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
 const DEMO_RECEIVER = '0x1111111111111111111111111111111111111111'
 const DEMO_SENDER = '0x2222222222222222222222222222222222222222'
 const DEMO_TOKEN = '0x3333333333333333333333333333333333333333'
@@ -74,6 +74,7 @@ const sampleStreams: StreamListItem[] = [
   },
 ]
 
+// Mirrors the current SDK-backed adapter; diverge this once past-stream history is fetched separately.
 const sampleStreamHistory: StreamListItem[] = [
   ...sampleStreams,
   {
@@ -117,6 +118,7 @@ const samplePools: PoolMembershipItem[] = [
     poolToken: DEMO_TOKEN,
     totalUnits: 250000000000000000000n,
     claimableAmount: 12500000000000000000n,
+    claimableAmountError: false,
     totalAmountClaimed: 48000000000000000000n,
     isConnected: false,
   },
@@ -493,6 +495,25 @@ export const PoolClaimError: Story = {
   ),
 }
 
+export const PoolClaimableAmountError: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        pools: [
+          {
+            ...samplePools[0],
+            isConnected: true,
+            claimableAmount: 0n,
+            claimableAmountError: true,
+          },
+        ],
+      })}
+      dataTestId="StreamingWidget-pool-claimable-amount-error"
+      initialTab="pools"
+    />
+  ),
+}
+
 export const BaseSupBalanceAndReserve: Story = {
   render: () => (
     <PreviewStoryShell
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index 15b4a15..5b4ec63 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -520,6 +520,7 @@ function PoolCard({
   onConnect,
   onDisconnect,
   onClaim,
+  onRetryClaimable,
 }: {
   pool: PoolMembershipItem
   connectStatus: WriteStatus
@@ -529,10 +530,11 @@ function PoolCard({
   onConnect: (poolAddress: Address) => void
   onDisconnect: (poolAddress: Address) => void
   onClaim: (poolAddress: Address) => void
+  onRetryClaimable: () => void
 }) {
   const isConnectPending = connectStatus === 'pending'
   const isClaimPending = claimStatus === 'pending'
-  const canClaim = pool.isConnected && pool.claimableAmount > 0n
+  const canClaim = pool.isConnected && pool.claimableAmount > 0n && !pool.claimableAmountError
 
   return (
     <PoolRow>
@@ -567,6 +569,16 @@ function PoolCard({
           {claimError}
         </Text>
       )}
+      {pool.claimableAmountError && (
+        <XStack gap="$1" alignItems="center" flexWrap="wrap">
+          <Text color="$error" variant="caption">
+            Could not load claimable amount.
+          </Text>
+          <Button variant="text" onPress={onRetryClaimable}>
+            <ButtonText>Tap to retry</ButtonText>
+          </Button>
+        </XStack>
+      )}
 
       <XStack gap="$2" alignItems="center">
         <WriteStatusBadge status={connectStatus} />
@@ -645,6 +657,7 @@ function PoolsTab({
             onConnect={actions.connectToPool}
             onDisconnect={actions.disconnectFromPool}
             onClaim={actions.claimFromPool}
+            onRetryClaimable={actions.refreshPools}
           />
         ))}
     </StreamingTabContent>
diff --git a/packages/streaming-widget/src/adapter.ts b/packages/streaming-widget/src/adapter.ts
index 4612781..3d7e8fe 100644
--- a/packages/streaming-widget/src/adapter.ts
+++ b/packages/streaming-widget/src/adapter.ts
@@ -150,6 +150,7 @@ function toPoolMembershipItem(pool: GDAPool): PoolMembershipItem {
     poolToken: pool.token,
     totalUnits: pool.totalUnits,
     claimableAmount: poolWithClaimable.claimableAmount ?? 0n,
+    claimableAmountError: false,
     totalAmountClaimed: pool.totalAmountClaimed,
     isConnected: pool.isConnected ?? false,
   }
@@ -335,9 +336,10 @@ export function useStreamingAdapter({
             return {
               ...pool,
               claimableAmount: claimableAmount > 0n ? claimableAmount : 0n,
+              claimableAmountError: false,
             }
           } catch {
-            return pool
+            return { ...pool, claimableAmountError: true }
           }
         }),
       )
diff --git a/packages/streaming-widget/src/widgetRuntimeContract.ts b/packages/streaming-widget/src/widgetRuntimeContract.ts
index a935c46..989c521 100644
--- a/packages/streaming-widget/src/widgetRuntimeContract.ts
+++ b/packages/streaming-widget/src/widgetRuntimeContract.ts
@@ -64,6 +64,8 @@ export interface PoolMembershipItem {
   totalUnits: bigint
   /** Claimable incoming distribution amount in wei, when exposed by the data source */
   claimableAmount: bigint
+  /** True when the claimable amount read fails and should be retried */
+  claimableAmountError: boolean
   totalAmountClaimed: bigint
   /** Whether this account has actively connected to the pool distribution */
   isConnected: boolean
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index b2d2045..4106488 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -166,6 +166,10 @@ test('StreamingWidget shows pool claim amount and lifecycle states', async ({ pa
   await gotoStory(page, 'pool-claim-error')
   await expectBodyToContain(page, ['Pool claim failed. Please retry.', 'Failed'])
   await saveScreenshot(page, 'sw-15-pool-claim-error')
+
+  await gotoStory(page, 'pool-claimable-amount-error')
+  await expectBodyToContain(page, ['Could not load claimable amount.', 'Tap to retry'])
+  await saveScreenshot(page, 'sw-20-pool-claimable-amount-error')
 })
 
 test('StreamingWidget shows Base SUP reserve and disables reserve off Base', async ({ page }) => {
diff --git a/tests/widgets/streaming-widget/test-results/sw-20-pool-claimable-amount-error.png b/tests/widgets/streaming-widget/test-results/sw-20-pool-claimable-amount-error.png
new file mode 100644
index 0000000000000000000000000000000000000000..63696e17470030300965bc475c08d90a00707dbc
GIT binary patch

From 9213ba79430d8856819dbe6091c29d94dae0d0bf Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Fri, 29 May 2026 10:28:46 +0100
Subject: [PATCH 4/7] fix: address streaming widget review feedback

---
 .../StreamingWidget.stories.tsx               |  82 +++++---
 .../streaming-widget/src/StreamingWidget.tsx  | 176 ++++++++++++++----
 packages/streaming-widget/src/adapter.ts      |  30 ++-
 packages/streaming-widget/src/index.ts        |   1 +
 .../src/widgetRuntimeContract.ts              |  13 +-
 packages/ui/src/components/Button.tsx         |  10 +-
 packages/ui/src/components/TokenAmount.tsx    |  50 ++---
 packages/ui/src/index.ts                      |   2 +-
 tests/widgets/streaming-widget/states.spec.ts |  55 ++++--
 .../test-results/sw-01-no-wallet.png          | Bin 23601 -> 23511 bytes
 .../test-results/sw-02-tab-navigation.png     | Bin 28625 -> 28575 bytes
 .../test-results/sw-03-wrong-chain.png        | Bin 26244 -> 26063 bytes
 .../test-results/sw-04-loading-state.png      | Bin 33722 -> 20527 bytes
 .../test-results/sw-05-empty-state.png        | Bin 37856 -> 21746 bytes
 .../test-results/sw-06-error-state.png        | Bin 42592 -> 23415 bytes
 .../test-results/sw-07-populated-streams.png  | Bin 82944 -> 31904 bytes
 .../sw-08-create-update-invalid.png           | Bin 110929 -> 58445 bytes
 .../sw-09-create-update-pending.png           | Bin 111143 -> 58113 bytes
 .../sw-10-create-update-success.png           | Bin 113653 -> 60375 bytes
 .../sw-11-create-update-failure.png           | Bin 113863 -> 60586 bytes
 .../test-results/sw-12-pool-claim.png         | Bin 22717 -> 23479 bytes
 .../test-results/sw-13-pool-claim-pending.png | Bin 22373 -> 23415 bytes
 .../test-results/sw-14-pool-claim-success.png | Bin 23616 -> 24463 bytes
 .../test-results/sw-15-pool-claim-error.png   | Bin 24905 -> 25796 bytes
 .../test-results/sw-16-base-sup-reserve.png   | Bin 28968 -> 34143 bytes
 .../sw-17-non-base-reserve-disabled.png       | Bin 28625 -> 28575 bytes
 .../test-results/sw-18-mobile-populated.png   | Bin 78869 -> 35788 bytes
 .../test-results/sw-19-desktop-populated.png  | Bin 91077 -> 40113 bytes
 .../sw-20-pool-claimable-amount-error.png     | Bin 25156 -> 26380 bytes
 .../sw-21-pool-claimable-retry-loading.png    | Bin 0 -> 14629 bytes
 .../test-results/sw-22-stream-history-tab.png | Bin 0 -> 55075 bytes
 31 files changed, 310 insertions(+), 109 deletions(-)
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-21-pool-claimable-retry-loading.png
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-22-stream-history-tab.png

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 9494d47..29be258 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -24,6 +24,7 @@ const DEMO_RECEIVER = '0x1111111111111111111111111111111111111111'
 const DEMO_SENDER = '0x2222222222222222222222222222222222222222'
 const DEMO_TOKEN = '0x3333333333333333333333333333333333333333'
 const DEMO_POOL = '0x4444444444444444444444444444444444444444'
+const DEMO_RESERVE_LOCKER = '0x8888888888888888888888888888888888888888'
 
 const defaultForm: SetStreamFormState = {
   receiver: '',
@@ -146,6 +147,7 @@ function createAdapter(
     balanceLoading: false,
     balanceError: null,
     supReserveBalance: null,
+    supReserveLockers: [],
     supReserveLoading: false,
     supReserveError: null,
     setStreamForm: defaultForm,
@@ -194,7 +196,12 @@ function PreviewStoryShell({
   return (
     <YStack
       data-testid={dataTestId}
-      style={{ width: 'min(400px, 100vw)', minHeight: '100vh' }}
+      style={{
+        width: '100%',
+        maxWidth: 400,
+        minHeight: '100vh',
+        boxSizing: 'border-box',
+      }}
     >
       <StreamingWidgetPreview
         adapter={adapter}
@@ -215,7 +222,12 @@ function StreamingWidgetStoryShell({
   return (
     <YStack
       data-testid={dataTestId}
-      style={{ width: 'min(400px, 100vw)', minHeight: '100vh' }}
+      style={{
+        width: '100%',
+        maxWidth: 400,
+        minHeight: '100vh',
+        boxSizing: 'border-box',
+      }}
     >
       <StreamingWidget provider={provider} environment="development" />
     </YStack>
@@ -287,6 +299,37 @@ function CustodialLocalFixtureStory() {
   }
 }
 
+function PoolClaimableAmountErrorStory() {
+  const [retrying, setRetrying] = React.useState(false)
+
+  return (
+    <PreviewStoryShell
+      adapter={createAdapter(
+        {
+          pools: retrying
+            ? []
+            : [
+                {
+                  ...samplePools[0],
+                  isConnected: false,
+                  claimableAmount: 0n,
+                  claimableAmountError: true,
+                },
+              ],
+          poolsLoading: retrying,
+        },
+        {
+          refreshPools: async () => {
+            setRetrying(true)
+          },
+        },
+      )}
+      dataTestId="StreamingWidget-pool-claimable-amount-error"
+      initialTab="pools"
+    />
+  )
+}
+
 export const InjectedWallet: Story = {
   render: () => <InjectedWalletStory />,
 }
@@ -447,7 +490,7 @@ export const PoolClaimState: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: true }],
+        pools: [{ ...samplePools[0], isConnected: false }],
       })}
       dataTestId="StreamingWidget-pool-claim"
       initialTab="pools"
@@ -459,7 +502,7 @@ export const PoolClaimPending: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: true }],
+        pools: [{ ...samplePools[0], isConnected: false }],
         poolClaimStatus: { [DEMO_POOL]: 'pending' },
       })}
       dataTestId="StreamingWidget-pool-claim-pending"
@@ -472,7 +515,7 @@ export const PoolClaimSuccess: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: true }],
+        pools: [{ ...samplePools[0], isConnected: false }],
         poolClaimStatus: { [DEMO_POOL]: 'success' },
       })}
       dataTestId="StreamingWidget-pool-claim-success"
@@ -485,7 +528,7 @@ export const PoolClaimError: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: true }],
+        pools: [{ ...samplePools[0], isConnected: false }],
         poolClaimStatus: { [DEMO_POOL]: 'error' },
         poolClaimError: { [DEMO_POOL]: 'Pool claim failed. Please retry.' },
       })}
@@ -496,22 +539,7 @@ export const PoolClaimError: Story = {
 }
 
 export const PoolClaimableAmountError: Story = {
-  render: () => (
-    <PreviewStoryShell
-      adapter={createAdapter({
-        pools: [
-          {
-            ...samplePools[0],
-            isConnected: true,
-            claimableAmount: 0n,
-            claimableAmountError: true,
-          },
-        ],
-      })}
-      dataTestId="StreamingWidget-pool-claimable-amount-error"
-      initialTab="pools"
-    />
-  ),
+  render: () => <PoolClaimableAmountErrorStory />,
 }
 
 export const BaseSupBalanceAndReserve: Story = {
@@ -520,7 +548,15 @@ export const BaseSupBalanceAndReserve: Story = {
       adapter={createAdapter({
         chainId: STREAMING_CHAINS.BASE,
         superTokenBalance: '712.10',
-        supReserveBalance: '95.25',
+        supReserveBalance: '112.75',
+        supReserveLockers: [
+          {
+            address: DEMO_RESERVE_LOCKER,
+            stakedBalance: 95250000000000000000n,
+            unstakedBalance: 17500000000000000000n,
+            totalBalance: 112750000000000000000n,
+          },
+        ],
       })}
       dataTestId="StreamingWidget-base-sup-reserve"
       initialTab="balances"
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index 5b4ec63..ada5763 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -6,6 +6,7 @@ import {
   Card,
   Heading,
   Text,
+  Anchor,
   Button,
   ButtonText,
   Spinner,
@@ -20,6 +21,7 @@ import {
   AddressDisplay,
   TokenAmount,
   WidgetTabs,
+  formatDisplayAmount,
 } from '@goodwidget/ui'
 import type { Address } from 'viem'
 import { formatUnits } from 'viem'
@@ -115,7 +117,7 @@ function formatFlowRateDisplay(flowRate: bigint, decimals = 18): string {
   if (flowRate === 0n) return '0'
   // Convert wei/s → per-month amount for display
   const perMonth = flowRate * BigInt(30 * 24 * 60 * 60)
-  return formatUnits(perMonth, decimals)
+  return formatDisplayAmount(formatUnits(perMonth, decimals))
 }
 
 /** Formats a unix timestamp (seconds) to a short locale date string */
@@ -135,6 +137,19 @@ function chainName(chainId: number): string {
   return `Chain ${chainId}`
 }
 
+function tokenSymbol(chainId: number | null): 'G$' | 'SUP' {
+  return chainId === STREAMING_CHAINS.BASE ? 'SUP' : 'G$'
+}
+
+function formatWeiAmount(amount: bigint): string {
+  return formatDisplayAmount(formatUnits(amount, 18))
+}
+
+function superfluidExplorerAccountUrl(chainId: number | null, address: Address): string {
+  const networkSlug = chainId === STREAMING_CHAINS.BASE ? 'base-mainnet' : 'celo'
+  return `https://explorer.superfluid.org/${networkSlug}/accounts/${address}`
+}
+
 // ---------------------------------------------------------------------------
 // Write-status badge helper
 // ---------------------------------------------------------------------------
@@ -213,6 +228,7 @@ function WalletGate({
 // ---------------------------------------------------------------------------
 function SetStreamForm({
   form,
+  token,
   status,
   error,
   txHash,
@@ -221,6 +237,7 @@ function SetStreamForm({
   onReset,
 }: {
   form: StreamingWidgetAdapterState['setStreamForm']
+  token: 'G$' | 'SUP'
   status: WriteStatus
   error: string | null
   txHash: string | null
@@ -248,7 +265,7 @@ function SetStreamForm({
       {/* Amount + time unit */}
       <XStack gap="$2" alignItems="flex-end">
         <YStack flex={1} gap="$1">
-          <Text variant="label">Amount</Text>
+          <Text variant="label">Amount ({token})</Text>
           <Input
             placeholder="100"
             value={form.amount}
@@ -272,7 +289,7 @@ function SetStreamForm({
       {form.flowRate !== null && form.flowRate > 0n && (
         <XStack gap="$2" alignItems="center">
           <Text variant="caption" secondary>
-            About {formatUnits(form.flowRate, 18)} tokens/s
+            About {formatWeiAmount(form.flowRate)} {token}/s
           </Text>
         </XStack>
       )}
@@ -321,7 +338,7 @@ function SetStreamForm({
 // ---------------------------------------------------------------------------
 // Single stream row
 // ---------------------------------------------------------------------------
-function StreamCard({ stream }: { stream: StreamListItem }) {
+function StreamCard({ stream, token }: { stream: StreamListItem; token: 'G$' | 'SUP' }) {
   const counterparty =
     stream.direction === 'outgoing' ? stream.receiver : stream.sender
   const flowPerMonth = formatFlowRateDisplay(stream.flowRate)
@@ -343,9 +360,12 @@ function StreamCard({ stream }: { stream: StreamListItem }) {
         <Text variant="caption" secondary>
           Flow rate
         </Text>
-        <Text fontWeight="600">
-          {flowPerMonth}/mo
-        </Text>
+        <XStack gap="$1" alignItems="baseline">
+          <TokenAmount token={token} amount={flowPerMonth} size="sm" />
+          <Text variant="caption" secondary>
+            /mo
+          </Text>
+        </XStack>
       </XStack>
 
       {stream.streamedSoFar > 0n && (
@@ -353,7 +373,7 @@ function StreamCard({ stream }: { stream: StreamListItem }) {
           <Text variant="caption" secondary>
             Streamed so far
           </Text>
-          <Text>{formatUnits(stream.streamedSoFar, 18)}</Text>
+          <TokenAmount token={token} amount={formatWeiAmount(stream.streamedSoFar)} size="sm" />
         </XStack>
       )}
     </StreamRow>
@@ -383,9 +403,7 @@ function StreamsTab({
   )
   const emptyStreamsMessage =
     direction === 'all' ? 'No streams found.' : `No ${direction} streams found.`
-  const [historyLimit, setHistoryLimit] = useState(4)
-  const recentStreams = state.streamHistory.slice(0, historyLimit)
-  const hasMoreHistory = state.streamHistory.length > historyLimit
+  const activeToken = tokenSymbol(state.chainId)
 
   return (
     <StreamingTabContent>
@@ -399,6 +417,7 @@ function StreamsTab({
       {showForm && (
         <SetStreamForm
           form={state.setStreamForm}
+          token={activeToken}
           status={state.setStreamStatus}
           error={state.setStreamError}
           txHash={state.setStreamTxHash}
@@ -458,10 +477,30 @@ function StreamsTab({
 
       {!state.streamsLoading &&
         !state.streamsError &&
-        filteredStreams.map((stream) => <StreamCard key={stream.id} stream={stream} />)}
+        filteredStreams.map((stream) => (
+          <StreamCard key={stream.id} stream={stream} token={activeToken} />
+        ))}
+    </StreamingTabContent>
+  )
+}
 
-      <Separator />
+// ---------------------------------------------------------------------------
+// Stream history tab
+// ---------------------------------------------------------------------------
+function HistoryTab({
+  state,
+  actions,
+}: {
+  state: StreamingWidgetAdapterState
+  actions: StreamingWidgetAdapterActions
+}) {
+  const [historyLimit, setHistoryLimit] = useState(4)
+  const recentStreams = state.streamHistory.slice(0, historyLimit)
+  const hasMoreHistory = state.streamHistory.length > historyLimit
+  const activeToken = tokenSymbol(state.chainId)
 
+  return (
+    <StreamingTabContent>
       <XStack justifyContent="space-between" alignItems="center">
         <Heading level={4}>Stream history</Heading>
         <Button variant="secondary" onPress={actions.refreshStreamHistory}>
@@ -497,7 +536,9 @@ function StreamsTab({
 
       {!state.streamHistoryLoading &&
         !state.streamHistoryError &&
-        recentStreams.map((stream) => <StreamCard key={`history-${stream.id}`} stream={stream} />)}
+        recentStreams.map((stream) => (
+          <StreamCard key={`history-${stream.id}`} stream={stream} token={activeToken} />
+        ))}
 
       {!state.streamHistoryLoading && !state.streamHistoryError && hasMoreHistory && (
         <Button variant="secondary" onPress={() => setHistoryLimit((count) => count + 4)}>
@@ -513,6 +554,7 @@ function StreamsTab({
 // ---------------------------------------------------------------------------
 function PoolCard({
   pool,
+  token,
   connectStatus,
   connectError,
   claimStatus,
@@ -523,6 +565,7 @@ function PoolCard({
   onRetryClaimable,
 }: {
   pool: PoolMembershipItem
+  token: 'G$' | 'SUP'
   connectStatus: WriteStatus
   connectError: string | null
   claimStatus: WriteStatus
@@ -534,7 +577,7 @@ function PoolCard({
 }) {
   const isConnectPending = connectStatus === 'pending'
   const isClaimPending = claimStatus === 'pending'
-  const canClaim = pool.isConnected && pool.claimableAmount > 0n && !pool.claimableAmountError
+  const canClaim = !pool.isConnected && pool.claimableAmount > 0n && !pool.claimableAmountError
 
   return (
     <PoolRow>
@@ -549,14 +592,14 @@ function PoolCard({
         <Text variant="caption" secondary>
           Claimable
         </Text>
-        <Text>{formatUnits(pool.claimableAmount, 18)}</Text>
+        <TokenAmount token={token} amount={formatWeiAmount(pool.claimableAmount)} size="sm" />
       </XStack>
 
       <XStack justifyContent="space-between" alignItems="center">
         <Text variant="caption" secondary>
           Total claimed
         </Text>
-        <Text>{formatUnits(pool.totalAmountClaimed, 18)}</Text>
+        <TokenAmount token={token} amount={formatWeiAmount(pool.totalAmountClaimed)} size="sm" />
       </XStack>
 
       {connectError && (
@@ -574,33 +617,46 @@ function PoolCard({
           <Text color="$error" variant="caption">
             Could not load claimable amount.
           </Text>
-          <Button variant="text" onPress={onRetryClaimable}>
-            <ButtonText>Tap to retry</ButtonText>
+          <Button variant="secondary" size="sm" onPress={onRetryClaimable}>
+            <ButtonText>Retry</ButtonText>
           </Button>
         </XStack>
       )}
 
-      <XStack gap="$2" alignItems="center">
-        <WriteStatusBadge status={connectStatus} />
+      <XStack gap="$2" alignItems="center" flexWrap="wrap">
         {pool.isConnected ? (
-          <Button disabled={isConnectPending} onPress={() => onDisconnect(pool.poolId)}>
-            {isConnectPending ? <Spinner size="sm" /> : <ButtonText>Disconnect</ButtonText>}
+          <Button
+            variant="secondary"
+            disabled={isConnectPending}
+            borderColor="$error"
+            onPress={() => onDisconnect(pool.poolId)}
+          >
+            {isConnectPending ? (
+              <Spinner size="sm" />
+            ) : (
+              <ButtonText color="$error">Disconnect</ButtonText>
+            )}
           </Button>
         ) : (
-          <Button disabled={isConnectPending} onPress={() => onConnect(pool.poolId)}>
-            {isConnectPending ? <Spinner size="sm" /> : <ButtonText>Connect to claim</ButtonText>}
-          </Button>
+          <>
+            <WriteStatusBadge status={claimStatus} />
+            <Button
+              disabled={!canClaim || isClaimPending || claimStatus === 'success'}
+              onPress={() => onClaim(pool.poolId)}
+            >
+              {isClaimPending ? <Spinner size="sm" /> : <ButtonText>Claim</ButtonText>}
+            </Button>
+            <WriteStatusBadge status={connectStatus} />
+            <Button
+              variant="secondary"
+              disabled={isConnectPending}
+              onPress={() => onConnect(pool.poolId)}
+            >
+              {isConnectPending ? <Spinner size="sm" /> : <ButtonText>Connect</ButtonText>}
+            </Button>
+          </>
         )}
       </XStack>
-
-      {pool.isConnected && (
-        <XStack gap="$2" alignItems="center">
-          <WriteStatusBadge status={claimStatus} />
-          <Button disabled={!canClaim || isClaimPending} onPress={() => onClaim(pool.poolId)}>
-            {isClaimPending ? <Spinner size="sm" /> : <ButtonText>Claim</ButtonText>}
-          </Button>
-        </XStack>
-      )}
     </PoolRow>
   )
 }
@@ -615,6 +671,8 @@ function PoolsTab({
   state: StreamingWidgetAdapterState
   actions: StreamingWidgetAdapterActions
 }) {
+  const activeToken = tokenSymbol(state.chainId)
+
   return (
     <StreamingTabContent>
       {state.poolsLoading && (
@@ -650,6 +708,7 @@ function PoolsTab({
           <PoolCard
             key={pool.poolId}
             pool={pool}
+            token={activeToken}
             connectStatus={state.poolConnectStatus[pool.poolId] ?? 'idle'}
             connectError={state.poolConnectError[pool.poolId] ?? null}
             claimStatus={state.poolClaimStatus[pool.poolId] ?? 'idle'}
@@ -675,6 +734,7 @@ function BalancesTab({
   actions: StreamingWidgetAdapterActions
 }) {
   const isOnBase = state.chainId === STREAMING_CHAINS.BASE
+  const activeToken = tokenSymbol(state.chainId)
 
   return (
     <StreamingTabContent>
@@ -697,7 +757,7 @@ function BalancesTab({
 
         {!state.balanceLoading && !state.balanceError && state.superTokenBalance !== null && (
           <TokenAmount
-            token={state.chainId === STREAMING_CHAINS.BASE ? 'SUP' : 'G$'}
+            token={activeToken}
             amount={state.superTokenBalance}
             size="xl"
           />
@@ -714,7 +774,7 @@ function BalancesTab({
       {isOnBase ? (
         <BalanceCard>
           <XStack justifyContent="space-between" alignItems="center">
-            <Text variant="label">SUP Reserve (Staked)</Text>
+            <Text variant="label">SUP Reserve</Text>
             {state.supReserveLoading && <Spinner size="sm" />}
           </XStack>
 
@@ -729,8 +789,44 @@ function BalancesTab({
           )}
 
           <Text variant="caption" secondary>
-            SUP tokens locked as reserve on Base.
+            SUP held in reserve lockers on Base.
           </Text>
+
+          {!state.supReserveLoading &&
+            !state.supReserveError &&
+            state.supReserveLockers.map((locker) => (
+              <YStack key={locker.address} gap="$2">
+                <XStack justifyContent="space-between" alignItems="center">
+                  <Text variant="caption" secondary>
+                    Reserve locker
+                  </Text>
+                  <AddressDisplay address={locker.address} size="sm" />
+                </XStack>
+                <XStack justifyContent="space-between" alignItems="center">
+                  <Text variant="caption" secondary>
+                    Available
+                  </Text>
+                  <TokenAmount
+                    token="SUP"
+                    amount={formatWeiAmount(locker.unstakedBalance)}
+                    size="sm"
+                  />
+                </XStack>
+                <XStack justifyContent="space-between" alignItems="center">
+                  <Text variant="caption" secondary>
+                    Staked
+                  </Text>
+                  <TokenAmount
+                    token="SUP"
+                    amount={formatWeiAmount(locker.stakedBalance)}
+                    size="sm"
+                  />
+                </XStack>
+                <Anchor href={superfluidExplorerAccountUrl(state.chainId, locker.address)}>
+                  Open in Superfluid Explorer
+                </Anchor>
+              </YStack>
+            ))}
         </BalanceCard>
       ) : (
         <BalanceCard>
@@ -793,16 +889,18 @@ function StreamingWidgetView({
           initialFormOpen={initialStreamsFormOpen}
         />
       )}
+      {activeTab === 'history' && <HistoryTab state={state} actions={actions} />}
       {activeTab === 'pools' && <PoolsTab state={state} actions={actions} />}
       {activeTab === 'balances' && <BalancesTab state={state} actions={actions} />}
     </>
   )
 
   return (
-    <YStack gap="$3" padding="$4">
+    <YStack gap="$3" padding="$5" width="100%" style={{ boxSizing: 'border-box' }}>
       <WidgetTabs
         tabs={[
           { id: 'streams', label: 'Streams' },
+          { id: 'history', label: 'History' },
           { id: 'pools', label: 'Pools' },
           { id: 'balances', label: 'Balances' },
         ]}
diff --git a/packages/streaming-widget/src/adapter.ts b/packages/streaming-widget/src/adapter.ts
index 3d7e8fe..9517152 100644
--- a/packages/streaming-widget/src/adapter.ts
+++ b/packages/streaming-widget/src/adapter.ts
@@ -217,6 +217,9 @@ export function useStreamingAdapter({
 
   // --- SUP reserve state (Base only) ---
   const [supReserveBalance, setSupReserveBalance] = useState<string | null>(null)
+  const [supReserveLockers, setSupReserveLockers] = useState<
+    StreamingWidgetAdapterState['supReserveLockers']
+  >([])
   const [supReserveLoading, setSupReserveLoading] = useState(false)
   const [supReserveError, setSupReserveError] = useState<string | null>(null)
 
@@ -373,8 +376,9 @@ export function useStreamingAdapter({
   // Fetch SUP reserve balance (Base only)
   // ---------------------------------------------------------------------------
   const fetchSupReserve = useCallback(async () => {
-    if (!subgraphClient || !address || chainId !== SupportedChains.BASE) {
+    if (!subgraphClient || !streamingSDK || !address || chainId !== SupportedChains.BASE) {
       setSupReserveBalance(null)
+      setSupReserveLockers([])
       return
     }
 
@@ -382,14 +386,31 @@ export function useStreamingAdapter({
     setSupReserveError(null)
     try {
       const lockers = await subgraphClient.querySUPReserves(address as Address)
-      const total = lockers.reduce((sum, l) => sum + l.stakedBalance, 0n)
+      const lockersWithBalances = await Promise.all(
+        lockers.map(async (locker) => {
+          const unstakedBalance = await streamingSDK.getSuperTokenBalance(
+            locker.id as Address,
+            'SUP',
+          )
+          return {
+            address: locker.id as Address,
+            stakedBalance: locker.stakedBalance,
+            unstakedBalance,
+            totalBalance: locker.stakedBalance + unstakedBalance,
+          }
+        }),
+      )
+      const total = lockersWithBalances.reduce((sum, locker) => sum + locker.totalBalance, 0n)
+      setSupReserveLockers(lockersWithBalances)
       setSupReserveBalance(formatUnits(total, 18))
     } catch (err) {
+      setSupReserveBalance(null)
+      setSupReserveLockers([])
       setSupReserveError(humanReadableError(err))
     } finally {
       setSupReserveLoading(false)
     }
-  }, [subgraphClient, address, chainId])
+  }, [subgraphClient, streamingSDK, address, chainId])
 
   // ---------------------------------------------------------------------------
   // Auto-fetch on wallet/chain change
@@ -409,6 +430,7 @@ export function useStreamingAdapter({
       setPools([])
       setSuperTokenBalance(null)
       setSupReserveBalance(null)
+      setSupReserveLockers([])
       setPoolConnectStatus({})
       setPoolConnectError({})
       setPoolClaimStatus({})
@@ -584,6 +606,7 @@ export function useStreamingAdapter({
       balanceLoading,
       balanceError,
       supReserveBalance,
+      supReserveLockers,
       supReserveLoading,
       supReserveError,
       setStreamForm,
@@ -613,6 +636,7 @@ export function useStreamingAdapter({
       balanceLoading,
       balanceError,
       supReserveBalance,
+      supReserveLockers,
       supReserveLoading,
       supReserveError,
       setStreamForm,
diff --git a/packages/streaming-widget/src/index.ts b/packages/streaming-widget/src/index.ts
index 8c420d5..2981ad4 100644
--- a/packages/streaming-widget/src/index.ts
+++ b/packages/streaming-widget/src/index.ts
@@ -7,6 +7,7 @@ export type {
   StreamTimeUnit,
   StreamListItem,
   PoolMembershipItem,
+  SupReserveLockerItem,
   SetStreamFormState,
   WriteStatus,
   StreamingWidgetAdapterState,
diff --git a/packages/streaming-widget/src/widgetRuntimeContract.ts b/packages/streaming-widget/src/widgetRuntimeContract.ts
index 989c521..f281390 100644
--- a/packages/streaming-widget/src/widgetRuntimeContract.ts
+++ b/packages/streaming-widget/src/widgetRuntimeContract.ts
@@ -29,7 +29,7 @@ export type StreamTimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'mo
 // ---------------------------------------------------------------------------
 // Widget tab IDs
 // ---------------------------------------------------------------------------
-export type StreamingWidgetTab = 'streams' | 'pools' | 'balances'
+export type StreamingWidgetTab = 'streams' | 'history' | 'pools' | 'balances'
 
 // ---------------------------------------------------------------------------
 // Operation lifecycle status for write actions
@@ -71,6 +71,16 @@ export interface PoolMembershipItem {
   isConnected: boolean
 }
 
+// ---------------------------------------------------------------------------
+// SUP reserve locker displayed on Base
+// ---------------------------------------------------------------------------
+export interface SupReserveLockerItem {
+  address: Address
+  stakedBalance: bigint
+  unstakedBalance: bigint
+  totalBalance: bigint
+}
+
 // ---------------------------------------------------------------------------
 // Form state for create/update stream
 // ---------------------------------------------------------------------------
@@ -116,6 +126,7 @@ export interface StreamingWidgetAdapterState {
 
   /** SUP reserve data — only populated on Base */
   supReserveBalance: string | null
+  supReserveLockers: SupReserveLockerItem[]
   supReserveLoading: boolean
   supReserveError: string | null
 
diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx
index 09fafe4..24874ed 100644
--- a/packages/ui/src/components/Button.tsx
+++ b/packages/ui/src/components/Button.tsx
@@ -32,8 +32,8 @@ export const ButtonFrame = createComponent(Stack, {
   backgroundColor: '$background',
   // GoodWalletV2 brand: solid buttons use pill radius by default
   borderRadius: '$full',
-  paddingHorizontal: '$4',
-  height: '$10',
+  paddingHorizontal: '$3',
+  height: '$7',
   gap: '$2',
   cursor: 'pointer',
   borderWidth: 0,
@@ -109,9 +109,9 @@ export const ButtonFrame = createComponent(Stack, {
 
     // Standard interactive sizes
     size: {
-      sm: { height: '$8', paddingHorizontal: '$3', gap: '$1' },
-      md: { height: '$10', paddingHorizontal: '$4', gap: '$2' },
-      lg: { height: '$11', paddingHorizontal: '$5', gap: '$2' },
+      sm: { height: '$6', paddingHorizontal: '$2', gap: '$1' },
+      md: { height: '$7', paddingHorizontal: '$3', gap: '$2' },
+      lg: { height: '$8', paddingHorizontal: '$4', gap: '$2' },
     },
 
     // Icon-only sizes (transparent bg, square, no padding)
diff --git a/packages/ui/src/components/TokenAmount.tsx b/packages/ui/src/components/TokenAmount.tsx
index fd6ebe3..4326128 100644
--- a/packages/ui/src/components/TokenAmount.tsx
+++ b/packages/ui/src/components/TokenAmount.tsx
@@ -51,19 +51,31 @@ interface TokenAmountProps {
   decimals?: number
   variant?: 'secondary'
   useAbbreviations?: boolean
+  maxSignificantDigits?: number
 }
 
-const getDecimals = (value: number): number => {
-  const absValue = Math.abs(value)
-  if (absValue >= 1 || absValue === 0) return 2
-  if (absValue >= 0.1) return 3
-  if (absValue >= 0.01) return 4
-  if (absValue >= 0.001) return 5
-  if (absValue >= 0.0001) return 6
-  if (absValue >= 0.00001) return 7
-  if (absValue >= 0.000001) return 8
-  if (absValue >= 0.0000001) return 9
-  return 10
+export function formatDisplayAmount(
+  amount: string | number,
+  {
+    decimals = 2,
+    useAbbreviations = true,
+    maxSignificantDigits = 6,
+  }: {
+    decimals?: number
+    useAbbreviations?: boolean
+    maxSignificantDigits?: number
+  } = {},
+): string {
+  const amountNumber = typeof amount === 'number' ? amount : Number(amount)
+  if (!Number.isFinite(amountNumber)) return String(amount)
+  if (!useAbbreviations) return amountNumber.toFixed(decimals)
+
+  return new Intl.NumberFormat('en-US', {
+    maximumSignificantDigits: maxSignificantDigits,
+    minimumFractionDigits: 0,
+    notation: Math.abs(amountNumber) >= 1_000_000 ? 'compact' : 'standard',
+    useGrouping: true,
+  }).format(amountNumber)
 }
 
 export function TokenAmount({
@@ -73,19 +85,15 @@ export function TokenAmount({
   decimals = 2,
   variant,
   useAbbreviations = true,
+  maxSignificantDigits = 6,
 }: TokenAmountProps) {
   const fontSize = { sm: '$3', md: '$5', lg: '$7', xl: '$9' } as const
 
-  const amountNumber = typeof amount === 'number' ? amount : parseFloat(amount)
-  const formatted = useAbbreviations
-    ? new Intl.NumberFormat('en-US', {
-        style: 'decimal',
-        minimumFractionDigits: 0,
-        maximumFractionDigits: getDecimals(amountNumber),
-        useGrouping: true,
-        notation: 'compact',
-      }).format(amountNumber)
-    : amountNumber.toFixed(decimals)
+  const formatted = formatDisplayAmount(amount, {
+    decimals,
+    useAbbreviations,
+    maxSignificantDigits,
+  })
 
   return (
     <TokenAmountFrame size={size}>
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 27a33dc..32225c5 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -86,7 +86,7 @@ export type { DialogConfig, DialogStatus } from './components/Dialog'
 
 // Web3
 export { AddressDisplay } from './components-test/AddressDisplay'
-export { TokenAmount } from './components/TokenAmount'
+export { TokenAmount, formatDisplayAmount } from './components/TokenAmount'
 export { TransactionButton } from './components-test/TransactionButton'
 export { ChainBadge } from './components-test/ChainBadge'
 export { WalletInfo } from './components-test/WalletInfo'
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index 4106488..2091b67 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -44,10 +44,13 @@ test('StreamingWidget shows the disconnected wallet gate', async ({ page }) => {
 test('StreamingWidget renders tab navigation and switches views', async ({ page }) => {
   await gotoStory(page, 'populated-state')
 
-  await expectBodyToContain(page, ['Streams', 'Pools', 'Balances', 'Active streams'])
+  await expectBodyToContain(page, ['Streams', 'History', 'Pools', 'Balances', 'Active streams'])
+
+  await page.getByText('History').first().click()
+  await expectBodyToContain(page, ['Stream history', 'Show more'])
 
   await page.getByText('Pools').first().click()
-  await expectBodyToContain(page, ['Claimable', 'Connect to claim'])
+  await expectBodyToContain(page, ['Claimable', 'Claim', 'Connect'])
 
   await page.getByText('Balances').first().click()
   await expectBodyToContain(page, ['Super Token Balance', 'SUP Reserve'])
@@ -69,25 +72,30 @@ test('StreamingWidget shows the unsupported network prompt', async ({ page }) =>
 test('StreamingWidget shows loading states for streams and history', async ({ page }) => {
   await gotoStory(page, 'loading-state')
 
-  await expectBodyToContain(page, ['Loading streams', 'Loading stream history'])
+  await expectBodyToContain(page, ['Loading streams'])
+
+  await page.getByText('History').first().click()
+  await expectBodyToContain(page, ['Loading stream history'])
   await saveScreenshot(page, 'sw-04-loading-state')
 })
 
 test('StreamingWidget shows empty states for streams and history', async ({ page }) => {
   await gotoStory(page, 'empty-state')
 
-  await expectBodyToContain(page, ['No streams found.', 'No stream history found.'])
+  await expectBodyToContain(page, ['No streams found.'])
+
+  await page.getByText('History').first().click()
+  await expectBodyToContain(page, ['No stream history found.'])
   await saveScreenshot(page, 'sw-05-empty-state')
 })
 
 test('StreamingWidget shows error states for streams and history', async ({ page }) => {
   await gotoStory(page, 'error-state')
 
-  await expectBodyToContain(page, [
-    'Unable to reach the network',
-    'Unable to load stream history.',
-    'Retry',
-  ])
+  await expectBodyToContain(page, ['Unable to reach the network', 'Retry'])
+
+  await page.getByText('History').first().click()
+  await expectBodyToContain(page, ['Unable to load stream history.', 'Retry'])
   await saveScreenshot(page, 'sw-06-error-state')
 })
 
@@ -96,10 +104,9 @@ test('StreamingWidget shows populated incoming and outgoing stream views', async
 
   await expectBodyToContain(page, [
     'Active streams',
-    'Stream history',
     'Incoming',
     'Outgoing',
-    'Show more',
+    /100\s+G\$\s*\/mo/,
   ])
 
   await page.getByText('Incoming').first().click()
@@ -109,17 +116,21 @@ test('StreamingWidget shows populated incoming and outgoing stream views', async
   await expectBodyToContain(page, ['Outgoing'])
 
   await saveScreenshot(page, 'sw-07-populated-streams')
+
+  await page.getByText('History').first().click()
+  await expectBodyToContain(page, ['Stream history', 'Show more'])
+  await saveScreenshot(page, 'sw-22-stream-history-tab')
 })
 
 test('StreamingWidget renders usable mobile and desktop layouts', async ({ page }) => {
   await page.setViewportSize({ width: 390, height: 844 })
   await gotoStory(page, 'populated-state')
-  await expectBodyToContain(page, ['Streams', 'Active streams', 'Stream history'])
+  await expectBodyToContain(page, ['Streams', 'History', 'Active streams'])
   await saveScreenshot(page, 'sw-18-mobile-populated')
 
   await page.setViewportSize({ width: 1280, height: 900 })
   await gotoStory(page, 'populated-state')
-  await expectBodyToContain(page, ['Streams', 'Pools', 'Balances', 'Active streams'])
+  await expectBodyToContain(page, ['Streams', 'History', 'Pools', 'Balances', 'Active streams'])
   await saveScreenshot(page, 'sw-19-desktop-populated')
 })
 
@@ -160,7 +171,7 @@ test('StreamingWidget shows pool claim amount and lifecycle states', async ({ pa
   await saveScreenshot(page, 'sw-13-pool-claim-pending')
 
   await gotoStory(page, 'pool-claim-success')
-  await expectBodyToContain(page, ['Connected', 'Done'])
+  await expectBodyToContain(page, ['Claimable', 'Done'])
   await saveScreenshot(page, 'sw-14-pool-claim-success')
 
   await gotoStory(page, 'pool-claim-error')
@@ -168,13 +179,25 @@ test('StreamingWidget shows pool claim amount and lifecycle states', async ({ pa
   await saveScreenshot(page, 'sw-15-pool-claim-error')
 
   await gotoStory(page, 'pool-claimable-amount-error')
-  await expectBodyToContain(page, ['Could not load claimable amount.', 'Tap to retry'])
+  await expectBodyToContain(page, ['Could not load claimable amount.', 'Retry'])
   await saveScreenshot(page, 'sw-20-pool-claimable-amount-error')
+
+  await page.getByText('Retry').first().click()
+  await expectBodyToContain(page, ['Loading pool memberships'])
+  await saveScreenshot(page, 'sw-21-pool-claimable-retry-loading')
 })
 
 test('StreamingWidget shows Base SUP reserve and disables reserve off Base', async ({ page }) => {
   await gotoStory(page, 'base-sup-balance-and-reserve')
-  await expectBodyToContain(page, ['Super Token Balance', 'SUP Reserve (Staked)', '95.25'])
+  await expectBodyToContain(page, [
+    'Super Token Balance',
+    'SUP Reserve',
+    '112.75',
+    'Reserve locker',
+    'Available',
+    'Staked',
+    'Open in Superfluid Explorer',
+  ])
   await saveScreenshot(page, 'sw-16-base-sup-reserve')
 
   await gotoStory(page, 'non-base-sup-reserve-disabled')
diff --git a/tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png b/tests/widgets/streaming-widget/test-results/sw-01-no-wallet.png
index e887a884bf60af507498c742fb3989984cfa6315..2160ecb3e99168a2a7c9162c3049707a94252fea 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-02-tab-navigation.png b/tests/widgets/streaming-widget/test-results/sw-02-tab-navigation.png
index 49e1b18f3123ee568a90284e66c3132f4d851f15..66eaf30840a89a6c2f5167e2ceaae2f8849d1a25 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-03-wrong-chain.png b/tests/widgets/streaming-widget/test-results/sw-03-wrong-chain.png
index aff96b40b3048fd81395e134af62574cdcf4985a..312c1b6188f260513cbbd2fc104900609af1be7b 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-04-loading-state.png b/tests/widgets/streaming-widget/test-results/sw-04-loading-state.png
index 3d5bf058dc5f5ac507b1b9a4708e7dbdb9c1053a..796aecda15a67e98a3a8566ebc7ff8b8c5b9c201 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-05-empty-state.png b/tests/widgets/streaming-widget/test-results/sw-05-empty-state.png
index 9c56a2b4381953566422ffe51c9ee1bb0861430e..86ef3001418515187b0f0b2aaa5623669a8e6453 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-06-error-state.png b/tests/widgets/streaming-widget/test-results/sw-06-error-state.png
index a18961ef69655ca438b94e734a89d49ac1cb5f67..b3ad0195b99293ceef979e910638d9570d737256 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png b/tests/widgets/streaming-widget/test-results/sw-07-populated-streams.png
index 04851afe73ecfb9b14db4f2a74dade4a3554303e..308ad6cfd0cd50542db9b69124cdd3bad8754cb1 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png b/tests/widgets/streaming-widget/test-results/sw-08-create-update-invalid.png
index 0cb733cbf9d27e1357ac851e1080fea9eb5f77c5..3b91e4d3cb51d5eba280d42da64ae65a27177e5c 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png b/tests/widgets/streaming-widget/test-results/sw-09-create-update-pending.png
index 684c86a781cd7db7708645bc55b2961cea76a194..3e18f6df60b3f5f17afce46b2c21c67c4ae8fe08 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png b/tests/widgets/streaming-widget/test-results/sw-10-create-update-success.png
index 798c917d92397fae6ef76b591e9a7a799c6e7ef5..68430249998050eff9b06da2c88196db7b79549d 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png b/tests/widgets/streaming-widget/test-results/sw-11-create-update-failure.png
index 765021ea4ef6f95e64d22332afeba1a7d84570b4..7d340bb71f24b97b923ddfb4efc83008df7d9db7 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png b/tests/widgets/streaming-widget/test-results/sw-12-pool-claim.png
index 958da9b26e070a5090c11fe777d1dd43b959b2a2..f77055ab53072c02505ab8f2136a2450724ae564 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png b/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
index ece39c70269586fb0e8a4e457cc157ea1e6f9ee1..965bc8c3c9dbb0774d8b1bde936cd4bb47bf1708 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png b/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png
index 54feed970be4559ce7d8f5cb9aee8881b0e13c8d..1bf06d97abbe3190168dfa36cad4709105b866a6 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png b/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png
index 792013620259e19172adaff368d03cc8075f77a5..c47f044a79a94cdfc5f3ed4a70c8452125e45091 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-16-base-sup-reserve.png b/tests/widgets/streaming-widget/test-results/sw-16-base-sup-reserve.png
index 7e8f8ce4d19662704b9e29c161f21d93df98a6dc..bb85df8794da5f8109ba340843234f019be40f95 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-17-non-base-reserve-disabled.png b/tests/widgets/streaming-widget/test-results/sw-17-non-base-reserve-disabled.png
index 49e1b18f3123ee568a90284e66c3132f4d851f15..66eaf30840a89a6c2f5167e2ceaae2f8849d1a25 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-18-mobile-populated.png b/tests/widgets/streaming-widget/test-results/sw-18-mobile-populated.png
index 4f8f982e02d75367258ade65822752d11ee89fa4..742cc15a04b66c513c3d6eb89762edc0d0ed8a16 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-19-desktop-populated.png b/tests/widgets/streaming-widget/test-results/sw-19-desktop-populated.png
index 981acdc8f713dfaaf506614610e8d4e58cf0de4b..0ea1111ce598a43f08d308c8a4f6e30c8b703408 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-20-pool-claimable-amount-error.png b/tests/widgets/streaming-widget/test-results/sw-20-pool-claimable-amount-error.png
index 63696e17470030300965bc475c08d90a00707dbc..a2c2fb8d767149375e2ff4eb3ff4c991e5a68649 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-21-pool-claimable-retry-loading.png b/tests/widgets/streaming-widget/test-results/sw-21-pool-claimable-retry-loading.png
new file mode 100644
index 0000000000000000000000000000000000000000..5088ac4c6c5adf0847c0a3c0a126fde3d5504b56
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-22-stream-history-tab.png b/tests/widgets/streaming-widget/test-results/sw-22-stream-history-tab.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4a40fc24508028b9601ba456158a8edab22d584
GIT binary patch

From 0a1214bbf360a86b7f421d7f2fa6ddfc1cac0ad8 Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Fri, 29 May 2026 13:08:55 +0100
Subject: [PATCH 5/7] fix: clarify pool claim lifecycle stories

---
 .../StreamingWidget.stories.tsx               |  14 ++++++++++++++
 .../streaming-widget/src/StreamingWidget.tsx  |   7 ++++++-
 tests/widgets/streaming-widget/states.spec.ts |   8 +++++++-
 .../test-results/sw-13-pool-claim-pending.png | Bin 23415 -> 24419 bytes
 .../test-results/sw-23-pool-connected.png     | Bin 0 -> 22487 bytes
 5 files changed, 27 insertions(+), 2 deletions(-)
 create mode 100644 tests/widgets/streaming-widget/test-results/sw-23-pool-connected.png

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 29be258..fbe801c 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -498,6 +498,20 @@ export const PoolClaimState: Story = {
   ),
 }
 
+export const PoolConnectedState: Story = {
+  render: () => (
+    <PreviewStoryShell
+      adapter={createAdapter({
+        pools: [{ ...samplePools[0], isConnected: true }],
+      })}
+      dataTestId="StreamingWidget-pool-connected"
+      initialTab="pools"
+    />
+  ),
+}
+
+// Claim lifecycle stories intentionally use a disconnected, claimable pool. Connected
+// memberships should only expose Disconnect; disconnected pools expose claim lifecycle states.
 export const PoolClaimPending: Story = {
   render: () => (
     <PreviewStoryShell
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index ada5763..b5d806c 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -155,7 +155,12 @@ function superfluidExplorerAccountUrl(chainId: number | null, address: Address):
 // ---------------------------------------------------------------------------
 function WriteStatusBadge({ status }: { status: WriteStatus }) {
   if (status === 'idle') return null
-  if (status === 'pending') return <Spinner size="sm" />
+  if (status === 'pending')
+    return (
+      <Badge type="warning">
+        <BadgeText>Pending</BadgeText>
+      </Badge>
+    )
   if (status === 'success')
     return (
       <Badge type="success">
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index 2091b67..aa84c7d 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -166,8 +166,14 @@ test('StreamingWidget shows pool claim amount and lifecycle states', async ({ pa
   await expectBodyToContain(page, ['Claimable', '12.5', 'Claim'])
   await saveScreenshot(page, 'sw-12-pool-claim')
 
+  await gotoStory(page, 'pool-connected-state')
+  await expectBodyToContain(page, ['Connected', 'Claimable', 'Disconnect'])
+  await expect(page.getByText('Claim', { exact: true })).toHaveCount(0)
+  await expect(page.getByText('Connect', { exact: true })).toHaveCount(0)
+  await saveScreenshot(page, 'sw-23-pool-connected')
+
   await gotoStory(page, 'pool-claim-pending')
-  await expectBodyToContain(page, ['Claimable', '12.5'])
+  await expectBodyToContain(page, ['Claimable', '12.5', 'Pending'])
   await saveScreenshot(page, 'sw-13-pool-claim-pending')
 
   await gotoStory(page, 'pool-claim-success')
diff --git a/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png b/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
index 965bc8c3c9dbb0774d8b1bde936cd4bb47bf1708..4651d3be628106b6bc68c25606badda5ec3ee740 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-23-pool-connected.png b/tests/widgets/streaming-widget/test-results/sw-23-pool-connected.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ac8124ff11f664ceb5f5dac568abd9ec5f94bb2
GIT binary patch

From d7ec12daa9a2fe80ae072820be7b8e934732ab3a Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Fri, 29 May 2026 13:52:08 +0100
Subject: [PATCH 6/7] fix: restore connected pool lifecycle stories

---
 .../StreamingWidget.stories.tsx               |  10 +++----
 .../streaming-widget/src/StreamingWidget.tsx  |  27 ++++++++++--------
 tests/widgets/streaming-widget/states.spec.ts |  17 +++++++++--
 .../test-results/sw-13-pool-claim-pending.png | Bin 24419 -> 23696 bytes
 .../test-results/sw-14-pool-claim-success.png | Bin 24463 -> 23448 bytes
 .../test-results/sw-15-pool-claim-error.png   | Bin 25796 -> 24731 bytes
 6 files changed, 34 insertions(+), 20 deletions(-)

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index fbe801c..6f94ca1 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -510,13 +510,13 @@ export const PoolConnectedState: Story = {
   ),
 }
 
-// Claim lifecycle stories intentionally use a disconnected, claimable pool. Connected
-// memberships should only expose Disconnect; disconnected pools expose claim lifecycle states.
+// Claim lifecycle stories stay connected to match the review fixture contract while
+// keeping connected memberships limited to the Disconnect action.
 export const PoolClaimPending: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: false }],
+        pools: [{ ...samplePools[0], isConnected: true }],
         poolClaimStatus: { [DEMO_POOL]: 'pending' },
       })}
       dataTestId="StreamingWidget-pool-claim-pending"
@@ -529,7 +529,7 @@ export const PoolClaimSuccess: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: false }],
+        pools: [{ ...samplePools[0], isConnected: true }],
         poolClaimStatus: { [DEMO_POOL]: 'success' },
       })}
       dataTestId="StreamingWidget-pool-claim-success"
@@ -542,7 +542,7 @@ export const PoolClaimError: Story = {
   render: () => (
     <PreviewStoryShell
       adapter={createAdapter({
-        pools: [{ ...samplePools[0], isConnected: false }],
+        pools: [{ ...samplePools[0], isConnected: true }],
         poolClaimStatus: { [DEMO_POOL]: 'error' },
         poolClaimError: { [DEMO_POOL]: 'Pool claim failed. Please retry.' },
       })}
diff --git a/packages/streaming-widget/src/StreamingWidget.tsx b/packages/streaming-widget/src/StreamingWidget.tsx
index b5d806c..8439633 100644
--- a/packages/streaming-widget/src/StreamingWidget.tsx
+++ b/packages/streaming-widget/src/StreamingWidget.tsx
@@ -630,18 +630,21 @@ function PoolCard({
 
       <XStack gap="$2" alignItems="center" flexWrap="wrap">
         {pool.isConnected ? (
-          <Button
-            variant="secondary"
-            disabled={isConnectPending}
-            borderColor="$error"
-            onPress={() => onDisconnect(pool.poolId)}
-          >
-            {isConnectPending ? (
-              <Spinner size="sm" />
-            ) : (
-              <ButtonText color="$error">Disconnect</ButtonText>
-            )}
-          </Button>
+          <>
+            <WriteStatusBadge status={claimStatus} />
+            <Button
+              variant="secondary"
+              disabled={isConnectPending}
+              borderColor="$error"
+              onPress={() => onDisconnect(pool.poolId)}
+            >
+              {isConnectPending ? (
+                <Spinner size="sm" />
+              ) : (
+                <ButtonText color="$error">Disconnect</ButtonText>
+              )}
+            </Button>
+          </>
         ) : (
           <>
             <WriteStatusBadge status={claimStatus} />
diff --git a/tests/widgets/streaming-widget/states.spec.ts b/tests/widgets/streaming-widget/states.spec.ts
index aa84c7d..c22fed2 100644
--- a/tests/widgets/streaming-widget/states.spec.ts
+++ b/tests/widgets/streaming-widget/states.spec.ts
@@ -173,15 +173,26 @@ test('StreamingWidget shows pool claim amount and lifecycle states', async ({ pa
   await saveScreenshot(page, 'sw-23-pool-connected')
 
   await gotoStory(page, 'pool-claim-pending')
-  await expectBodyToContain(page, ['Claimable', '12.5', 'Pending'])
+  await expectBodyToContain(page, ['Connected', 'Claimable', '12.5', 'Pending', 'Disconnect'])
+  await expect(page.getByText('Claim', { exact: true })).toHaveCount(0)
+  await expect(page.getByText('Connect', { exact: true })).toHaveCount(0)
   await saveScreenshot(page, 'sw-13-pool-claim-pending')
 
   await gotoStory(page, 'pool-claim-success')
-  await expectBodyToContain(page, ['Claimable', 'Done'])
+  await expectBodyToContain(page, ['Connected', 'Claimable', 'Done', 'Disconnect'])
+  await expect(page.getByText('Claim', { exact: true })).toHaveCount(0)
+  await expect(page.getByText('Connect', { exact: true })).toHaveCount(0)
   await saveScreenshot(page, 'sw-14-pool-claim-success')
 
   await gotoStory(page, 'pool-claim-error')
-  await expectBodyToContain(page, ['Pool claim failed. Please retry.', 'Failed'])
+  await expectBodyToContain(page, [
+    'Connected',
+    'Pool claim failed. Please retry.',
+    'Failed',
+    'Disconnect',
+  ])
+  await expect(page.getByText('Claim', { exact: true })).toHaveCount(0)
+  await expect(page.getByText('Connect', { exact: true })).toHaveCount(0)
   await saveScreenshot(page, 'sw-15-pool-claim-error')
 
   await gotoStory(page, 'pool-claimable-amount-error')
diff --git a/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png b/tests/widgets/streaming-widget/test-results/sw-13-pool-claim-pending.png
index 4651d3be628106b6bc68c25606badda5ec3ee740..e9fb1a778b768c8bd98e73382b08b0bd186a75d9 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png b/tests/widgets/streaming-widget/test-results/sw-14-pool-claim-success.png
index 1bf06d97abbe3190168dfa36cad4709105b866a6..be9d03006221cb1be5dff081e4e172e6c7218073 100644
GIT binary patch
diff --git a/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png b/tests/widgets/streaming-widget/test-results/sw-15-pool-claim-error.png
index c47f044a79a94cdfc5f3ed4a70c8452125e45091..c75842de544d03d0fba6bdffb7e752307beef87a 100644
GIT binary patch

From ec3aa937c058981d06dde771b4cb871de4991f34 Mon Sep 17 00:00:00 2001
From: Victor Edeh <108542677+edehvictor@users.noreply.github.com>
Date: Sat, 30 May 2026 13:35:43 +0100
Subject: [PATCH 7/7] docs: clarify pool lifecycle story comment

---
 .../src/stories/streaming-widget/StreamingWidget.stories.tsx   | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
index 6f94ca1..04aff7f 100644
--- a/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
+++ b/examples/storybook/src/stories/streaming-widget/StreamingWidget.stories.tsx
@@ -510,8 +510,7 @@ export const PoolConnectedState: Story = {
   ),
 }
 
-// Claim lifecycle stories stay connected to match the review fixture contract while
-// keeping connected memberships limited to the Disconnect action.
+// Claim lifecycle stories use isConnected: true so write status badges render correctly.
 export const PoolClaimPending: Story = {
   render: () => (
     <PreviewStoryShell
