From 6f007bad0800d341650a2a3b465629930add8db6 Mon Sep 17 00:00:00 2001 From: Victor Edeh Date: Thu, 16 Jul 2026 19:41:16 +0100 Subject: [PATCH 1/3] feat(governance): finalize membership lifecycle --- .../src/fixtures/governanceRuntimeMock.ts | 177 ++++ .../GovernanceOnboarding.stories.tsx | 1 + .../GovernanceRuntime.stories.tsx | 178 +++- .../src/GovernanceOnboardingWidget.tsx | 17 +- .../src/GovernanceWidget.tsx | 229 +++-- packages/governance-widget/src/adapter.ts | 780 +++++------------- .../src/hooks/useGovernanceFunding.ts | 127 +++ .../src/hooks/useGovernanceMembership.ts | 480 +++++++++++ .../src/hooks/useGovernanceVoting.ts | 339 ++++++++ packages/governance-widget/src/index.ts | 4 + .../onboarding/GovernanceOnboardingFlow.tsx | 54 +- .../src/onboarding/HouseSelectionCard.tsx | 21 +- .../src/onboarding/OnboardingIdentityCard.tsx | 1 - .../governance-widget/src/onboarding/copy.ts | 4 +- .../src/onboarding/steps/HouseStepContent.tsx | 20 +- .../onboarding/steps/ProfileStepContent.tsx | 1 - .../src/onboarding/steps/StakeStepContent.tsx | 24 +- .../src/sdks/contractReads.ts | 52 +- .../governance-widget/src/sdks/contracts.ts | 25 +- .../governance-widget/src/sdks/funding.ts | 6 + .../src/sdks/transactions.ts | 12 +- packages/governance-widget/src/types.ts | 1 + .../src/widgetRuntimeContract.ts | 31 +- .../governance-widget/adapter-logic.spec.ts | 367 +++++++- .../governance-widget/onboarding.spec.ts | 12 +- .../widgets/governance-widget/runtime.spec.ts | 329 +++++++- .../test-results/gw-01-impact-light.png | Bin 27259 -> 19547 bytes .../gw-02-impact-dark-mobile-disabled.png | Bin 31444 -> 23569 bytes .../gw-03-balance-variants-light.png | Bin 14172 -> 11046 bytes .../gw-04-balance-dark-compact.png | Bin 9019 -> 6513 bytes .../gw-05-alignment-default-light.png | Bin 28663 -> 23930 bytes .../gw-06-alignment-dark-long-options.png | Bin 46169 -> 34514 bytes .../gw-07-optimistic-high-quorum-light.png | Bin 32046 -> 24023 bytes ...gw-08-optimistic-dark-low-quorum-mixed.png | Bin 38225 -> 29760 bytes .../gw-09-funding-distribution-light.png | Bin 39012 -> 28988 bytes ...10-funding-distribution-dark-populated.png | Bin 39333 -> 29374 bytes ...funding-distribution-empty-dark-mobile.png | Bin 20000 -> 16391 bytes .../gw-12-interaction-alignment.png | Bin 28663 -> 23930 bytes .../gw-13-impact-light-component-override.png | Bin 32328 -> 22924 bytes .../test-results/gwo-01-welcome-verified.png | Bin 41291 -> 33716 bytes .../test-results/gwo-02-house-selection.png | Bin 69007 -> 38446 bytes .../test-results/gwo-03-profile-alignment.png | Bin 81543 -> 60518 bytes .../gwo-04-stake-progress-active.png | Bin 84075 -> 48208 bytes .../test-results/gwo-05-success.png | Bin 44346 -> 31163 bytes .../gwo-06-welcome-unverified.png | Bin 45846 -> 33947 bytes .../gwo-07-house-selection-standalone.png | Bin 67887 -> 38394 bytes .../gwo-08-profile-citizenship-ready.png | Bin 63256 -> 47666 bytes .../gwo-09-profile-alignment-error.png | Bin 83329 -> 61669 bytes .../gwo-10-stake-progress-failed.png | Bin 79685 -> 46178 bytes .../gwo-11-success-standalone.png | Bin 44346 -> 31163 bytes .../gwr-01-disconnected-dashboard.png | Bin 130894 -> 87495 bytes .../test-results/gwr-02-loading-connected.png | Bin 139636 -> 91612 bytes .../gwr-03-onboarding-hoa-unavailable.png | Bin 89814 -> 39429 bytes .../test-results/gwr-04-pending-alignment.png | Bin 51168 -> 22876 bytes .../gwr-05-active-citizenship.png | Bin 137433 -> 105736 bytes .../gwr-06-active-alignment-injected.png | Bin 147160 -> 116566 bytes .../test-results/gwr-07-vote-detail-open.png | Bin 66317 -> 37058 bytes .../test-results/gwr-08-already-voted.png | Bin 147695 -> 109763 bytes .../gwr-09-vote-closed-executed.png | Bin 137252 -> 105460 bytes .../test-results/gwr-10-empty-recipients.png | Bin 133772 -> 99566 bytes .../gwr-11-pool-unavailable-mocked.png | Bin 123369 -> 96572 bytes .../test-results/gwr-12-unsupported-chain.png | Bin 148548 -> 96078 bytes .../test-results/gwr-13-restake-required.png | Bin 47802 -> 0 bytes .../test-results/gwr-13-unstake-ready.png | Bin 0 -> 103884 bytes .../gwr-14-friendly-contract-error.png | Bin 145365 -> 93943 bytes .../test-results/gwr-15-hoa-greyed-out.png | Bin 98301 -> 42513 bytes .../gwr-16-unstake-wallet-confirmation.png | Bin 0 -> 105424 bytes .../test-results/gwr-17-unstake-submitted.png | Bin 0 -> 106021 bytes .../test-results/gwr-18-unstake-rejected.png | Bin 0 -> 105621 bytes .../gwr-19-unstaked-onboarding.png | Bin 0 -> 38361 bytes .../test-results/gwr-20-revoked.png | Bin 0 -> 21943 bytes .../gwr-21-real-adapter-mocked-runtime.png | Bin 0 -> 82089 bytes .../test-results/gwr-22-upcoming-vote.png | Bin 0 -> 103925 bytes .../test-results/gwr-23-unstake-reverted.png | Bin 0 -> 107301 bytes 74 files changed, 2509 insertions(+), 783 deletions(-) create mode 100644 examples/storybook/src/fixtures/governanceRuntimeMock.ts create mode 100644 packages/governance-widget/src/hooks/useGovernanceFunding.ts create mode 100644 packages/governance-widget/src/hooks/useGovernanceMembership.ts create mode 100644 packages/governance-widget/src/hooks/useGovernanceVoting.ts delete mode 100644 tests/widgets/governance-widget/test-results/gwr-13-restake-required.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-13-unstake-ready.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-16-unstake-wallet-confirmation.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-17-unstake-submitted.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-18-unstake-rejected.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-19-unstaked-onboarding.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-20-revoked.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-21-real-adapter-mocked-runtime.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-22-upcoming-vote.png create mode 100644 tests/widgets/governance-widget/test-results/gwr-23-unstake-reverted.png diff --git a/examples/storybook/src/fixtures/governanceRuntimeMock.ts b/examples/storybook/src/fixtures/governanceRuntimeMock.ts new file mode 100644 index 00000000..0bc0e02e --- /dev/null +++ b/examples/storybook/src/fixtures/governanceRuntimeMock.ts @@ -0,0 +1,177 @@ +import { + decodeFunctionData, + encodeFunctionResult, + parseAbi, + type Address, + type Hex, +} from 'viem' + +const HOUSES_READ_ABI = parseAbi([ + 'function minimumStake(uint8 house) view returns (uint256)', + 'function getMember(address account) view returns ((uint8 house, uint8 status, uint256 stakedAmount, uint64 joinedAt, uint64 updatedAt, uint64 unstakedAt, uint256 memberIndex, string name, string socialLinks, string projectWebpage, string missionStatement, string distributionStrategy))', + 'function getHoaEligibility(address account) view returns ((bool isEligible, uint64 listedAt, uint64 updatedAt, uint64 delistedAt))', + 'function getActiveMembers(uint8 house) view returns (address[])', + 'function cycleStartTime() view returns (uint64)', + 'function termDuration() view returns (uint64)', + 'function votingTermLength() view returns (uint64)', + 'function isVotingPeriod() view returns (bool)', + 'function getCurrentVoteId() view returns (uint256)', + 'function getVoteConfig(uint256 voteId) view returns ((uint64 startTime, uint64 endTime, uint64 executedAt, bool executed))', + 'function getVoteRecipients(uint256 voteId) view returns (address[])', + 'function getHasVoted(uint256 voteId, address voter) view returns (bool)', + 'function getFinalizedUnits(uint256 voteId, address recipient) view returns (uint128)', + 'function flowSplitterConfig() view returns (address splitter, uint256 poolId, address poolAddress)', +]) + +const GOOD_ID_READ_ABI = parseAbi([ + 'function getWhitelistedRoot(address account) view returns (address)', +]) + +export const MOCK_HOUSES = '0x4444444444444444444444444444444444444444' as Address +export const MOCK_GOOD_ID = '0x5555555555555555555555555555555555555555' as Address +export const MOCK_CITIZEN = '0x6666666666666666666666666666666666666666' as Address +export const MOCK_ALIGNMENT = '0x7777777777777777777777777777777777777777' as Address +export const MOCK_POOL = '0x8888888888888888888888888888888888888888' as Address + +export interface MockGovernanceReadOptions { + memberStatus?: 0 | 1 | 2 | 3 | 4 +} + +export function encodeMockGovernanceRead( + to: Address, + data: Hex, + options: MockGovernanceReadOptions = {}, +): Hex { + if (to.toLowerCase() === MOCK_GOOD_ID.toLowerCase()) { + const decoded = decodeFunctionData({ abi: GOOD_ID_READ_ABI, data }) + if (decoded.functionName !== 'getWhitelistedRoot') { + throw new Error(`Unexpected GoodID read: ${decoded.functionName}`) + } + return encodeFunctionResult({ + abi: GOOD_ID_READ_ABI, + functionName: 'getWhitelistedRoot', + result: MOCK_CITIZEN, + }) + } + + if (to.toLowerCase() !== MOCK_HOUSES.toLowerCase()) { + throw new Error(`Unexpected contract address: ${to}`) + } + + const decoded = decodeFunctionData({ abi: HOUSES_READ_ABI, data }) + switch (decoded.functionName) { + case 'getMember': + const memberStatus = options.memberStatus ?? 2 + const hasMembership = memberStatus !== 0 && memberStatus !== 4 + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getMember', + result: { + house: 0, + status: memberStatus, + stakedAmount: hasMembership ? 1_000n * 10n ** 18n : 0n, + joinedAt: hasMembership ? 1_761_955_200n : 0n, + updatedAt: hasMembership ? 1_764_547_200n : 0n, + unstakedAt: memberStatus === 4 ? 1_784_044_800n : 0n, + memberIndex: 0n, + name: hasMembership ? 'Mocked Citizen' : '', + socialLinks: hasMembership ? 'https://example.com/citizen' : '', + projectWebpage: '', + missionStatement: '', + distributionStrategy: '', + }, + }) + case 'minimumStake': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'minimumStake', + result: 1_000n * 10n ** 18n, + }) + case 'getHoaEligibility': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getHoaEligibility', + result: { + isEligible: true, + listedAt: 1_761_955_200n, + updatedAt: 1_761_955_200n, + delistedAt: 0n, + }, + }) + case 'getActiveMembers': { + const house = Number(decoded.args[0]) + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getActiveMembers', + result: house === 0 ? [MOCK_CITIZEN] : [MOCK_ALIGNMENT], + }) + } + case 'cycleStartTime': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'cycleStartTime', + result: 1_764_547_200n, + }) + case 'termDuration': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'termDuration', + result: 19_440_000n, + }) + case 'votingTermLength': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'votingTermLength', + result: 1_209_600n, + }) + case 'isVotingPeriod': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'isVotingPeriod', + result: true, + }) + case 'getCurrentVoteId': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getCurrentVoteId', + result: 1n, + }) + case 'getVoteConfig': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getVoteConfig', + result: { + startTime: 1_783_987_200n, + endTime: 1_785_196_800n, + executedAt: 0n, + executed: false, + }, + }) + case 'getVoteRecipients': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getVoteRecipients', + result: [MOCK_ALIGNMENT], + }) + case 'getHasVoted': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getHasVoted', + result: false, + }) + case 'getFinalizedUnits': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'getFinalizedUnits', + result: 0n, + }) + case 'flowSplitterConfig': + return encodeFunctionResult({ + abi: HOUSES_READ_ABI, + functionName: 'flowSplitterConfig', + result: [MOCK_HOUSES, 1n, MOCK_POOL], + }) + default: + throw new Error(`Unexpected houses read: ${decoded.functionName}`) + } +} diff --git a/examples/storybook/src/stories/governance-widget/GovernanceOnboarding.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceOnboarding.stories.tsx index eb236198..a0882f8e 100644 --- a/examples/storybook/src/stories/governance-widget/GovernanceOnboarding.stories.tsx +++ b/examples/storybook/src/stories/governance-widget/GovernanceOnboarding.stories.tsx @@ -197,6 +197,7 @@ function CustodialInteractiveFlowStory() { storyProps={{ identityStatus: 'verified', initialStepId: 'welcome', + initialHouse: 'citizenship', walletAddress: '0x4E5B2D7a45C2e31a8F0d09b4bE1fA11aD3aC9F08', dataTestId: 'GovernanceOnboardingWidget-interactive-flow', transactionSteps: stepsState, diff --git a/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx index 56f77ac6..13b22baf 100644 --- a/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx +++ b/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx @@ -26,8 +26,15 @@ export default meta type Story = StoryObj const connectedAddress = '0x4E5B2D7a45C2e31a8F0d09b4bE1fA11aD3aC9F08' as const +const alignmentRecipients = [ + '0x1111111111111111111111111111111111111111', + '0x2222222222222222222222222222222222222222', + '0x3333333333333333333333333333333333333333', +] as const -function createDashboard(overrides: Partial = {}) { +function createDashboard( + overrides: Partial = {}, +): GovernanceWidgetAdapterState['dashboard'] { return { impact: { title: 'Distributed', @@ -55,12 +62,16 @@ function createDashboard(overrides: Partial = {}, ): GovernanceWidgetAdapterState { const isConnected = status !== 'disconnected' - const member = - status === 'active_citizenship' || status === 'active_alignment' || status === 'restake_required' + const member: GovernanceWidgetAdapterState['member'] = + status === 'active_citizenship' || status === 'active_alignment' || status === 'revoked' ? { house: status === 'active_alignment' ? 'alignment' : 'citizenship', - status: status === 'restake_required' ? 'unstaked' : 'active', + status: status === 'revoked' ? 'revoked' : 'active', stakedAmount: 250000000000000000000n, joinedAt: Date.UTC(2026, 0, 10), updatedAt: Date.UTC(2026, 2, 1), - unstakedAt: status === 'restake_required' ? Date.UTC(2026, 5, 1) : null, + unstakedAt: null, memberIndex: 0n, name: status === 'active_alignment' ? 'Solar Commons' : 'Maya Citizen', socialLinks: 'https://twitter.com/gooddollar', @@ -130,6 +141,13 @@ function createState( { 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, } @@ -145,7 +163,7 @@ function createAdapterFactory(state: GovernanceWidgetAdapterState): GovernanceWi retry: async () => {}, selectHouse: () => {}, register: async () => {}, - restake: async () => {}, + unstake: async () => {}, openVote: () => {}, closeVote: () => {}, setVoteAllocation: () => {}, @@ -217,6 +235,25 @@ export const ActiveCitizenship: Story = { render: () => , } +export const UpcomingVote: Story = { + render: () => ( + + ), +} + export const ActiveAlignmentInjected: Story = { render: () => ( , } -export const RestakeRequired: 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 = { @@ -345,3 +470,24 @@ export const FriendlyContractError: Story = { /> ), } + +export const RealAdapterMockedRuntime: Story = { + render: () => { + const injectedProvider = getInjectedEip1193Provider() + const provider = isInjectedProviderUsable(injectedProvider) + ? injectedProvider + : createCustodialEip1193Provider() + + return ( + + ) + }, +} diff --git a/packages/governance-widget/src/GovernanceOnboardingWidget.tsx b/packages/governance-widget/src/GovernanceOnboardingWidget.tsx index e0272f63..23d67c76 100644 --- a/packages/governance-widget/src/GovernanceOnboardingWidget.tsx +++ b/packages/governance-widget/src/GovernanceOnboardingWidget.tsx @@ -2,17 +2,13 @@ import { useMemo } from 'react' import { PageWizardProvider } from '@goodwidget/ui' import { GovernanceOnboardingFlow } from './onboarding/GovernanceOnboardingFlow' import { DEFAULT_FINAL_ACTIONS, DEFAULT_TRANSACTION_STEPS, ONBOARDING_STEPS } from './onboarding/constants' +import { HOUSE_COPY } from './onboarding/copy' import type { GovernanceOnboardingStepId, GovernanceOnboardingWidgetProps, GovernanceWizardData, } from './types' -/** - * GovernanceOnboardingWidget keeps the five onboarding pages UI-only for now. - * The component owns light/dark-safe visuals, simple local navigation, and a - * presentational state contract that stories and later runtime integrations can drive. - */ export function GovernanceOnboardingWidget({ currentStepId, initialStepId = 'welcome', @@ -22,7 +18,8 @@ export function GovernanceOnboardingWidget({ disabledHouseOptions = [], initialProfileDraft, initialFieldErrors = {}, - stakeAmountLabel = '250 G$', + stakeAmountLabel, + stakeAmountLabels, transactionSteps = DEFAULT_TRANSACTION_STEPS, finalActions = DEFAULT_FINAL_ACTIONS, dataTestId, @@ -39,6 +36,12 @@ export function GovernanceOnboardingWidget({ }), [initialHouse, initialProfileDraft], ) + const resolvedStakeAmountLabels = stakeAmountLabels ?? (stakeAmountLabel + ? { citizenship: stakeAmountLabel, alignment: stakeAmountLabel } + : { + citizenship: HOUSE_COPY.citizenship.defaultStakeAmount, + alignment: HOUSE_COPY.alignment.defaultStakeAmount, + }) return ( + - - GoodDAO Governance - - Browse governance impact, funding distribution, and active Alignment voting. - - + + + + + GoodDAO + {state.address ? ( @@ -57,7 +83,7 @@ function GovernanceHeader({ )} - + ) } @@ -174,7 +200,8 @@ function PendingAlignmentState({ state }: { state: GovernanceWidgetAdapterState Alignment membership pending - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Your House of Alignment application is waiting for committee approval. + Your House of Alignment application is recorded on-chain and is waiting for + committee approval. No further transaction is required while it is pending. Wallet: {state.address ?? 'Not connected'} @@ -184,32 +211,81 @@ function PendingAlignmentState({ state }: { state: GovernanceWidgetAdapterState ) } -function RestakeState({ +function MembershipExitState({ state, actions, }: { state: GovernanceWidgetAdapterState actions: GovernanceWidgetAdapterActions }) { + const transaction = state.transaction.kind === 'unstake' ? state.transaction : null + const isPending = transaction?.status === 'wallet_confirmation' || transaction?.status === 'submitted' + const canSubmit = state.unstakeAvailability.canUnstake && !isPending + return ( - + - Restore governance membership + Membership stake - This member is currently {state.member?.status ?? 'inactive'}. You can restake to rejoin the selected house. + Active governance stakes remain locked for one full term. Once the lock expires, + unstaking returns your G$ and removes your active membership. + + Available from + + {formatMemberDateTime(state.unstakeAvailability.unlockAt)} + + + {!state.unstakeAvailability.canUnstake ? ( + + {state.unstakeAvailability.disabledReason} + + ) : null} + {transaction?.status === 'wallet_confirmation' ? ( + Confirm the unstake transaction in your wallet. + ) : null} + {transaction?.status === 'submitted' ? ( + + Transaction submitted. Waiting for a successful Celo receipt… + + ) : null} + {transaction?.status === 'rejected' || + transaction?.status === 'reverted' || + transaction?.status === 'failed' ? ( + + {transaction.error ?? 'The unstake transaction did not complete.'} + + ) : null} ) } +function RevokedState({ state }: { state: GovernanceWidgetAdapterState }) { + return ( + + + Membership revoked + + This governance membership was revoked and cannot be reactivated from the widget. + Contact the GoodDAO governance team if you believe this status is incorrect. + + + Wallet: {state.address ?? 'Not connected'} + + + + ) +} + function MemberFooter({ state }: { state: GovernanceWidgetAdapterState }) { if (!state.member || !isActiveStatus(state.status)) return null @@ -251,7 +327,8 @@ function GovernanceVoteDetail({ - Placeholder voting detail. Enter allocation basis points; totals must equal 10,000 before voting. + Allocate basis points across the recipients captured when this vote opened. + Your allocation must total exactly 10,000 basis points. {vote.options.map((option) => @@ -279,6 +356,18 @@ function GovernanceVoteDetail({ ) : null} {!canSubmit ? {vote.disabledReason ?? 'Voting is unavailable.'} : null} + {state.transaction.kind === 'vote' && state.transaction.status === 'wallet_confirmation' ? ( + Confirm the vote in your wallet. + ) : null} + {state.transaction.kind === 'vote' && state.transaction.status === 'submitted' ? ( + Vote submitted. Waiting for confirmation… + ) : null} + {state.transaction.kind === 'vote' && state.transaction.status === 'confirmed' ? ( + Vote confirmed on Celo. + ) : null} + {state.transaction.kind === 'vote' && state.transaction.error ? ( + {state.transaction.error} + ) : null} ) @@ -166,37 +165,34 @@ export function GovernanceOnboardingFlow({ selectedHouse={resolvedHouse} profileDraft={profileDraft} fieldErrors={fieldErrors} - stakeAmountLabel={stakeAmountLabel} + stakeAmountLabel={selectedStakeAmountLabel} onProfileFieldChange={updateProfileField} onProfileFieldBlur={handleFieldBlur} ctaDisabled={!profileIsComplete} - // CTA button lives inside the card — no shell footer button needed onContinuePress={handleProfileContinue} /> ) - // Footer is null — "Create Profile and Stake" is inside ProfileStepContent card shellFooter = null break case 'stake': { - // Disable the CTA until every on-chain transaction step has completed. - // L03TJ3 feedback: "I can continue to success while the progress is not finalized?" - const allStepsCompleted = - transactionSteps.length > 0 && - transactionSteps.every((step) => step.status === 'completed') - shellTitle = 'Creating profile & staking' + const allStepsCompleted = areTransactionStepsComplete(transactionSteps) + shellTitle = 'Securing your membership' shellDescription = - 'Please wait while your transaction is confirmed on-chain. You can review each step below.' + 'Transactions are being processed on-chain. Please do not close this window.' shellContent = ( - + ) - shellFooter = ( - - - ) + ) : null break } @@ -205,7 +201,7 @@ export function GovernanceOnboardingFlow({ shellContent = ( ) diff --git a/packages/governance-widget/src/onboarding/HouseSelectionCard.tsx b/packages/governance-widget/src/onboarding/HouseSelectionCard.tsx index 83e36c6c..295f41a1 100644 --- a/packages/governance-widget/src/onboarding/HouseSelectionCard.tsx +++ b/packages/governance-widget/src/onboarding/HouseSelectionCard.tsx @@ -1,18 +1,12 @@ import { Stack } from 'tamagui' -import { Badge, BadgeText, Heading, Icon, PillText, Text, XStack, createComponent } from '@goodwidget/ui' +import { Heading, Icon, PillText, XStack, createComponent } from '@goodwidget/ui' import { HOUSE_COPY } from './copy' import type { GovernanceHouse } from '../types' -/** Maps each house to its Figma-specified icon name. */ const HOUSE_ICON: Record = { citizenship: 'user', alignment: 'compass', } - - -/** - * Internal house-selection button. Uses createComponent to register for theme overrides. - */ const HouseOptionButton = createComponent(Stack, { name: 'GovernanceHouseOptionButton', tag: 'button', @@ -123,18 +117,14 @@ export function HouseSelectionCard({ onPress={onPress} data-testid={`GovernanceOnboardingWidget-house-${house}`} > - {/* ── Header: icon + title + radio (matches Figma layout) ── */} - {houseCopy.title} + {houseCopy.title} - {/* ── Summary text ─────────────────────────────────────────── */} - {houseCopy.summary} - {houseCopy.label} @@ -142,14 +132,7 @@ export function HouseSelectionCard({ {`${stakeAmountLabel} stake`} - {isSelected ? ( - - Selected - - ) : null} - - {/* "Continue with this house" row removed — not in Figma design */} ) } diff --git a/packages/governance-widget/src/onboarding/OnboardingIdentityCard.tsx b/packages/governance-widget/src/onboarding/OnboardingIdentityCard.tsx index 0430b601..45ef3893 100644 --- a/packages/governance-widget/src/onboarding/OnboardingIdentityCard.tsx +++ b/packages/governance-widget/src/onboarding/OnboardingIdentityCard.tsx @@ -124,7 +124,6 @@ export function OnboardingIdentityCard({ - {/* ── CTA button ─────────────────────────────────────────── */} {isVerified ? (