You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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
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.
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.)
packages/core — no change; disconnectOverride/disconnect already fully implemented in provider.tsx/types.ts.
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).
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).
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.
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).
[DRAFT][PLAN] AI Credits widget: Disconnect button (standalone only)
Plan for #158.
Reference files mapped (existing code to build on)
packages/ai-credits-widget/src/AiCreditsWidget.tsx<WidgetTabs>then the active tab panel inside aYStack. Insertion point for an always-visible wallet header, placed above<WidgetTabs>so it's visible across all 3 tabs (Buy/Manage/History) — mirrors Superfluid'sCampaignHeader, which is visible regardless of active view. Component signature (~L422-448) destructuresconnectOverrideand passes it toGoodWidgetProvider, but does not currently destructure or passdisconnectOverride— needs adding as a sibling in both places.packages/ai-credits-widget/src/widgetRuntimeContract.tsAiCreditsWidgetProps(~L133-151) hasconnectOverride?: () => Promise<void>(L135) but nodisconnectOverride— add as a sibling. Actions interface hasconnect: () => Promise<void>(L64) but nodisconnect— add as a sibling. State already exposes `address: stringpackages/ai-credits-widget/src/adapter.tsuseWallet()destructuring (L369) currently omitsdisconnect— needs adding.handleConnect(~L601-616) is the exact pattern to mirror forhandleDisconnect: auseCallbackthat calls the underlying context method and lets state update via existing effects/listeners, no extra bookkeeping. Actions object + itsuseMemodependency array (~L1612-1645) wireconnect: handleConnectand listhandleConnectas a dep — adddisconnect: handleDisconnectandhandleDisconnectas siblings in both places.packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsxpackages/superfluid-campaign-widget/src/components/shared/CampaignHeader.tsxisConnected ? <WalletChip address={address} onDisconnect={onDisconnect} /> : <ConnectWalletPrompt ... />(~L62-63), rendered in an always-visible header row.packages/superfluid-campaign-widget/src/widgetRuntimeContract.tsdisconnectOverride?: () => Promise<void>(L196) with doc comment/** Integrator-owned wallet disconnect flow. */— naming/shape precedent to match exactly inai-credits-widget.packages/core/src/provider.tsxGoodWidgetProvideralready acceptsdisconnectOverride(~L60-69) and exposes adisconnectcallback viauseWallet()(~L141-143, L156-157) that just callsdisconnectOverride?.(). No core package change needed — this plan is purely about threading the existing core capability throughai-credits-widget, which never opted in.packages/core/src/types.tsGoodWidgetProviderProps(~L38-48) already declares bothconnectOverrideanddisconnectOverride. Confirms core is ready; only the widget-level plumbing is missing.tests/widgets/ai-credits-widget/states.spec.tstest-results/). Extend with the new header states.Existing
@goodwidgetpackages to import@goodwidget/core— already a dependency; no new import needed,useWallet()'sdisconnectis already exposed oncedisconnectOverrideis 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
WalletChipcurrently lives only insidepackages/superfluid-campaign-widget/src/components/shared/, private to that widget. This issue needs the same component inai-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.WalletChipintopackages/ui(e.g.packages/ui/src/components/WalletChip.tsx), preserving its props and internal behavior byte-for-byte, including the message-gated fallback whenonDisconnectis absent — that fallback is correct for any shared component and must not be forked or altered.ai-credits-widgetimports it from@goodwidget/uiand only renders it whenBoolean(disconnectOverride)is true (the deviation called for in the issue) — this gating happens at the call site, not insideWalletChipitself, so Superfluid's own always-show-with-message behavior is untouched.superfluid-campaign-widget's existing imports (CampaignHeader.tsx,LeaderboardView.tsx) to the relocated@goodwidget/uiversion 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 toai-credits-widgetonly. If left undone,packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsxbecomes a duplicate; that duplication is an accepted, explicitly-flagged tradeoff of staying in scope, not an oversight.Required states, flows, and behaviors
disconnectOverrideprovided) + wallet connected → wallet-address chip with a disconnect option is visible in a header row, above<WidgetTabs>, on all three tabs (Buy/Manage/History).disconnectOverridenot provided) + wallet connected → no chip, no disconnect affordance, no fallback message — the header row renders nothing wallet-related, since GoodWallet owns the session.state.status === 'disconnected' | 'connecting') → unchanged;DisconnectedPanelstill renders as today, header is not applicable (no address to show).actions.disconnect()→disconnectOverride()→ integrator's flow runs → widget's own connection state updates via the same listeners/effects that already drivestate.statuson account/provider changes (no new bespoke state needed, mirrors howhandleConnectlets existing effects pick up the resulting state).superfluid-campaign-widget's ownWalletChipusage — same message-gated fallback, same props, same visual output, whether or not it's later migrated to the relocated@goodwidget/uicopy.Execution plan
packages/ui— createpackages/ui/src/components/WalletChip.tsxby moving (not copying-and-diverging) the implementation frompackages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx, unchanged. Export it frompackages/ui's public entry point alongsideButton/Dialog/etc.packages/superfluid-campaign-widget— updateCampaignHeader.tsxandLeaderboardView.tsxto importWalletChipfrom@goodwidget/uiinstead of the localshared/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.)packages/core— no change;disconnectOverride/disconnectalready fully implemented inprovider.tsx/types.ts.widgetRuntimeContract.ts— adddisconnectOverride?: () => Promise<void>toAiCreditsWidgetPropsas a sibling ofconnectOverride(~L135), anddisconnect: () => Promise<void>to the actions interface as a sibling ofconnect(~L64).adapter.ts— destructuredisconnectfromuseWallet()alongsideconnect(~L369). AddhandleDisconnect, mirroringhandleConnect'suseCallbackshape (~L601-616) at minimum viable complexity: calldisconnect(), no extra state juggling since existing listeners already drivestate.statusoff provider/account changes. Adddisconnect: handleDisconnectto the actions object andhandleDisconnectto itsuseMemodependency array (~L1612-1645).AiCreditsWidget.tsx— destructuredisconnectOverridein the component signature and pass it toGoodWidgetProvideras a sibling ofconnectOverride(~L422-448). In the connected-view branch (~L395), add a header row above<WidgetTabs>that renders<WalletChip address={state.address} onDisconnect={actions.disconnect} />only whenBoolean(disconnectOverride)is true; render nothing in that slot otherwise.tests/widgets/ai-credits-widget/states.spec.tswith states for: chip visible + disconnect works (standalone), chip absent entirely (embedded, nodisconnectOverride), chip visible across all three tabs. Commit new baseline screenshots undertests/widgets/ai-credits-widget/test-results/per repo convention. Also re-runtests/widgets/superfluid-campaign-widget/states.spec.tsafter step 2's import-path change to confirm no visual regression from the move.Acceptance criteria
disconnectOverrideprop exists onAiCreditsWidgetProps, threaded through toGoodWidgetProvider, matching the existingconnectOverridepattern exactly.disconnectOverrideis provided and the wallet is connected, a wallet-address chip with a working Disconnect action is visible on all three tabs (Buy/Manage/History).disconnectOverrideis not provided (embedded mode), no chip and no fallback message render — the button is hidden entirely, not message-gated.WalletChipis relocated topackages/uiand bothai-credits-widgetandsuperfluid-campaign-widgetimport the same shared implementation; no duplicate component remains.tests/widgets/ai-credits-widget/states.spec.tscovers the new header states with committed baseline screenshots, and the existing Superfluid spec still passes after the import-path change.Human-reviewer checklist
<WidgetTabs>, visible on all tabs) matches product expectations — the issue didn't include a mockup, so this placement is inferred from theCampaignHeaderprecedent, not specified verbatim.WalletChipintopackages/ui(rather than leaving Superfluid's copy untouched and just duplicating it intoai-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 tosuperfluid-campaign-widgetfiles in this PR.superfluid-campaign-widgetPlaywright baselines after the import-path change in step 2.state.addressis the correct/only source for the chip's displayed address inai-credits-widget(no separate buyer-vs-connected-wallet distinction that WalletChip's singleaddressprop doesn't already handle in Superfluid).