Skip to content

[DRAFT][PLAN] AI Credits widget: Disconnect button (standalone only) #159

Description

@goodbounties-nanoclaw-agent

[DRAFT][PLAN] AI Credits widget: Disconnect button (standalone only)

Plan for #158.

Reference files mapped (existing code to build on)

File Role
packages/ai-credits-widget/src/AiCreditsWidget.tsx Connected-view branch (~L395-419) renders <WidgetTabs> then the active tab panel inside a YStack. Insertion point for an always-visible wallet header, placed above <WidgetTabs> so it's visible across all 3 tabs (Buy/Manage/History) — mirrors Superfluid's CampaignHeader, which is visible regardless of active view. Component signature (~L422-448) destructures connectOverride and passes it to GoodWidgetProvider, but does not currently destructure or pass disconnectOverride — needs adding as a sibling in both places.
packages/ai-credits-widget/src/widgetRuntimeContract.ts AiCreditsWidgetProps (~L133-151) has connectOverride?: () => Promise<void> (L135) but no disconnectOverride — add as a sibling. Actions interface has connect: () => Promise<void> (L64) but no disconnect — add as a sibling. State already exposes `address: string
packages/ai-credits-widget/src/adapter.ts useWallet() destructuring (L369) currently omits disconnect — needs adding. handleConnect (~L601-616) is the exact pattern to mirror for handleDisconnect: a useCallback that calls the underlying context method and lets state update via existing effects/listeners, no extra bookkeeping. Actions object + its useMemo dependency array (~L1612-1645) wire connect: handleConnect and list handleConnect as a dep — add disconnect: handleDisconnect and handleDisconnect as siblings in both places.
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx The component to reuse. Props: `{ address: string
packages/superfluid-campaign-widget/src/components/shared/CampaignHeader.tsx Integration pattern to mirror: isConnected ? <WalletChip address={address} onDisconnect={onDisconnect} /> : <ConnectWalletPrompt ... /> (~L62-63), rendered in an always-visible header row.
packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts disconnectOverride?: () => Promise<void> (L196) with doc comment /** Integrator-owned wallet disconnect flow. */ — naming/shape precedent to match exactly in ai-credits-widget.
packages/core/src/provider.tsx GoodWidgetProvider already accepts disconnectOverride (~L60-69) and exposes a disconnect callback via useWallet() (~L141-143, L156-157) that just calls disconnectOverride?.(). No core package change needed — this plan is purely about threading the existing core capability through ai-credits-widget, which never opted in.
packages/core/src/types.ts GoodWidgetProviderProps (~L38-48) already declares both connectOverride and disconnectOverride. Confirms core is ready; only the widget-level plumbing is missing.
tests/widgets/ai-credits-widget/states.spec.ts Existing Playwright spec (repo convention: every GoodWidget task needs this spec + committed baselines under test-results/). Extend with the new header states.

Existing @goodwidget packages to import

  • @goodwidget/core — already a dependency; no new import needed, useWallet()'s disconnect is already exposed once disconnectOverride is threaded through.
  • @goodwidget/ui — no current wallet-chip primitive lives here; see "New components assessed" below for why one should move here.

New components assessed

WalletChip currently lives only inside packages/superfluid-campaign-widget/src/components/shared/, private to that widget. This issue needs the same component in ai-credits-widget. Per the org convention (assess widget-local vs. packages/ui): since a second, independent widget package now needs this exact UI (address chip + disconnect dropdown), it qualifies as reusable rather than widget-specific.

  • Move WalletChip into packages/ui (e.g. packages/ui/src/components/WalletChip.tsx), preserving its props and internal behavior byte-for-byte, including the message-gated fallback when onDisconnect is absent — that fallback is correct for any shared component and must not be forked or altered.
  • ai-credits-widget imports it from @goodwidget/ui and only renders it when Boolean(disconnectOverride) is true (the deviation called for in the issue) — this gating happens at the call site, not inside WalletChip itself, so Superfluid's own always-show-with-message behavior is untouched.
  • Migrating superfluid-campaign-widget's existing imports (CampaignHeader.tsx, LeaderboardView.tsx) to the relocated @goodwidget/ui version is a natural follow-up but is out of scope for this issue — flagged in the human-reviewer checklist rather than done unprompted, since issue [Feature] AI Credits widget: Disconnect button (standalone only) #158 is explicitly scoped to ai-credits-widget only. If left undone, packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx becomes a duplicate; that duplication is an accepted, explicitly-flagged tradeoff of staying in scope, not an oversight.

Required states, flows, and behaviors

  • Standalone app (disconnectOverride provided) + wallet connected → wallet-address chip with a disconnect option is visible in a header row, above <WidgetTabs>, on all three tabs (Buy/Manage/History).
  • Embedded in GoodWallet (disconnectOverride not provided) + wallet connected → no chip, no disconnect affordance, no fallback message — the header row renders nothing wallet-related, since GoodWallet owns the session.
  • Wallet disconnected (state.status === 'disconnected' | 'connecting') → unchanged; DisconnectedPanel still renders as today, header is not applicable (no address to show).
  • Clicking Disconnect in the chip's dropdown → calls actions.disconnect()disconnectOverride() → integrator's flow runs → widget's own connection state updates via the same listeners/effects that already drive state.status on account/provider changes (no new bespoke state needed, mirrors how handleConnect lets existing effects pick up the resulting state).
  • No behavior change to superfluid-campaign-widget's own WalletChip usage — same message-gated fallback, same props, same visual output, whether or not it's later migrated to the relocated @goodwidget/ui copy.

Execution plan

  1. packages/ui — create packages/ui/src/components/WalletChip.tsx by moving (not copying-and-diverging) the implementation from packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx, unchanged. Export it from packages/ui's public entry point alongside Button/Dialog/etc.
  2. packages/superfluid-campaign-widget — update CampaignHeader.tsx and LeaderboardView.tsx to import WalletChip from @goodwidget/ui instead of the local shared/ copy, then delete the now-unused local file. (This keeps Superfluid on exactly one implementation rather than leaving a stale fork — the "don't design a new mechanism" direction is about not inventing new disconnect behavior, not about tolerating duplicate copies of the moved component.)
  3. packages/core — no change; disconnectOverride/disconnect already fully implemented in provider.tsx/types.ts.
  4. widgetRuntimeContract.ts — add disconnectOverride?: () => Promise<void> to AiCreditsWidgetProps as a sibling of connectOverride (~L135), and disconnect: () => Promise<void> to the actions interface as a sibling of connect (~L64).
  5. adapter.ts — destructure disconnect from useWallet() alongside connect (~L369). Add handleDisconnect, mirroring handleConnect's useCallback shape (~L601-616) at minimum viable complexity: call disconnect(), no extra state juggling since existing listeners already drive state.status off provider/account changes. Add disconnect: handleDisconnect to the actions object and handleDisconnect to its useMemo dependency array (~L1612-1645).
  6. AiCreditsWidget.tsx — destructure disconnectOverride in the component signature and pass it to GoodWidgetProvider as a sibling of connectOverride (~L422-448). In the connected-view branch (~L395), add a header row above <WidgetTabs> that renders <WalletChip address={state.address} onDisconnect={actions.disconnect} /> only when Boolean(disconnectOverride) is true; render nothing in that slot otherwise.
  7. Tests — extend tests/widgets/ai-credits-widget/states.spec.ts with states for: chip visible + disconnect works (standalone), chip absent entirely (embedded, no disconnectOverride), chip visible across all three tabs. Commit new baseline screenshots under tests/widgets/ai-credits-widget/test-results/ per repo convention. Also re-run tests/widgets/superfluid-campaign-widget/states.spec.ts after step 2's import-path change to confirm no visual regression from the move.

Acceptance criteria

  • disconnectOverride prop exists on AiCreditsWidgetProps, threaded through to GoodWidgetProvider, matching the existing connectOverride pattern exactly.
  • When disconnectOverride is provided and the wallet is connected, a wallet-address chip with a working Disconnect action is visible on all three tabs (Buy/Manage/History).
  • When disconnectOverride is not provided (embedded mode), no chip and no fallback message render — the button is hidden entirely, not message-gated.
  • WalletChip is relocated to packages/ui and both ai-credits-widget and superfluid-campaign-widget import the same shared implementation; no duplicate component remains.
  • Superfluid's own disconnect UI/behavior is visually and functionally unchanged after the move.
  • tests/widgets/ai-credits-widget/states.spec.ts covers the new header states with committed baseline screenshots, and the existing Superfluid spec still passes after the import-path change.

Human-reviewer checklist

  • Confirm the header-row placement (above <WidgetTabs>, visible on all tabs) matches product expectations — the issue didn't include a mockup, so this placement is inferred from the CampaignHeader precedent, not specified verbatim.
  • Confirm moving WalletChip into packages/ui (rather than leaving Superfluid's copy untouched and just duplicating it into ai-credits-widget) is the intended tradeoff — this plan treats "mirror the pattern" as mirroring the component, not forking it, but flag if the Bounty Lead wants zero changes to superfluid-campaign-widget files in this PR.
  • Verify no visual regression in superfluid-campaign-widget Playwright baselines after the import-path change in step 2.
  • Confirm state.address is the correct/only source for the chip's displayed address in ai-credits-widget (no separate buyer-vs-connected-wallet distinction that WalletChip's single address prop doesn't already handle in Superfluid).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    Prepare AI Task

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions