From bdbafbfcf40cb64a95f20d1045f576cb4f91699b Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:24 +0000 Subject: [PATCH 01/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../src/fixtures/governanceInteractiveMock.ts | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 examples/storybook/src/fixtures/governanceInteractiveMock.ts diff --git a/examples/storybook/src/fixtures/governanceInteractiveMock.ts b/examples/storybook/src/fixtures/governanceInteractiveMock.ts new file mode 100644 index 00000000..0084fb8b --- /dev/null +++ b/examples/storybook/src/fixtures/governanceInteractiveMock.ts @@ -0,0 +1,252 @@ +import { decodeAbiParameters, decodeFunctionData, parseAbi, type Address, type Hex } from 'viem' +import type { EIP1193Provider } from '@goodwidget/core' +import { + encodeMockGovernanceRead, + MOCK_ALIGNMENT, + MOCK_CITIZEN, + MOCK_G_TOKEN, + MOCK_GOOD_ID, + MOCK_HOUSES, +} from './governanceRuntimeMock' + +// Minimal write-side ABI fragments, mirroring packages/governance-widget/src/sdks/contracts.ts. +// Duplicated locally (same convention as the read ABI in governanceRuntimeMock.ts) so this +// browser fixture has no dependency on package internals. +const G_TOKEN_WRITE_ABI = parseAbi([ + 'function transferAndCall(address to, uint256 value, bytes data) returns (bool)', +]) +const HOUSES_WRITE_ABI = parseAbi([ + 'function castVote(address[] recipients, uint256[] allocations)', + 'function unstake()', +]) +const REGISTRATION_DATA_TYPES = [ + { type: 'uint8' }, + { type: 'string' }, + { type: 'string' }, + { type: 'string' }, + { type: 'string' }, + { type: 'string' }, +] as const + +const MOCK_GOVERNANCE_RPC_PATH = '/mock-governance-rpc' +const MOCK_SUPERFLUID_URL_FRAGMENT = 'celo-mainnet/protocol-v1' +const MOCK_ACCOUNT: Address = '0x1234123412341234123412341234123412341234' +const MOCK_NOW_SECONDS = 1_784_419_200 + +type PendingTransactionEffect = + | { kind: 'registration'; house: 0 | 1 } + | { kind: 'vote' } + | { kind: 'unstake' } + +interface InteractiveGovernanceSession { + memberStatus: 0 | 1 | 2 | 3 | 4 + memberHouse: 0 | 1 + hasVoted: boolean +} + +function buildMockReceipt(status: 'success' | 'reverted', hash: Hex) { + return { + blockHash: `0x${'b'.repeat(64)}`, + blockNumber: '0x10', + contractAddress: null, + cumulativeGasUsed: '0x5208', + effectiveGasPrice: '0x1', + from: MOCK_ACCOUNT, + gasUsed: '0x5208', + logs: [], + logsBloom: `0x${'0'.repeat(512)}`, + status: status === 'reverted' ? '0x0' : '0x1', + to: MOCK_HOUSES, + transactionHash: hash, + transactionIndex: '0x0', + type: '0x2', + } +} + +function buildMockBlock() { + return { + baseFeePerGas: '0x0', + difficulty: '0x0', + extraData: '0x', + gasLimit: '0x1c9c380', + gasUsed: '0x0', + hash: `0x${'b'.repeat(64)}`, + logsBloom: `0x${'0'.repeat(512)}`, + miner: MOCK_HOUSES, + mixHash: `0x${'c'.repeat(64)}`, + nonce: '0x0000000000000000', + number: '0x10', + parentHash: `0x${'d'.repeat(64)}`, + receiptsRoot: `0x${'e'.repeat(64)}`, + sha3Uncles: `0x${'f'.repeat(64)}`, + size: '0x0', + stateRoot: `0x${'1'.repeat(64)}`, + timestamp: `0x${MOCK_NOW_SECONDS.toString(16)}`, + totalDifficulty: '0x0', + transactions: [], + transactionsRoot: `0x${'2'.repeat(64)}`, + uncles: [], + } +} + +function buildMockFundingStreams() { + return { + data: { + streams: [ + { + sender: { id: MOCK_CITIZEN.toLowerCase() }, + currentFlowRate: '0', + streamedUntilUpdatedAt: '300000000000000000000', + updatedAtTimestamp: String(MOCK_NOW_SECONDS), + }, + { + sender: { id: MOCK_ALIGNMENT.toLowerCase() }, + currentFlowRate: '1', + streamedUntilUpdatedAt: '150000000000000000000', + updatedAtTimestamp: String(MOCK_NOW_SECONDS), + }, + ], + }, + } +} + +function requestUrl(input: RequestInfo | URL): string { + if (typeof input === 'string') return input + if (input instanceof URL) return input.href + return input.url +} + +export interface InteractiveGovernanceEnvironment { + provider: EIP1193Provider + celoRpcUrl: string + addresses: { housesAddress: Address; goodIdAddress: Address; gTokenAddress: Address } + teardown: () => void +} + +/** + * Wires a self-contained, browser-native mocked Celo RPC and Superfluid + * subgraph behind a `window.fetch` override, paired with a matching mock + * EIP-1193 wallet. This lets a human open the story directly in Storybook + * and drive the real `useGovernanceAdapter` runtime end-to-end (onboarding -> + * vote -> unstake) without a live contract or Playwright's `page.route` + * network interception, which only runs under automation. + */ +export function createInteractiveGovernanceEnvironment(): InteractiveGovernanceEnvironment { + const session: InteractiveGovernanceSession = { memberStatus: 0, memberHouse: 0, hasVoted: false } + const pendingEffectsByHash = new Map() + const listeners: Record void>> = {} + const originalFetch = window.fetch.bind(window) + let transactionCounter = 0 + + const nextTransactionHash = (): Hex => { + transactionCounter += 1 + return `0x${transactionCounter.toString(16).padStart(64, '0')}` as Hex + } + + const applyReceiptEffect = (effect: PendingTransactionEffect) => { + if (effect.kind === 'registration') { + session.memberStatus = 2 + session.memberHouse = effect.house + } else if (effect.kind === 'vote') { + session.hasVoted = true + } else if (effect.kind === 'unstake') { + session.memberStatus = 4 + } + } + + const provider = { + async request({ method, params }: { method: string; params?: unknown }) { + switch (method) { + case 'eth_requestAccounts': + case 'eth_accounts': + return [MOCK_ACCOUNT] + case 'eth_chainId': + return '0xa4ec' + case 'wallet_switchEthereumChain': + return null + case 'eth_estimateGas': + return '0x5208' + case 'eth_sendTransaction': { + const tx = (params as Array>)?.[0] ?? {} + const to = String(tx.to ?? '').toLowerCase() + const data = tx.data as Hex + const hash = nextTransactionHash() + + if (to === MOCK_HOUSES.toLowerCase()) { + const decoded = decodeFunctionData({ abi: HOUSES_WRITE_ABI, data }) + if (decoded.functionName === 'castVote') pendingEffectsByHash.set(hash, { kind: 'vote' }) + if (decoded.functionName === 'unstake') pendingEffectsByHash.set(hash, { kind: 'unstake' }) + } else if (to === MOCK_G_TOKEN.toLowerCase()) { + const decoded = decodeFunctionData({ abi: G_TOKEN_WRITE_ABI, data }) + if (decoded.functionName === 'transferAndCall') { + const registrationData = decoded.args[2] + const [house] = decodeAbiParameters(REGISTRATION_DATA_TYPES, registrationData) + pendingEffectsByHash.set(hash, { kind: 'registration', house: Number(house) === 1 ? 1 : 0 }) + } + } + return hash + } + default: + throw new Error(`Interactive governance mock: unsupported wallet method "${method}"`) + } + }, + on(event: string, listener: (...args: unknown[]) => void) { + listeners[event] = [...(listeners[event] ?? []), listener] + }, + removeListener(event: string, listener: (...args: unknown[]) => void) { + listeners[event] = (listeners[event] ?? []).filter((entry) => entry !== listener) + }, + } as EIP1193Provider + + window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = requestUrl(input) + + if (url.includes(MOCK_GOVERNANCE_RPC_PATH)) { + const payload = JSON.parse(String(init?.body ?? '{}')) as { + id: number + method: string + params?: unknown[] + } + const respond = (result: unknown) => + new Response(JSON.stringify({ jsonrpc: '2.0', id: payload.id, result }), { + headers: { 'content-type': 'application/json' }, + }) + + if (payload.method === 'eth_getTransactionReceipt') { + const hash = payload.params?.[0] as Hex + const effect = pendingEffectsByHash.get(hash) + if (effect) applyReceiptEffect(effect) + return respond(buildMockReceipt('success', hash)) + } + if (payload.method === 'eth_getBlockByNumber') return respond(buildMockBlock()) + if (payload.method === 'eth_blockNumber') return respond('0x10') + if (payload.method !== 'eth_call') return respond('0x') + + const call = (payload.params?.[0] as { to?: Address; data?: Hex } | undefined) ?? {} + if (!call.to || !call.data) return respond('0x') + const result = encodeMockGovernanceRead(call.to, call.data, { + memberStatusByAccount: { [MOCK_ACCOUNT.toLowerCase()]: session.memberStatus }, + memberHouseByAccount: { [MOCK_ACCOUNT.toLowerCase()]: session.memberHouse }, + hasVotedByVoter: { [MOCK_ACCOUNT.toLowerCase()]: session.hasVoted }, + }) + return respond(result) + } + + if (url.includes(MOCK_SUPERFLUID_URL_FRAGMENT)) { + return new Response(JSON.stringify(buildMockFundingStreams()), { + headers: { 'content-type': 'application/json' }, + }) + } + + return originalFetch(input, init) + }) as typeof window.fetch + + return { + provider, + celoRpcUrl: MOCK_GOVERNANCE_RPC_PATH, + addresses: { housesAddress: MOCK_HOUSES, goodIdAddress: MOCK_GOOD_ID, gTokenAddress: MOCK_G_TOKEN }, + teardown: () => { + window.fetch = originalFetch + }, + } +} From 0ecdc6620ce97d3869a9c194655cfa0b72c5389c Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:26 +0000 Subject: [PATCH 02/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../src/fixtures/governanceRuntimeMock.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/storybook/src/fixtures/governanceRuntimeMock.ts b/examples/storybook/src/fixtures/governanceRuntimeMock.ts index 656703e8..049d58b2 100644 --- a/examples/storybook/src/fixtures/governanceRuntimeMock.ts +++ b/examples/storybook/src/fixtures/governanceRuntimeMock.ts @@ -31,11 +31,15 @@ export const MOCK_GOOD_ID = '0x5555555555555555555555555555555555555555' as Addr export const MOCK_CITIZEN = '0x6666666666666666666666666666666666666666' as Address export const MOCK_ALIGNMENT = '0x7777777777777777777777777777777777777777' as Address export const MOCK_POOL = '0x8888888888888888888888888888888888888888' as Address +export const MOCK_G_TOKEN = '0x9999999999999999999999999999999999999999' as Address export interface MockGovernanceReadOptions { memberStatus?: 0 | 1 | 2 | 3 | 4 memberStatusByAccount?: Record memberHouseByAccount?: Record + // Keyed by voter address, lowercased. Lets an interactive session reflect a + // just-submitted vote without needing a real per-voteId ledger. + hasVotedByVoter?: Record } export function encodeMockGovernanceRead( @@ -148,12 +152,14 @@ export function encodeMockGovernanceRead( functionName: 'getVoteRecipients', result: [MOCK_ALIGNMENT], }) - case 'getHasVoted': + case 'getHasVoted': { + const voter = String(decoded.args[1]).toLowerCase() return encodeFunctionResult({ abi: HOUSES_READ_ABI, functionName: 'getHasVoted', - result: false, + result: options.hasVotedByVoter?.[voter] ?? false, }) + } case 'getFinalizedUnits': return encodeFunctionResult({ abi: HOUSES_READ_ABI, @@ -167,6 +173,9 @@ export function encodeMockGovernanceRead( result: [MOCK_HOUSES, 1n, MOCK_POOL], }) default: - throw new Error(`Unexpected houses read: ${decoded.functionName}`) + // Every HOUSES_READ_ABI function is handled above, so this branch is unreachable at + // the type level (decoded narrows to `never`) but kept as a runtime guard against a + // future ABI addition that isn't wired into this mock yet. + throw new Error(`Unexpected houses read call data: ${data}`) } } From 22d5a959ae734bac6cb639f99b1c3ad48170a57d Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:28 +0000 Subject: [PATCH 03/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../governance-widget/GovernanceWidget.mdx | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 examples/storybook/src/stories/governance-widget/GovernanceWidget.mdx diff --git a/examples/storybook/src/stories/governance-widget/GovernanceWidget.mdx b/examples/storybook/src/stories/governance-widget/GovernanceWidget.mdx new file mode 100644 index 00000000..2301ef6d --- /dev/null +++ b/examples/storybook/src/stories/governance-widget/GovernanceWidget.mdx @@ -0,0 +1,101 @@ +import { Canvas, Meta, Source } from '@storybook/blocks'; +import * as ShowcaseStories from './GovernanceWidgetShowcase.stories'; +import * as ThemeOverridesStories from './GovernanceWidgetThemeOverrides.stories'; +import { DocsCallout, DocsCard, DocsGrid, DocsPage, DocsSection } from '../docs/DocsLayout'; + + + + + + + + + + + + + + + + + ) +}`} + /> + + + + + Connects a mock wallet and mock RPC/subgraph directly to the real adapter — the only QA + story that exercises the runtime rather than a static state. + + + + + + Disconnected, onboarding, active membership, voting, unstaking, and error states all live in + `QA / GovernanceWidget / Runtime Fixtures`. + + + + + + + Real wallet, real GoodDaoHouses contract, no mocked reads or writes. + + + Static dashboard fixtures for screenshots and automation, plus one live-mocked-data story + for driving the real runtime by hand. + + + + + + + Use the showcase story for product-facing wallet checks against the real contract. Use the + QA fixtures for repeatable screenshots and state coverage, and the live mocked-data flow when + you need to manually exercise the real runtime without a live contract. + + + From 1f70539fe53614f08f9b8b7e83946c1b9a7f2d6c Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:29 +0000 Subject: [PATCH 04/10] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude --- .../GovernanceRuntime.stories.tsx | 491 ------------------ 1 file changed, 491 deletions(-) delete mode 100644 examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx diff --git a/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx deleted file mode 100644 index ff1d89fb..00000000 --- a/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import React from 'react' -import type { Meta, StoryObj } from '@storybook/react' -import { Card, Text, YStack } from '@goodwidget/ui' -import { - GovernanceWidget, - type GovernanceWidgetAdapterFactory, - type GovernanceWidgetAdapterState, - type GovernanceWidgetStatus, -} from '@goodwidget/governance-widget' -import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' -import { - getInjectedEip1193Provider, - isInjectedProviderUsable, -} from '../../fixtures/injectedEip1193' - -const meta: Meta = { - title: 'QA/GovernanceWidget Runtime Fixtures', - component: GovernanceWidget, - parameters: { - layout: 'padded', - goodWidgetProvider: { useShell: false, useProvider: false }, - }, -} - -export default meta -type Story = StoryObj - -const connectedAddress = '0x4E5B2D7a45C2e31a8F0d09b4bE1fA11aD3aC9F08' as const -const alignmentRecipients = [ - '0x1111111111111111111111111111111111111111', - '0x2222222222222222222222222222222222222222', - '0x3333333333333333333333333333333333333333', -] as const - -function createDashboard( - overrides: Partial = {}, -): GovernanceWidgetAdapterState['dashboard'] { - return { - impact: { - title: 'Distributed', - metrics: [ - { label: 'UBI Pool', amount: { value: 12400000, token: 'G$' } }, - { - label: 'Impact Pool', - amount: { value: 5234891, token: 'G$', isStreaming: true, streamLabel: 'Live stream active' }, - }, - ], - description: - 'Empowering 640k+ people worldwide through transparent, decentralized funding for public goods.', - ctaLabel: 'View Impact Report Q3', - }, - activeMembers: { - icon: 'check' as const, - title: 'Active Members', - amount: 12402, - amountType: 'raw' as const, - metadataType: 'time-window' as const, - metadata: { label: 'Active members only', tone: 'muted' as const, icon: 'info' as const }, - }, - alignmentVoting: { - voteId: 'alignment-current', - title: 'Q3 House Of Alignment Funding Allocation', - summaryLabel: 'Current top 3 voted', - options: [ - { id: alignmentRecipients[0], label: 'Local Food Chain', percentage: 42 }, - { id: alignmentRecipients[1], label: 'Web3 Literacy', percentage: 31 }, - { id: alignmentRecipients[2], label: 'Civic Onboarding', percentage: 27 }, - ], - recipients: [...alignmentRecipients], - allocationsBps: { - [alignmentRecipients[0]]: 4200, - [alignmentRecipients[1]]: 3100, - [alignmentRecipients[2]]: 2700, - }, - allocationTotalBps: 10000, - canVote: false, - hasVoted: false, - isVotingOpen: true, - executed: false, - finalizedUnits: {}, - disabledReason: 'Only active House of Alignment members can vote.', - }, - fundingDistribution: { - title: 'Funding distribution', - centerLabel: 'Mocked pool total', - totalAmount: { value: 450000, token: 'G$', isStreaming: true, streamLabel: 'Mock pool data' }, - projects: [ - { id: 'education', name: 'Education Hubs', amount: { value: 157500, token: 'G$' }, percentage: 35 }, - { id: 'merchant', name: 'Merchant Onboard', amount: { value: 112500, token: 'G$' }, percentage: 25 }, - { id: 'grants', name: 'Dev Grants', amount: { value: 90000, token: 'G$' }, percentage: 20 }, - { id: 'creator', name: 'Creator Fund', amount: { value: 90000, token: 'G$' }, percentage: 20 }, - ], - isStreaming: true, - emptyStateLabel: 'No active funding distribution yet.', - }, - ...overrides, - } -} - -function createState( - status: GovernanceWidgetStatus, - overrides: Partial = {}, -): GovernanceWidgetAdapterState { - const isConnected = status !== 'disconnected' - const member: GovernanceWidgetAdapterState['member'] = - status === 'active_citizenship' || status === 'active_alignment' || status === 'revoked' - ? { - house: status === 'active_alignment' ? 'alignment' : 'citizenship', - status: status === 'revoked' ? 'revoked' : 'active', - stakedAmount: 250000000000000000000n, - joinedAt: Date.UTC(2026, 0, 10), - updatedAt: Date.UTC(2026, 2, 1), - unstakedAt: null, - memberIndex: 0n, - name: status === 'active_alignment' ? 'Solar Commons' : 'Maya Citizen', - socialLinks: 'https://twitter.com/gooddollar', - projectWebpage: 'https://solar.example', - missionStatement: 'Expand regenerative local access.', - distributionStrategy: 'Allocate quarterly grants through community review.', - } - : null - - return { - status, - address: isConnected ? connectedAddress : null, - chainId: status === 'unsupported_chain' ? 1 : 42220, - identityStatus: status === 'onboarding_required' ? 'unverified' : 'verified', - identityVerificationUrl: null, - member, - dashboard: createDashboard(), - selectedHouse: 'citizenship', - onboardingStepId: undefined, - profileDraft: {}, - stakeAmountLabel: '250 G$', - minimumStakeAmounts: { citizenship: 250000000000000000000n, alignment: 500000000000000000000n }, - transactionSteps: [ - { id: 'prepare', title: 'Prepare wallet balance', status: 'completed' }, - { id: 'approve', title: 'Approve governance stake', status: 'active' }, - { id: 'stake', title: 'Lock the membership stake', status: 'pending' }, - { id: 'finalize', title: 'Finalize governance access', status: 'pending' }, - ], - registrationHash: null, - transaction: { kind: null, status: 'idle', hash: null, error: null }, - unstakeAvailability: { - canUnstake: false, - unlockAt: Date.UTC(2026, 8, 1, 12), - disabledReason: 'Membership remains locked until the current governance term has passed.', - }, - lifecycleNotice: null, - error: null, - ...overrides, - } -} - -function createAdapterFactory(state: GovernanceWidgetAdapterState): GovernanceWidgetAdapterFactory { - return () => ({ - state, - actions: { - connect: async () => {}, - switchToCelo: async () => {}, - refresh: async () => {}, - retry: async () => {}, - selectHouse: () => {}, - register: async () => {}, - unstake: async () => {}, - openVote: () => {}, - closeVote: () => {}, - setVoteAllocation: () => {}, - submitVote: async () => {}, - startIdentityVerification: async () => {}, - }, - }) -} - -function RuntimeStory({ - state, - defaultTheme = 'light', - useInjectedProvider = false, -}: { - state: GovernanceWidgetAdapterState - defaultTheme?: 'light' | 'dark' - useInjectedProvider?: boolean -}) { - const injectedProvider = getInjectedEip1193Provider() - - if (useInjectedProvider && !isInjectedProviderUsable(injectedProvider)) { - return ( - - - No injected wallet found - Install or enable an injected EIP-1193 wallet, then refresh Storybook. - - - ) - } - - const provider = useInjectedProvider ? injectedProvider : createCustodialEip1193Provider() - - return ( - - ) -} - -export const DisconnectedDashboard: Story = { - render: () => , -} - -export const LoadingConnected: Story = { - render: () => , -} - -export const OnboardingHouseSelection: Story = { - render: () => ( - - ), -} - -export const PendingAlignment: Story = { - render: () => , -} - -export const ActiveCitizenship: Story = { - render: () => , -} - -export const UpcomingVote: Story = { - render: () => ( - - ), -} - -export const ActiveAlignmentInjected: Story = { - render: () => ( - - ), -} - -export const VoteDetailOpen: Story = { - render: () => ( - - ), -} - -export const AlreadyVoted: Story = { - render: () => ( - - ), -} - -export const VoteClosedExecuted: Story = { - render: () => ( - - ), -} - -export const EmptyRecipients: Story = { - render: () => ( - - ), -} - -export const PoolUnavailableMocked: Story = { - render: () => ( - - ), -} - -export const UnsupportedChain: Story = { - render: () => , -} - -export const ActiveMembershipUnstakeReady: Story = { - render: () => ( - - ), -} - -export const UnstakeWalletConfirmation: Story = { - render: () => ( - - ), -} - -export const UnstakeSubmitted: Story = { - render: () => ( - - ), -} - -export const UnstakeRejected: Story = { - render: () => ( - - ), -} - -export const UnstakeReverted: Story = { - render: () => ( - - ), -} - -export const UnstakedReturnsToOnboarding: Story = { - render: () => ( - - ), -} - -export const RevokedMembership: Story = { - render: () => , -} - -export const FriendlyContractError: Story = { - render: () => ( - - ), -} - -export const RealAdapterMockedRuntime: Story = { - render: () => { - const injectedProvider = getInjectedEip1193Provider() - const provider = isInjectedProviderUsable(injectedProvider) - ? injectedProvider - : createCustodialEip1193Provider() - - return ( - - ) - }, -} From b09ac14eacf9880211cab8381a4ebff217800175 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:30 +0000 Subject: [PATCH 05/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../GovernanceWidgetQA.stories.tsx | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 examples/storybook/src/stories/governance-widget/GovernanceWidgetQA.stories.tsx diff --git a/examples/storybook/src/stories/governance-widget/GovernanceWidgetQA.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceWidgetQA.stories.tsx new file mode 100644 index 00000000..5e11261d --- /dev/null +++ b/examples/storybook/src/stories/governance-widget/GovernanceWidgetQA.stories.tsx @@ -0,0 +1,387 @@ +import React, { useEffect, useRef } from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import { Card, Text, YStack } from '@goodwidget/ui' +import { GovernanceWidget, type GovernanceWidgetAdapterState } from '@goodwidget/governance-widget' +import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' +import { + getInjectedEip1193Provider, + isInjectedProviderUsable, +} from '../../fixtures/injectedEip1193' +import { + createInteractiveGovernanceEnvironment, + type InteractiveGovernanceEnvironment, +} from '../../fixtures/governanceInteractiveMock' +import { + alignmentRecipients, + createAdapterFactory, + createDashboard, + createState, +} from '../helpers/governanceWidgetStories' + +const meta: Meta = { + title: 'QA/GovernanceWidget/Runtime Fixtures', + component: GovernanceWidget, + tags: ['autodocs', 'qa'], + parameters: { + layout: 'padded', + goodWidgetProvider: { useShell: false, useProvider: false }, + }, +} + +export default meta +type Story = StoryObj + +function RuntimeStory({ + state, + defaultTheme = 'light', + useInjectedProvider = false, +}: { + state: GovernanceWidgetAdapterState + defaultTheme?: 'light' | 'dark' + useInjectedProvider?: boolean +}) { + const injectedProvider = getInjectedEip1193Provider() + + if (useInjectedProvider && !isInjectedProviderUsable(injectedProvider)) { + return ( + + + No injected wallet found + Install or enable an injected EIP-1193 wallet, then refresh Storybook. + + + ) + } + + const provider = useInjectedProvider ? injectedProvider : createCustodialEip1193Provider() + + return ( + + ) +} + +// Uses the real useGovernanceAdapter runtime (no adapterFactory override) against a +// browser-native mocked Celo RPC + Superfluid subgraph, so a human can drive the full +// onboarding -> vote -> unstake flow directly in Storybook, not just under Playwright. +function LiveMockedDataFlowStory() { + const environmentRef = useRef(null) + if (!environmentRef.current) environmentRef.current = createInteractiveGovernanceEnvironment() + + useEffect(() => { + const environment = environmentRef.current + return () => environment?.teardown() + }, []) + + const { provider, celoRpcUrl, addresses } = environmentRef.current + + return ( + + ) +} + +export const DisconnectedDashboard: Story = { + render: () => , +} + +export const LoadingConnected: Story = { + render: () => , +} + +export const OnboardingHouseSelection: Story = { + render: () => ( + + ), +} + +export const PendingAlignment: Story = { + render: () => , +} + +export const ActiveCitizenship: Story = { + render: () => , +} + +export const UpcomingVote: Story = { + render: () => ( + + ), +} + +export const ActiveAlignmentInjected: Story = { + render: () => ( + + ), +} + +export const VoteDetailOpen: Story = { + render: () => ( + + ), +} + +export const AlreadyVoted: Story = { + render: () => ( + + ), +} + +export const VoteClosedExecuted: Story = { + render: () => ( + + ), +} + +export const EmptyRecipients: Story = { + render: () => ( + + ), +} + +export const PoolUnavailableMocked: Story = { + render: () => ( + + ), +} + +export const UnsupportedChain: Story = { + render: () => , +} + +export const ActiveMembershipUnstakeReady: Story = { + render: () => ( + + ), +} + +export const UnstakeWalletConfirmation: Story = { + render: () => ( + + ), +} + +export const UnstakeSubmitted: Story = { + render: () => ( + + ), +} + +export const UnstakeRejected: Story = { + render: () => ( + + ), +} + +export const UnstakeReverted: Story = { + render: () => ( + + ), +} + +export const UnstakedReturnsToOnboarding: Story = { + render: () => ( + + ), +} + +export const RevokedMembership: Story = { + render: () => , +} + +export const FriendlyContractError: Story = { + render: () => ( + + ), +} + +// Real useGovernanceAdapter runtime (no adapterFactory override), but network mocking is +// left entirely to the caller: Playwright's runtime.spec.ts drives this story via its own +// page.route interception of `/mock-governance-rpc` and injects window.ethereum itself, so +// it can pause/resume reads and receipts mid-test. Kept distinct from LiveMockedDataFlow +// below, which is self-contained and meant for a human to open directly in Storybook. +export const RealAdapterMockedRuntime: Story = { + render: () => { + const injectedProvider = getInjectedEip1193Provider() + const provider = isInjectedProviderUsable(injectedProvider) + ? injectedProvider + : createCustodialEip1193Provider() + + return ( + + ) + }, +} + +// The live testable flow with mocked data: separated from GovernanceWidgetShowcase (which +// always uses a real wallet against the real contract), and separated from the static +// fixtures above (which never touch useGovernanceAdapter). Self-contained mocked RPC + +// wallet, so a human can drive it directly in Storybook without Playwright. +export const LiveMockedDataFlow: Story = { + render: () => , +} From 9f25397ed81f1269adeb1256a7c5d0b8f65142b5 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:32 +0000 Subject: [PATCH 06/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../GovernanceWidgetShowcase.stories.tsx | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 examples/storybook/src/stories/governance-widget/GovernanceWidgetShowcase.stories.tsx diff --git a/examples/storybook/src/stories/governance-widget/GovernanceWidgetShowcase.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceWidgetShowcase.stories.tsx new file mode 100644 index 00000000..886ddb3d --- /dev/null +++ b/examples/storybook/src/stories/governance-widget/GovernanceWidgetShowcase.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Card, Text, YStack } from '@goodwidget/ui' +import { GovernanceWidget } from '@goodwidget/governance-widget' +import { + getInjectedEip1193Provider, + isInjectedProviderUsable, +} from '../../fixtures/injectedEip1193' +import { createCustodialEip1193Provider } from '../../fixtures/custodialEip1193' + +// The GoodDaoHouses contract is not yet on the production Celo deployment — this is the +// `development-celo` address recorded in GoodProtocol PR #300 (GoodProtocol PR #299 has the +// final contract build this widget targets). FlowSplitter isn't wired to this deployment yet, +// so the funding-distribution chart is expected to render its empty state here. +const DEV_CELO_HOUSES_ADDRESS = '0x4Bc3Cdc036f21b68E034C0f1d90775fc3D725735' as const + +interface GovernanceWidgetStoryArgs { + defaultTheme: 'light' | 'dark' +} + +const meta: Meta = { + title: 'Widgets/GovernanceWidget/Showcase', + component: GovernanceWidget, + tags: ['integrator', 'manual', 'showcase'], + parameters: { layout: 'padded' }, + argTypes: { + defaultTheme: { + control: 'radio', + options: ['dark', 'light'], + description: 'Base theme applied via the widget’s own defaultTheme prop.', + }, + }, + args: { + defaultTheme: 'dark', + }, +} + +export default meta +type Story = StoryObj + +function InjectedWalletStory({ defaultTheme }: GovernanceWidgetStoryArgs) { + const injectedProvider = getInjectedEip1193Provider() + + if (!isInjectedProviderUsable(injectedProvider)) { + return ( + + + No injected wallet found + + Install or enable an injected EIP-1193 wallet on Celo, then refresh Storybook. + + + + ) + } + + return ( + + ) +} + +function CustodialWalletStory({ defaultTheme }: GovernanceWidgetStoryArgs) { + try { + const provider = createCustodialEip1193Provider() + + return ( + + ) + } catch (error: unknown) { + return ( + + + Custodial fixture not configured + + {error instanceof Error ? error.message : 'Set a local private key in custodialEip1193.ts'} + + + + ) + } +} + +// Real wallet, real dev-celo GoodDaoHouses contract, no mocked reads or writes — this is the +// live integrator-facing surface, deliberately kept separate from the QA fixtures/mocked flow. +export const InjectedWallet: Story = { + render: ({ defaultTheme }) => , +} + +export const CustodialWallet: Story = { + render: ({ defaultTheme }) => , +} From 92dbf9796a1737ec0204728876f98b670454a243 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:33 +0000 Subject: [PATCH 07/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- ...GovernanceWidgetThemeOverrides.stories.tsx | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 examples/storybook/src/stories/governance-widget/GovernanceWidgetThemeOverrides.stories.tsx diff --git a/examples/storybook/src/stories/governance-widget/GovernanceWidgetThemeOverrides.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceWidgetThemeOverrides.stories.tsx new file mode 100644 index 00000000..e30885ab --- /dev/null +++ b/examples/storybook/src/stories/governance-widget/GovernanceWidgetThemeOverrides.stories.tsx @@ -0,0 +1,138 @@ +/** + * GovernanceWidget — Theme Overrides — demonstrates the widget's public theming + * surface as live color-picker controls. The code snippet is generated from the + * live arg values, so it can never drift from what's rendered. + * + * GovernanceWidget's own named theme components (packages/governance-widget/src/shared.tsx) + * are `GovernanceWrapper` (the card shell every section — impact, alignment voting, + * optimistic voting, funding distribution — renders inside of) and `ImpactCard` / + * `ImpactCardAction` (the impact summary card and its call-to-action button). All other + * governance surfaces reuse shared @goodwidget/ui components (`BalanceCard`, `Button`) + * that already have theme keys wired for other widgets — those are documented as + * reference-only below since they aren't governance-specific. + * + * Controls are wired for `dark_GovernanceWrapper` and `dark_ImpactCard` / + * `dark_ImpactCardAction` — the handful of high-impact targets that visibly shift the + * default brand, not exhaustive coverage of every value. + */ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import type { GoodWidgetThemeOverrides } from '@goodwidget/core' +import { ThemedDashboardStory } from '../helpers/governanceWidgetStories' +import { DocsCallout, DocsList } from '../docs/DocsLayout' + +const REFERENCE_ONLY_TARGETS: Array<{ name: string; fields: string[] }> = [ + { name: 'BalanceCard', fields: ['background', 'borderColor', 'shadowColor'] }, + { name: 'Button', fields: ['background', 'color', 'borderColor'] }, +] + +function CodeBlock({ children }: { children: string }) { + return ( +
+      {children}
+    
+ ) +} + +interface OverridesArgs { + wrapperBorderColor: string + wrapperShadowColor: string + impactCardBackground: string + impactCardActionBackground: string +} + +function buildThemeOverrides(args: OverridesArgs): GoodWidgetThemeOverrides { + return { + themes: { + dark_GovernanceWrapper: { + borderColor: args.wrapperBorderColor, + shadowColor: args.wrapperShadowColor, + }, + dark_ImpactCard: { + backgroundColor: args.impactCardBackground, + }, + dark_ImpactCardAction: { + backgroundColor: args.impactCardActionBackground, + }, + }, + } +} + +const meta: Meta = { + title: 'Widgets/GovernanceWidget/Theme overrides', + tags: ['integrator', 'showcase'], + parameters: { layout: 'padded' }, + argTypes: { + wrapperBorderColor: { control: 'color', description: 'themes.dark_GovernanceWrapper.borderColor' }, + wrapperShadowColor: { control: 'color', description: 'themes.dark_GovernanceWrapper.shadowColor' }, + impactCardBackground: { control: 'color', description: 'themes.dark_ImpactCard.backgroundColor' }, + impactCardActionBackground: { + control: 'color', + description: 'themes.dark_ImpactCardAction.backgroundColor', + }, + }, + args: { + wrapperBorderColor: '#7C3AED', + wrapperShadowColor: '#7C3AED', + impactCardBackground: '#1E1B4B', + impactCardActionBackground: '#7C3AED', + }, +} +export default meta +type Story = StoryObj + +export const Playground: Story = { + render: (args) => { + const themeOverrides = buildThemeOverrides(args) + return ( +
+ + {``} + + + + +
  • + dark_GovernanceWrapper / light_GovernanceWrapper: backgroundColor, + borderColor, color, shadowColor — wired to the controls above (borderColor and + shadowColor only) +
  • +
  • + dark_ImpactCard / light_ImpactCard: backgroundColor, borderColor, + color — wired to the controls above (backgroundColor only) +
  • +
  • + dark_ImpactCardAction / light_ImpactCardAction: backgroundColor, + color — wired to the controls above (backgroundColor only) +
  • + {REFERENCE_ONLY_TARGETS.map((target) => ( +
  • + + dark_{target.name} / light_{target.name} + + : {target.fields.join(', ')} — shared with other widgets, not governance-specific +
  • + ))} +
    +
    + + +
    + ) + }, +} From 99430b38b9771575eb5d842c0569060d94e43c83 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:34 +0000 Subject: [PATCH 08/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../helpers/governanceWidgetStories.tsx | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 examples/storybook/src/stories/helpers/governanceWidgetStories.tsx diff --git a/examples/storybook/src/stories/helpers/governanceWidgetStories.tsx b/examples/storybook/src/stories/helpers/governanceWidgetStories.tsx new file mode 100644 index 00000000..9506ae54 --- /dev/null +++ b/examples/storybook/src/stories/helpers/governanceWidgetStories.tsx @@ -0,0 +1,185 @@ +import type { GoodWidgetThemeOverrides } from '@goodwidget/core' +import { + GovernanceWidget, + type GovernanceWidgetAdapterFactory, + type GovernanceWidgetAdapterState, + type GovernanceWidgetStatus, +} from '@goodwidget/governance-widget' + +const connectedAddress = '0x4E5B2D7a45C2e31a8F0d09b4bE1fA11aD3aC9F08' as const +export const alignmentRecipients = [ + '0x1111111111111111111111111111111111111111', + '0x2222222222222222222222222222222222222222', + '0x3333333333333333333333333333333333333333', +] as const + +export function createDashboard( + overrides: Partial = {}, +): GovernanceWidgetAdapterState['dashboard'] { + return { + impact: { + title: 'Distributed', + metrics: [ + { label: 'UBI Pool', amount: { value: 12400000, token: 'G$' } }, + { + label: 'Impact Pool', + amount: { value: 5234891, token: 'G$', isStreaming: true, streamLabel: 'Live stream active' }, + }, + ], + description: + 'Empowering 640k+ people worldwide through transparent, decentralized funding for public goods.', + ctaLabel: 'View Impact Report Q3', + }, + activeMembers: { + icon: 'check' as const, + title: 'Active Members', + amount: 12402, + amountType: 'raw' as const, + metadataType: 'time-window' as const, + metadata: { label: 'Active members only', tone: 'muted' as const, icon: 'info' as const }, + }, + alignmentVoting: { + voteId: 'alignment-current', + title: 'Q3 House Of Alignment Funding Allocation', + summaryLabel: 'Current top 3 voted', + options: [ + { id: alignmentRecipients[0], label: 'Local Food Chain', percentage: 42 }, + { id: alignmentRecipients[1], label: 'Web3 Literacy', percentage: 31 }, + { id: alignmentRecipients[2], label: 'Civic Onboarding', percentage: 27 }, + ], + recipients: [...alignmentRecipients], + allocationsBps: { + [alignmentRecipients[0]]: 4200, + [alignmentRecipients[1]]: 3100, + [alignmentRecipients[2]]: 2700, + }, + allocationTotalBps: 10000, + canVote: false, + hasVoted: false, + isVotingOpen: true, + executed: false, + finalizedUnits: {}, + disabledReason: 'Only active House of Alignment members can vote.', + }, + fundingDistribution: { + title: 'Funding distribution', + centerLabel: 'Mocked pool total', + totalAmount: { value: 450000, token: 'G$', isStreaming: true, streamLabel: 'Mock pool data' }, + projects: [ + { id: 'education', name: 'Education Hubs', amount: { value: 157500, token: 'G$' }, percentage: 35 }, + { id: 'merchant', name: 'Merchant Onboard', amount: { value: 112500, token: 'G$' }, percentage: 25 }, + { id: 'grants', name: 'Dev Grants', amount: { value: 90000, token: 'G$' }, percentage: 20 }, + { id: 'creator', name: 'Creator Fund', amount: { value: 90000, token: 'G$' }, percentage: 20 }, + ], + isStreaming: true, + emptyStateLabel: 'No active funding distribution yet.', + }, + ...overrides, + } +} + +export function createState( + status: GovernanceWidgetStatus, + overrides: Partial = {}, +): GovernanceWidgetAdapterState { + const isConnected = status !== 'disconnected' + const member: GovernanceWidgetAdapterState['member'] = + status === 'active_citizenship' || status === 'active_alignment' || status === 'revoked' + ? { + house: status === 'active_alignment' ? 'alignment' : 'citizenship', + status: status === 'revoked' ? 'revoked' : 'active', + stakedAmount: 250000000000000000000n, + joinedAt: Date.UTC(2026, 0, 10), + updatedAt: Date.UTC(2026, 2, 1), + unstakedAt: null, + memberIndex: 0n, + name: status === 'active_alignment' ? 'Solar Commons' : 'Maya Citizen', + socialLinks: 'https://twitter.com/gooddollar', + projectWebpage: 'https://solar.example', + missionStatement: 'Expand regenerative local access.', + distributionStrategy: 'Allocate quarterly grants through community review.', + } + : null + + return { + status, + address: isConnected ? connectedAddress : null, + chainId: status === 'unsupported_chain' ? 1 : 42220, + identityStatus: status === 'onboarding_required' ? 'unverified' : 'verified', + identityVerificationUrl: null, + member, + dashboard: createDashboard(), + selectedHouse: 'citizenship', + onboardingStepId: undefined, + profileDraft: {}, + stakeAmountLabel: '250 G$', + minimumStakeAmounts: { citizenship: 250000000000000000000n, alignment: 500000000000000000000n }, + transactionSteps: [ + { id: 'prepare', title: 'Prepare wallet balance', status: 'completed' }, + { id: 'approve', title: 'Approve governance stake', status: 'active' }, + { id: 'stake', title: 'Lock the membership stake', status: 'pending' }, + { id: 'finalize', title: 'Finalize governance access', status: 'pending' }, + ], + registrationHash: null, + transaction: { kind: null, status: 'idle', hash: null, error: null }, + unstakeAvailability: { + canUnstake: false, + unlockAt: Date.UTC(2026, 8, 1, 12), + disabledReason: 'Membership remains locked until the current governance term has passed.', + }, + lifecycleNotice: null, + error: null, + ...overrides, + } +} + +export function createAdapterFactory(state: GovernanceWidgetAdapterState): GovernanceWidgetAdapterFactory { + return () => ({ + state, + actions: { + connect: async () => {}, + switchToCelo: async () => {}, + refresh: async () => {}, + retry: async () => {}, + selectHouse: () => {}, + register: async () => {}, + unstake: async () => {}, + openVote: () => {}, + closeVote: () => {}, + setVoteAllocation: () => {}, + submitVote: async () => {}, + startIdentityVerification: async () => {}, + }, + }) +} + +// A single fully-populated dashboard state (active alignment member, open vote, live funding +// distribution) used by the theme-overrides Playground so every themeable governance surface +// (GovernanceWrapper, ImpactCard, ImpactCardAction, the shared BalanceCard/Button) renders at +// once behind a mocked adapterFactory — no wallet or network required. +export function ThemedDashboardStory({ + themeOverrides, + defaultTheme = 'dark', +}: { + themeOverrides?: GoodWidgetThemeOverrides + defaultTheme?: 'light' | 'dark' +}) { + const state = createState('active_alignment', { + dashboard: createDashboard({ + alignmentVoting: { + ...createDashboard().alignmentVoting, + canVote: true, + disabledReason: undefined, + }, + }), + }) + + return ( + + ) +} From 640a401d9fc8a4c82c5b3a2839c1069d72d602d3 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:36 +0000 Subject: [PATCH 09/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- .../src/GovernanceWidget.tsx | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/governance-widget/src/GovernanceWidget.tsx b/packages/governance-widget/src/GovernanceWidget.tsx index e1ecc7b3..4ad1dc46 100644 --- a/packages/governance-widget/src/GovernanceWidget.tsx +++ b/packages/governance-widget/src/GovernanceWidget.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Button, ButtonText, Card, Heading, Icon, Input, Spinner, Text, XStack, YStack } from '@goodwidget/ui' import { AlignmentVotingProposalCard } from './AlignmentVotingProposalCard' import { BalanceCard } from './BalanceCard' @@ -290,6 +290,19 @@ function RevokedState({ state }: { state: GovernanceWidgetAdapterState }) { ) } +function GovernanceSignupBanner({ onResume }: { onResume: () => void }) { + return ( + + + Sign up and stake to participate in GoodDAO + + + + ) +} + function MemberFooter({ state }: { state: GovernanceWidgetAdapterState }) { if (!state.member || !isActiveStatus(state.status)) return null @@ -408,12 +421,21 @@ function GovernanceWidgetView({ testId?: string }) { const { state, actions } = adapter + // Skip is a view-only choice, not membership state: it never touches the + // contract, so a reload or a wallet reconnect (address change) drops back + // to onboarding rather than silently remembering the skip. + const [isOnboardingSkipped, setIsOnboardingSkipped] = useState(false) + useEffect(() => { + setIsOnboardingSkipped(false) + }, [state.address]) + const shouldShowDashboard = state.status === 'disconnected' || state.status === 'loading' || state.status === 'unsupported_chain' || state.status === 'friendly_error' || - isActiveStatus(state.status) + isActiveStatus(state.status) || + (state.status === 'onboarding_required' && isOnboardingSkipped) return ( @@ -428,7 +450,10 @@ function GovernanceWidgetView({ ) : null} {state.status === 'vote_detail' ? : null} - {state.status === 'onboarding_required' ? ( + {state.status === 'onboarding_required' && isOnboardingSkipped ? ( + setIsOnboardingSkipped(false)} /> + ) : null} + {state.status === 'onboarding_required' && !isOnboardingSkipped ? ( {state.lifecycleNotice ? ( @@ -456,6 +481,13 @@ function GovernanceWidgetView({ void actions.register(profileDraft) }} /> + ) : null} {state.status === 'pending_alignment' ? : null} From 50e3249a41c169f5caf5716e94d4331308de5e59 Mon Sep 17 00:00:00 2001 From: "goodbounties-nanoclaw-agent[bot]" <307944451+goodbounties-nanoclaw-agent[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:49:37 +0000 Subject: [PATCH 10/10] [gdpatchagent] feat(governance-widget): onboarding skip banner, showcase/QA split, mocked live flow - Add a session-only onboarding skip with a persistent "Sign up and stake" banner; resets on reload or wallet reconnect since it's a view-only choice, not membership state. - Split GovernanceWidgetShowcase (real wallet, real dev-celo contract, no mocks) from GovernanceWidgetQA (deterministic fixtures for screenshots/ automation), and add a self-contained "live mocked-data flow" QA story so a human can drive the real runtime end-to-end in Storybook without a live contract or Playwright. - Add GovernanceWidgetThemeOverrides story and a GovernanceWidget.mdx docs page tying the showcase, theme overrides, and QA stories together. - Extract shared story helpers into stories/helpers/governanceWidgetStories, matching the staking-migration-widget convention. - Restore the RealAdapterMockedRuntime QA story (dropped when GovernanceRuntime.stories.tsx was replaced), which 15 existing Playwright tests depend on, and add coverage for the new mocked-data-flow story. Co-Authored-By: Claude On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv) --- tests/widgets/governance-widget/runtime.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/widgets/governance-widget/runtime.spec.ts b/tests/widgets/governance-widget/runtime.spec.ts index e1d8bca5..ae741812 100644 --- a/tests/widgets/governance-widget/runtime.spec.ts +++ b/tests/widgets/governance-widget/runtime.spec.ts @@ -722,6 +722,19 @@ test('vote submission is single-flight and ignores a stale receipt after account await expect(page.getByText('Vote confirmed on Celo.')).toHaveCount(0) }) +test('live mocked-data flow completes citizenship registration end-to-end', async ({ page }) => { + // Exercises the self-contained fixture (createInteractiveGovernanceEnvironment) used by the + // qa-governancewidget-runtime-fixtures--live-mocked-data-flow story: its window.fetch and + // EIP-1193 provider mocks are installed by the story component itself, not by this test, so + // this only needs to navigate and drive the UI like a human would. + await gotoStory(page, 'qa-governancewidget-runtime-fixtures--live-mocked-data-flow') + + await submitCitizenshipRegistration(page) + + await expect(page.getByTestId('GovernanceWidget-dashboard')).toBeVisible() + await expect(page.getByTestId('GovernanceWidget-member-footer')).toContainText('House of Citizenship') +}) + test('real adapter clears account-scoped governance state while a new wallet loads', async ({ page }) => { logRuntimeDiagnostics(page) await installInjectedProvider(page)