From ec4dcaff604046e1e109ff8a0baec50d25443580 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 19:09:04 +0000
Subject: [PATCH 1/6] Initial plan
From 678b8613fe9df3dba7469e35a7d86c717b6e4e43 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 19:18:51 +0000
Subject: [PATCH 2/6] Add governance widget runtime adapter
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
packages/governance-widget/package.json | 4 +-
.../src/GovernanceOnboardingWidget.tsx | 4 +
.../src/GovernanceWidget.tsx | 359 ++++++++++++
packages/governance-widget/src/adapter.ts | 513 ++++++++++++++++++
packages/governance-widget/src/index.ts | 22 +
.../onboarding/GovernanceOnboardingFlow.tsx | 6 +
.../governance-widget/src/sdks/contracts.ts | 323 +++++++++++
.../governance-widget/src/sdks/identity.ts | 30 +
packages/governance-widget/src/types.ts | 2 +
.../src/widgetRuntimeContract.ts | 106 ++++
pnpm-lock.yaml | 6 +
11 files changed, 1374 insertions(+), 1 deletion(-)
create mode 100644 packages/governance-widget/src/GovernanceWidget.tsx
create mode 100644 packages/governance-widget/src/adapter.ts
create mode 100644 packages/governance-widget/src/sdks/contracts.ts
create mode 100644 packages/governance-widget/src/sdks/identity.ts
create mode 100644 packages/governance-widget/src/widgetRuntimeContract.ts
diff --git a/packages/governance-widget/package.json b/packages/governance-widget/package.json
index dc91ce9f..882176c1 100644
--- a/packages/governance-widget/package.json
+++ b/packages/governance-widget/package.json
@@ -30,10 +30,12 @@
}
},
"dependencies": {
+ "@goodsdks/citizen-sdk": "1.2.5",
"@goodwidget/core": "workspace:*",
"@goodwidget/ui": "workspace:*",
"react-native-svg": "15.15.5",
- "tamagui": "1.121.0"
+ "tamagui": "1.121.0",
+ "viem": "^2.0.0"
},
"devDependencies": {
"@types/react": "^18.3.0",
diff --git a/packages/governance-widget/src/GovernanceOnboardingWidget.tsx b/packages/governance-widget/src/GovernanceOnboardingWidget.tsx
index 088b4b45..a9ce7866 100644
--- a/packages/governance-widget/src/GovernanceOnboardingWidget.tsx
+++ b/packages/governance-widget/src/GovernanceOnboardingWidget.tsx
@@ -26,6 +26,8 @@ export function GovernanceOnboardingWidget({
transactionSteps = DEFAULT_TRANSACTION_STEPS,
finalActions = DEFAULT_FINAL_ACTIONS,
dataTestId,
+ onHouseChange,
+ onProfileSubmit,
onStepChange,
onFinalActionPress,
}: GovernanceOnboardingWidgetProps) {
@@ -53,6 +55,8 @@ export function GovernanceOnboardingWidget({
stakeAmountLabel={stakeAmountLabel}
transactionSteps={transactionSteps}
finalActions={finalActions}
+ onHouseChange={onHouseChange}
+ onProfileSubmit={onProfileSubmit}
onFinalActionPress={onFinalActionPress}
dataTestId={dataTestId}
/>
diff --git a/packages/governance-widget/src/GovernanceWidget.tsx b/packages/governance-widget/src/GovernanceWidget.tsx
new file mode 100644
index 00000000..291e2d5e
--- /dev/null
+++ b/packages/governance-widget/src/GovernanceWidget.tsx
@@ -0,0 +1,359 @@
+import { useMemo } from 'react'
+import { Button, ButtonText, Card, Heading, Input, Spinner, Text, XStack, YStack } from '@goodwidget/ui'
+import { AlignmentVotingProposalCard } from './AlignmentVotingProposalCard'
+import { BalanceCard } from './BalanceCard'
+import { FundingDistributionChart } from './FundingDistributionChart'
+import { GovernanceOnboardingWidget } from './GovernanceOnboardingWidget'
+import { GovernanceWidgetProvider } from './GovernanceWidgetProvider'
+import { ImpactCard } from './ImpactCard'
+import { useGovernanceAdapter } from './adapter'
+import type {
+ GovernanceWidgetAdapterActions,
+ GovernanceWidgetAdapterFactoryInput,
+ GovernanceWidgetAdapterState,
+ GovernanceWidgetProps,
+} from './widgetRuntimeContract'
+import { isActiveStatus } from './adapter'
+
+function formatMemberDate(timestamp: number | null): string {
+ if (!timestamp) return 'Not available'
+ return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(
+ new Date(timestamp),
+ )
+}
+
+function GovernanceHeader({
+ state,
+ actions,
+}: {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}) {
+ const addressLabel = state.address ? `${state.address.slice(0, 6)}…${state.address.slice(-4)}` : null
+
+ return (
+
+
+
+ GoodDAO Governance
+
+ Browse governance impact, funding distribution, and active Alignment voting.
+
+
+ {state.address ? (
+
+
+ Connected wallet
+
+ {addressLabel}
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+function RuntimeNotice({
+ state,
+ actions,
+}: {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}) {
+ if (state.status === 'loading') {
+ return (
+
+
+
+ Loading wallet, identity, membership, and governance data…
+
+
+ )
+ }
+
+ if (state.status === 'unsupported_chain') {
+ return (
+
+
+
+ Switch to Celo Mainnet
+
+
+ GoodDAO Houses are deployed on Celo Mainnet. Switch networks to continue with membership actions.
+
+
+
+
+ )
+ }
+
+ if (state.status === 'friendly_error') {
+ return (
+
+
+
+ Governance data unavailable
+
+ {state.error ?? 'Please try again.'}
+
+
+
+ )
+ }
+
+ return null
+}
+
+function GovernanceDashboard({
+ state,
+ actions,
+}: {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}) {
+ return (
+
+
+
+ actions.openVote()}
+ />
+ {state.dashboard.alignmentVoting.hasVoted ? (
+
+
+ You already voted in this cycle. Ballot updates are not available for this contract version.
+
+
+ ) : null}
+
+
+ )
+}
+
+function PendingAlignmentState({ state }: { state: GovernanceWidgetAdapterState }) {
+ return (
+
+
+ Alignment membership pending
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Your House of Alignment application is waiting for committee approval.
+
+
+ Wallet: {state.address ?? 'Not connected'}
+
+
+
+ )
+}
+
+function RestakeState({
+ state,
+ actions,
+}: {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}) {
+ return (
+
+
+ Restore governance membership
+
+ This member is currently {state.member?.status ?? 'inactive'}. You can restake to rejoin the selected house.
+
+
+
+
+ )
+}
+
+function MemberFooter({ state }: { state: GovernanceWidgetAdapterState }) {
+ if (!state.member || !isActiveStatus(state.status)) return null
+
+ return (
+
+
+
+ House: {state.member.house === 'alignment' ? 'House of Alignment' : 'House of Citizenship'}
+
+
+ Joined: {formatMemberDate(state.member.joinedAt)}
+
+
+ Status: {state.member.status}
+
+
+
+ )
+}
+
+function GovernanceVoteDetail({
+ state,
+ actions,
+}: {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}) {
+ const vote = state.dashboard.alignmentVoting
+ const canSubmit = vote.canVote && vote.allocationTotalBps === 10000 && !vote.hasVoted && vote.isVotingOpen
+
+ return (
+
+
+
+ {vote.title}
+
+
+
+ Placeholder voting detail. Enter allocation basis points; totals must equal 10,000 before voting.
+
+
+ {vote.options.map((option) => (
+ actions.setVoteAllocation(option.id, Number.parseInt(value || '0', 10))}
+ />
+ ))}
+
+
+ Allocation total: {vote.allocationTotalBps} / 10,000 bps
+
+ {vote.hasVoted ? (
+
+ Already voted — this contract does not support ballot replacement.
+
+ ) : null}
+ {!canSubmit ? {vote.disabledReason ?? 'Voting is unavailable.'} : null}
+
+
+
+ )
+}
+
+function GovernanceWidgetContent({
+ adapterFactory,
+ adapterInput,
+ testId,
+}: {
+ adapterFactory?: GovernanceWidgetProps['adapterFactory']
+ adapterInput: GovernanceWidgetAdapterFactoryInput
+ testId?: string
+}) {
+ const defaultAdapter = useGovernanceAdapter(adapterInput)
+ const adapter = adapterFactory ? adapterFactory(adapterInput) : defaultAdapter
+ const { state, actions } = adapter
+ const shouldShowDashboard =
+ state.status === 'disconnected' ||
+ state.status === 'loading' ||
+ state.status === 'unsupported_chain' ||
+ state.status === 'friendly_error' ||
+ isActiveStatus(state.status)
+
+ return (
+
+
+
+ {state.status === 'vote_detail' ? : null}
+ {state.status === 'onboarding_required' ? (
+ {
+ void actions.register(profileDraft)
+ }}
+ />
+ ) : null}
+ {state.status === 'pending_alignment' ? : null}
+ {state.status === 'restake_required' ? : null}
+ {shouldShowDashboard ? : null}
+
+
+ )
+}
+
+export function GovernanceWidget({
+ provider,
+ themeOverrides,
+ config,
+ defaultTheme = 'light',
+ adapterFactory,
+ testId,
+ environment,
+ celoRpcUrl,
+ addresses,
+}: GovernanceWidgetProps) {
+ const adapterInput = useMemo(
+ () => ({ environment, celoRpcUrl, addresses }),
+ [addresses, celoRpcUrl, environment],
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/governance-widget/src/adapter.ts b/packages/governance-widget/src/adapter.ts
new file mode 100644
index 00000000..522d74f7
--- /dev/null
+++ b/packages/governance-widget/src/adapter.ts
@@ -0,0 +1,513 @@
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useWallet } from '@goodwidget/core'
+import type { Address, Hex } from 'viem'
+import type { StepperStepItem } from '@goodwidget/ui'
+import type { GovernanceHouse, GovernanceProfileDraft } from './types'
+import type {
+ GovernanceDashboardState,
+ GovernanceWidgetAdapterActions,
+ GovernanceWidgetAdapterFactoryInput,
+ GovernanceWidgetAdapterResult,
+ GovernanceWidgetAdapterState,
+ GovernanceWidgetStatus,
+ GovernanceVotingState,
+} from './widgetRuntimeContract'
+import {
+ CELO_CHAIN_ID,
+ createGovernancePublicClient,
+ createGovernanceWalletClient,
+ GOODDAO_HOUSES_ABI,
+ houseToContractValue,
+ mapMemberRecord,
+ parseStakeAmountLabelToWei,
+ readGoodIdVerification,
+ readMinimumStakeLabel,
+ registerWithTransferAndCall,
+ requestCeloMainnetSwitch,
+ resolveGovernanceAddresses,
+ type GovernanceMemberRecord,
+} from './sdks/contracts'
+import { createGovernanceIdentitySdk, createGovernanceIdentityVerificationLink } from './sdks/identity'
+
+const IMPACT_METRICS: GovernanceDashboardState['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.',
+}
+
+const DEFAULT_VOTE_OPTIONS = [
+ { id: 'food-chain', label: 'Local Food Chain', percentage: 42 },
+ { id: 'web3-literacy', label: 'Web3 Literacy', percentage: 31 },
+ { id: 'civic-onboarding', label: 'Civic Onboarding', percentage: 27 },
+]
+
+const DEFAULT_FUNDING_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 },
+]
+
+function createTransactionSteps(activeStepId: string, failedMessage?: string): StepperStepItem[] {
+ const stepOrder = ['prepare', 'approve', 'stake', 'finalize']
+ const activeIndex = stepOrder.indexOf(activeStepId)
+
+ return [
+ {
+ id: 'prepare',
+ title: 'Prepare wallet balance',
+ description: 'Keep the required G$ amount available before the staking transaction starts.',
+ status: activeIndex > 0 ? 'completed' : activeIndex === 0 ? 'active' : 'pending',
+ },
+ {
+ id: 'approve',
+ title: 'Approve governance stake',
+ description: 'Confirm the G$ transferAndCall transaction from your wallet.',
+ status: activeIndex > 1 ? 'completed' : activeIndex === 1 ? 'active' : 'pending',
+ },
+ {
+ id: 'stake',
+ title: 'Lock the membership stake',
+ description: failedMessage,
+ status: failedMessage ? 'failed' : activeIndex > 2 ? 'completed' : activeIndex === 2 ? 'active' : 'pending',
+ },
+ {
+ id: 'finalize',
+ title: 'Finalize governance access',
+ status: activeIndex > 3 ? 'completed' : activeIndex === 3 ? 'active' : 'pending',
+ },
+ ]
+}
+
+function createDashboardState(activeMemberCount = 12402): GovernanceDashboardState {
+ const alignmentVoting: GovernanceVotingState = {
+ voteId: 'alignment-current',
+ title: 'Q3 House Of Alignment Funding Allocation',
+ summaryLabel: 'Current top 3 voted',
+ options: DEFAULT_VOTE_OPTIONS,
+ recipients: [],
+ allocationsBps: {
+ 'food-chain': 4200,
+ 'web3-literacy': 3100,
+ 'civic-onboarding': 2700,
+ },
+ allocationTotalBps: 10000,
+ canVote: false,
+ hasVoted: false,
+ isVotingOpen: true,
+ executed: false,
+ disabledReason: 'Connect as an active Alignment member to vote.',
+ }
+
+ return {
+ impact: IMPACT_METRICS,
+ activeMembers: {
+ icon: 'check',
+ title: 'Active Members',
+ amount: activeMemberCount,
+ amountType: 'raw',
+ metadataType: 'time-window',
+ metadata: { label: 'Active members only', tone: 'muted', icon: 'info' },
+ },
+ alignmentVoting,
+ fundingDistribution: {
+ title: 'Funding distribution',
+ centerLabel: 'Mocked pool total',
+ totalAmount: { value: 450000, token: 'G$', isStreaming: true, streamLabel: 'Mock pool data' },
+ projects: DEFAULT_FUNDING_PROJECTS,
+ isStreaming: true,
+ emptyStateLabel: 'No active funding distribution yet.',
+ },
+ }
+}
+
+function createInitialState(status: GovernanceWidgetStatus = 'disconnected'): GovernanceWidgetAdapterState {
+ return {
+ status,
+ address: null,
+ chainId: null,
+ identityStatus: 'unverified',
+ identityVerificationUrl: null,
+ member: null,
+ dashboard: createDashboardState(),
+ selectedHouse: 'citizenship',
+ disabledHouseOptions: ['alignment'],
+ onboardingStepId: undefined,
+ profileDraft: {},
+ stakeAmountLabel: '250 G$',
+ transactionSteps: createTransactionSteps('approve'),
+ registrationHash: null,
+ error: null,
+ }
+}
+
+function friendlyGovernanceError(err: unknown): string {
+ if (!(err instanceof Error)) {
+ return 'Something went wrong. Please try again.'
+ }
+
+ const message = err.message
+ if (message.includes('User rejected') || message.includes('4001')) {
+ return 'Transaction rejected in the wallet.'
+ }
+ if (message.includes('Already voted')) {
+ return 'You already voted in this allocation cycle.'
+ }
+ if (message.includes('insufficient funds')) {
+ return 'Your wallet does not have enough funds for this governance action.'
+ }
+ if (message.includes('whitelist') || message.includes('not allowed')) {
+ return 'This wallet is not currently eligible for House of Alignment registration.'
+ }
+ if (message.includes('revert') || message.includes('reverted')) {
+ return 'The governance contract rejected this action. Review your details and try again.'
+ }
+ if (message.includes('fetch') || message.includes('network') || message.includes('HTTP')) {
+ return 'Unable to reach Celo Mainnet. Check your connection and try again.'
+ }
+
+ return 'Unable to complete the governance action. Please try again.'
+}
+
+function statusFromMember(member: GovernanceMemberRecord | null): GovernanceWidgetStatus {
+ if (!member || member.status === 'none') return 'onboarding_required'
+ if (member.status === 'pending' && member.house === 'alignment') return 'pending_alignment'
+ if (member.status === 'active' && member.house === 'alignment') return 'active_alignment'
+ if (member.status === 'active') return 'active_citizenship'
+ if (member.status === 'revoked' || member.status === 'unstaked') return 'restake_required'
+ return 'onboarding_required'
+}
+
+function isActiveStatus(status: GovernanceWidgetStatus): boolean {
+ return status === 'active_alignment' || status === 'active_citizenship'
+}
+
+export function useGovernanceAdapter({
+ environment = 'production',
+ celoRpcUrl,
+ addresses: addressOverrides,
+}: GovernanceWidgetAdapterFactoryInput = {}): GovernanceWidgetAdapterResult {
+ const { address, chainId, provider, connect } = useWallet()
+ const [state, setState] = useState(() => createInitialState())
+ const publicClient = useMemo(() => createGovernancePublicClient(celoRpcUrl), [celoRpcUrl])
+ const addresses = useMemo(() => resolveGovernanceAddresses(addressOverrides), [addressOverrides])
+ const account = address as Address | null
+
+ const loadRuntimeState = useCallback(async () => {
+ if (!account) {
+ setState((previous) => ({ ...previous, ...createInitialState('disconnected'), chainId: chainId ?? null }))
+ return
+ }
+
+ if (chainId !== CELO_CHAIN_ID) {
+ setState((previous) => ({
+ ...previous,
+ status: 'unsupported_chain',
+ address: account,
+ chainId: chainId ?? null,
+ error: null,
+ }))
+ return
+ }
+
+ if (!addresses.houses) {
+ setState((previous) => ({
+ ...previous,
+ status: 'friendly_error',
+ address: account,
+ chainId,
+ error: 'Governance contract address is not configured yet.',
+ }))
+ return
+ }
+
+ setState((previous) => ({ ...previous, status: 'loading', address: account, chainId, error: null }))
+
+ try {
+ const [memberResult, citizenshipStake, alignmentStake, citizenshipMembers, alignmentMembers, isVerified] =
+ await Promise.all([
+ publicClient.readContract({
+ address: addresses.houses,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'getMember',
+ args: [account],
+ }),
+ readMinimumStakeLabel(publicClient, addresses.houses, 'citizenship'),
+ readMinimumStakeLabel(publicClient, addresses.houses, 'alignment'),
+ publicClient.readContract({
+ address: addresses.houses,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'getActiveMembers',
+ args: [houseToContractValue('citizenship')],
+ }),
+ publicClient.readContract({
+ address: addresses.houses,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'getActiveMembers',
+ args: [houseToContractValue('alignment')],
+ }),
+ readGoodIdVerification(publicClient, addresses.goodId, account),
+ ])
+
+ const member = mapMemberRecord(memberResult)
+ const status = statusFromMember(member)
+ const activeMemberCount = citizenshipMembers.length + alignmentMembers.length
+ const dashboard = createDashboardState(activeMemberCount)
+ const selectedHouse = member.house ?? 'citizenship'
+ dashboard.alignmentVoting.canVote = status === 'active_alignment'
+ dashboard.alignmentVoting.disabledReason = dashboard.alignmentVoting.canVote
+ ? undefined
+ : 'Only active House of Alignment members can vote.'
+
+ setState((previous) => ({
+ ...previous,
+ status,
+ address: account,
+ chainId,
+ identityStatus: isVerified ? 'verified' : 'unverified',
+ member,
+ dashboard,
+ selectedHouse,
+ disabledHouseOptions: isVerified ? [] : ['alignment'],
+ stakeAmountLabel: selectedHouse === 'alignment' ? alignmentStake.label : citizenshipStake.label,
+ error: null,
+ }))
+ } catch (err: unknown) {
+ setState((previous) => ({
+ ...previous,
+ status: 'friendly_error',
+ address: account,
+ chainId,
+ error: friendlyGovernanceError(err),
+ }))
+ }
+ }, [account, addresses, chainId, publicClient])
+
+ useEffect(() => {
+ void loadRuntimeState()
+ }, [loadRuntimeState])
+
+ const switchToCelo = useCallback(async () => {
+ try {
+ await requestCeloMainnetSwitch(provider)
+ } finally {
+ await loadRuntimeState()
+ }
+ }, [loadRuntimeState, provider])
+
+ const selectHouse = useCallback(
+ (house: GovernanceHouse) => {
+ setState((previous) => ({
+ ...previous,
+ selectedHouse: house,
+ stakeAmountLabel: house === 'alignment' ? previous.stakeAmountLabel : previous.stakeAmountLabel,
+ }))
+ },
+ [],
+ )
+
+ const register = useCallback(
+ async (profileDraft: GovernanceProfileDraft) => {
+ if (!account || !addresses.houses) return
+ const walletClient = createGovernanceWalletClient({ provider, account })
+ if (!walletClient) return
+
+ setState((previous) => ({
+ ...previous,
+ onboardingStepId: 'stake',
+ profileDraft,
+ transactionSteps: createTransactionSteps('approve'),
+ error: null,
+ }))
+
+ try {
+ const hash = await registerWithTransferAndCall({
+ walletClient,
+ account,
+ addresses: { ...addresses, houses: addresses.houses },
+ selectedHouse: state.selectedHouse,
+ profileDraft,
+ stakeAmountWei: parseStakeAmountLabelToWei(state.stakeAmountLabel),
+ })
+
+ setState((previous) => ({
+ ...previous,
+ registrationHash: hash,
+ transactionSteps: createTransactionSteps('finalize').map((step) => ({ ...step, status: 'completed' })),
+ onboardingStepId: 'success',
+ }))
+ await loadRuntimeState()
+ } catch (err: unknown) {
+ setState((previous) => ({
+ ...previous,
+ status: 'onboarding_required',
+ error: friendlyGovernanceError(err),
+ transactionSteps: createTransactionSteps('stake', friendlyGovernanceError(err)),
+ }))
+ }
+ },
+ [account, addresses, loadRuntimeState, provider, state.selectedHouse, state.stakeAmountLabel],
+ )
+
+ const restake = useCallback(async () => {
+ if (!account || !addresses.houses) return
+ const walletClient = createGovernanceWalletClient({ provider, account })
+ if (!walletClient) return
+
+ try {
+ const hash = await walletClient.writeContract({
+ account,
+ chain: undefined,
+ address: addresses.houses,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'stake',
+ args: [parseStakeAmountLabelToWei(state.stakeAmountLabel)],
+ })
+ setState((previous) => ({ ...previous, registrationHash: hash, error: null }))
+ await loadRuntimeState()
+ } catch (err: unknown) {
+ setState((previous) => ({ ...previous, status: 'friendly_error', error: friendlyGovernanceError(err) }))
+ }
+ }, [account, addresses.houses, loadRuntimeState, provider, state.stakeAmountLabel])
+
+ const openVote = useCallback(() => {
+ setState((previous) => ({ ...previous, status: 'vote_detail' }))
+ }, [])
+
+ const closeVote = useCallback(() => {
+ setState((previous) => ({
+ ...previous,
+ status: previous.member ? statusFromMember(previous.member) : 'disconnected',
+ }))
+ }, [])
+
+ const setVoteAllocation = useCallback((recipientId: string, basisPoints: number) => {
+ setState((previous) => {
+ const allocationsBps = {
+ ...previous.dashboard.alignmentVoting.allocationsBps,
+ [recipientId]: Math.max(0, Math.min(10000, basisPoints)),
+ }
+ const allocationTotalBps = Object.values(allocationsBps).reduce((total, amount) => total + amount, 0)
+ return {
+ ...previous,
+ dashboard: {
+ ...previous.dashboard,
+ alignmentVoting: {
+ ...previous.dashboard.alignmentVoting,
+ allocationsBps,
+ allocationTotalBps,
+ },
+ },
+ }
+ })
+ }, [])
+
+ const submitVote = useCallback(async () => {
+ if (!account || !addresses.houses || state.dashboard.alignmentVoting.allocationTotalBps !== 10000) return
+ const walletClient = createGovernanceWalletClient({ provider, account })
+ if (!walletClient) return
+
+ try {
+ const recipients = state.dashboard.alignmentVoting.options.map((option) => option.id as Address)
+ const allocations = state.dashboard.alignmentVoting.options.map(
+ (option) => BigInt(state.dashboard.alignmentVoting.allocationsBps[option.id] ?? 0),
+ )
+ await walletClient.writeContract({
+ account,
+ chain: undefined,
+ address: addresses.houses,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'castVote',
+ args: [recipients, allocations],
+ })
+ setState((previous) => ({
+ ...previous,
+ dashboard: {
+ ...previous.dashboard,
+ alignmentVoting: { ...previous.dashboard.alignmentVoting, hasVoted: true, canVote: false },
+ },
+ }))
+ } catch (err: unknown) {
+ setState((previous) => ({ ...previous, status: 'friendly_error', error: friendlyGovernanceError(err) }))
+ }
+ }, [account, addresses.houses, provider, state.dashboard.alignmentVoting])
+
+ const startIdentityVerification = useCallback(async () => {
+ if (!account) return
+ const walletClient = createGovernanceWalletClient({ provider, account })
+ if (!walletClient) return
+
+ const identitySdk = createGovernanceIdentitySdk({
+ publicClient,
+ walletClient,
+ environment,
+ })
+ const returnUrl = typeof window !== 'undefined' ? window.location.href : undefined
+ const identityVerificationUrl = await createGovernanceIdentityVerificationLink({
+ identitySdk,
+ returnUrl,
+ chainId: chainId ?? undefined,
+ })
+ setState((previous) => ({ ...previous, identityVerificationUrl }))
+ if (typeof window !== 'undefined') {
+ window.open(identityVerificationUrl, '_blank', 'noopener,noreferrer')
+ }
+ }, [account, chainId, environment, provider, publicClient])
+
+ const actions = useMemo(
+ () => ({
+ connect,
+ switchToCelo,
+ refresh: loadRuntimeState,
+ retry: loadRuntimeState,
+ selectHouse,
+ register,
+ restake,
+ openVote,
+ closeVote,
+ setVoteAllocation,
+ submitVote,
+ startIdentityVerification,
+ }),
+ [
+ closeVote,
+ connect,
+ loadRuntimeState,
+ openVote,
+ register,
+ restake,
+ selectHouse,
+ setVoteAllocation,
+ startIdentityVerification,
+ submitVote,
+ switchToCelo,
+ ],
+ )
+
+ const resolvedState = useMemo(
+ () => ({
+ ...state,
+ address: account,
+ chainId: chainId ?? null,
+ dashboard: {
+ ...state.dashboard,
+ impact: {
+ ...state.dashboard.impact,
+ ctaLabel: 'View Impact Report Q3',
+ },
+ },
+ }),
+ [account, chainId, state],
+ )
+
+ return { state: resolvedState, actions }
+}
+
+export { createDashboardState, createInitialState, friendlyGovernanceError, isActiveStatus }
diff --git a/packages/governance-widget/src/index.ts b/packages/governance-widget/src/index.ts
index 141a7b1e..fc6d98d2 100644
--- a/packages/governance-widget/src/index.ts
+++ b/packages/governance-widget/src/index.ts
@@ -5,6 +5,7 @@ export { OptimisticVotingProposalCard } from './OptimisticVotingProposalCard'
export { FundingDistributionChart } from './FundingDistributionChart'
export { GovernanceWidgetProvider } from './GovernanceWidgetProvider'
export { GovernanceOnboardingWidget } from './GovernanceOnboardingWidget'
+export { GovernanceWidget } from './GovernanceWidget'
export type { GovernanceWidgetProviderProps } from './types'
export type {
GovernanceAmount,
@@ -35,3 +36,24 @@ export type {
} from './types'
export { DEFAULT_TRANSACTION_STEPS } from './onboarding/constants'
+export { useGovernanceAdapter } from './adapter'
+export type {
+ GovernanceDashboardState,
+ GovernanceVotingState,
+ GovernanceWidgetAdapterActions,
+ GovernanceWidgetAdapterFactory,
+ GovernanceWidgetAdapterFactoryInput,
+ GovernanceWidgetAdapterResult,
+ GovernanceWidgetAdapterState,
+ GovernanceWidgetProps,
+ GovernanceWidgetStatus,
+} from './widgetRuntimeContract'
+export {
+ CELO_CHAIN_ID,
+ CELO_GOODID_ADDRESS,
+ DEFAULT_CELO_RPC_URL,
+ G_TOKEN_CELO_ADDRESS,
+ MOCK_FLOW_SPLITTER_POOL_ADDRESS,
+ encodeGovernanceRegistrationData,
+ resolveGovernanceAddresses,
+} from './sdks/contracts'
diff --git a/packages/governance-widget/src/onboarding/GovernanceOnboardingFlow.tsx b/packages/governance-widget/src/onboarding/GovernanceOnboardingFlow.tsx
index 1bd80102..83535685 100644
--- a/packages/governance-widget/src/onboarding/GovernanceOnboardingFlow.tsx
+++ b/packages/governance-widget/src/onboarding/GovernanceOnboardingFlow.tsx
@@ -28,6 +28,8 @@ interface GovernanceOnboardingFlowProps {
stakeAmountLabel: string
transactionSteps: StepperStepItem[]
finalActions: GovernanceOnboardingAction[]
+ onHouseChange?: (house: GovernanceHouse) => void
+ onProfileSubmit?: (profileDraft: GovernanceWizardData['profileDraft'], house: GovernanceHouse) => void
onFinalActionPress?: (actionId: string) => void
dataTestId?: string
}
@@ -40,6 +42,8 @@ export function GovernanceOnboardingFlow({
stakeAmountLabel,
transactionSteps = DEFAULT_TRANSACTION_STEPS,
finalActions = DEFAULT_FINAL_ACTIONS,
+ onHouseChange,
+ onProfileSubmit,
onFinalActionPress,
dataTestId,
}: GovernanceOnboardingFlowProps) {
@@ -95,12 +99,14 @@ export function GovernanceOnboardingFlow({
setFieldErrors(nextFieldErrors)
if (Object.keys(nextFieldErrors).length === 0) {
+ onProfileSubmit?.(profileDraft, resolvedHouse)
next()
}
}
const handleHouseSelect = (nextHouse: GovernanceHouse) => {
setData({ selectedHouse: nextHouse })
+ onHouseChange?.(nextHouse)
}
let shellTitle = 'Governance onboarding'
diff --git a/packages/governance-widget/src/sdks/contracts.ts b/packages/governance-widget/src/sdks/contracts.ts
new file mode 100644
index 00000000..dddea0c0
--- /dev/null
+++ b/packages/governance-widget/src/sdks/contracts.ts
@@ -0,0 +1,323 @@
+import {
+ createPublicClient,
+ createWalletClient,
+ custom,
+ encodeAbiParameters,
+ formatUnits,
+ http,
+ parseAbi,
+ parseUnits,
+ type Address,
+ type Chain,
+ type EIP1193Provider as ViemEip1193Provider,
+ type Hex,
+ type PublicClient,
+ type WalletClient,
+} from 'viem'
+
+import type { EIP1193Provider } from '@goodwidget/core'
+import type { GovernanceHouse, GovernanceProfileDraft } from '../types'
+
+export const CELO_CHAIN_ID = 42220
+export const DEFAULT_CELO_RPC_URL = 'https://forno.celo.org'
+export const G_TOKEN_CELO_ADDRESS: Address = '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A'
+export const CELO_GOODID_ADDRESS: Address = '0xC361A6E67822a0EDc17D899227dd9FC50BD62F42'
+export const MOCK_FLOW_SPLITTER_POOL_ADDRESS: Address = '0x0000000000000000000000000000000000000001'
+
+export const CELO_CHAIN: Chain = {
+ id: CELO_CHAIN_ID,
+ name: 'Celo',
+ nativeCurrency: { name: 'Celo', symbol: 'CELO', decimals: 18 },
+ rpcUrls: {
+ default: { http: [DEFAULT_CELO_RPC_URL] },
+ public: { http: [DEFAULT_CELO_RPC_URL] },
+ },
+}
+
+export const GOODDAO_HOUSES_ABI = parseAbi([
+ 'function minimumStake(uint8 house) view returns (uint256)',
+ 'function getMember(address account) view returns (uint8 house, uint8 status, uint256 stakedAmount, uint256 joinedAt, uint256 updatedAt, uint256 unstakedAt, string name, string socialLinks, string projectWebpage, string missionStatement, string distributionStrategy)',
+ 'function getActiveMembers(uint8 house) view returns (address[])',
+ 'function cycleStartTime() view returns (uint256)',
+ 'function termDuration() view returns (uint256)',
+ 'function votingTermLength() view returns (uint256)',
+ 'function isVotingPeriod() view returns (bool)',
+ 'function getCurrentVoteId() view returns (uint256)',
+ 'function getVoteConfig(uint256 voteId) view returns (uint256 startTime, uint256 endTime, 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 pool, uint256 poolId)',
+ 'function registerAndStake(uint8 house, string name, string socialLinks, string projectWebpage, string missionStatement, string distributionStrategy)',
+ 'function stake(uint256 amount)',
+ 'function castVote(address[] recipients, uint256[] allocations)',
+ 'function unstake()',
+])
+
+export const FLOW_SPLITTER_ABI = parseAbi([
+ 'function getPoolById(uint256 poolId) view returns ((uint256 id, address poolAddress, address token, string metadata, bytes32 adminRole))',
+ 'function getPoolByAdminRole(bytes32 adminRole) view returns ((uint256 id, address poolAddress, address token, string metadata, bytes32 adminRole))',
+ 'function getPoolNameById(uint256 poolId) view returns (string)',
+ 'function getPoolSymbolById(uint256 poolId) view returns (string)',
+ 'function isPoolAdmin(uint256 poolId, address account) view returns (bool)',
+])
+
+export const G_TOKEN_ABI = parseAbi([
+ 'function balanceOf(address account) view returns (uint256)',
+ 'function decimals() view returns (uint8)',
+ 'function transferAndCall(address to, uint256 value, bytes data) returns (bool)',
+])
+
+export const GOODID_ABI = parseAbi([
+ 'function getWhitelistedRoot(address account) view returns (address)',
+])
+
+export type GovernanceMemberStatus = 'none' | 'pending' | 'active' | 'revoked' | 'unstaked'
+
+export interface GovernanceMemberRecord {
+ house: GovernanceHouse
+ status: GovernanceMemberStatus
+ stakedAmount: bigint
+ joinedAt: number | null
+ updatedAt: number | null
+ unstakedAt: number | null
+ name: string
+ socialLinks: string
+ projectWebpage: string
+ missionStatement: string
+ distributionStrategy: string
+}
+
+export interface GovernanceContractAddresses {
+ houses?: Address
+ gToken: Address
+ goodId: Address
+ flowSplitterPool: Address
+}
+
+export interface GovernanceClientOptions {
+ provider?: EIP1193Provider | null
+ account?: Address | null
+ rpcUrl?: string
+}
+
+export interface GovernanceEnvironmentConfig {
+ housesAddress?: Address
+ gTokenAddress?: Address
+ goodIdAddress?: Address
+ flowSplitterPoolAddress?: Address
+}
+
+const HOUSE_TO_CONTRACT: Record = {
+ citizenship: 0,
+ alignment: 1,
+}
+
+const CONTRACT_TO_HOUSE: Record = {
+ 0: 'citizenship',
+ 1: 'alignment',
+}
+
+const CONTRACT_TO_STATUS: Record = {
+ 0: 'none',
+ 1: 'pending',
+ 2: 'active',
+ 3: 'revoked',
+ 4: 'unstaked',
+}
+
+type PublicRuntimeEnv = Record
+
+type RuntimeGlobal = typeof globalThis & {
+ process?: { env?: PublicRuntimeEnv }
+ __GOODWIDGET_GOVERNANCE__?: GovernanceEnvironmentConfig
+}
+
+function readConfiguredAddress(key: keyof GovernanceEnvironmentConfig, envNames: string[]): Address | undefined {
+ const runtimeGlobal = globalThis as RuntimeGlobal
+ const explicitAddress = runtimeGlobal.__GOODWIDGET_GOVERNANCE__?.[key]
+ if (explicitAddress) return explicitAddress
+
+ const publicEnv = runtimeGlobal.process?.env
+ const configuredValue = envNames.map((envName) => publicEnv?.[envName]).find(Boolean)
+ return configuredValue as Address | undefined
+}
+
+export function resolveGovernanceAddresses(
+ overrides: GovernanceEnvironmentConfig = {},
+): GovernanceContractAddresses {
+ return {
+ houses:
+ overrides.housesAddress ??
+ readConfiguredAddress('housesAddress', [
+ 'GOODWIDGET_GOVERNANCE_HOUSES_ADDRESS',
+ 'VITE_GOODWIDGET_GOVERNANCE_HOUSES_ADDRESS',
+ 'NEXT_PUBLIC_GOODWIDGET_GOVERNANCE_HOUSES_ADDRESS',
+ ]),
+ gToken: overrides.gTokenAddress ?? G_TOKEN_CELO_ADDRESS,
+ goodId: overrides.goodIdAddress ?? CELO_GOODID_ADDRESS,
+ flowSplitterPool:
+ overrides.flowSplitterPoolAddress ??
+ readConfiguredAddress('flowSplitterPoolAddress', [
+ 'GOODWIDGET_GOVERNANCE_FLOW_SPLITTER_POOL_ADDRESS',
+ 'VITE_GOODWIDGET_GOVERNANCE_FLOW_SPLITTER_POOL_ADDRESS',
+ 'NEXT_PUBLIC_GOODWIDGET_GOVERNANCE_FLOW_SPLITTER_POOL_ADDRESS',
+ ]) ??
+ MOCK_FLOW_SPLITTER_POOL_ADDRESS,
+ }
+}
+
+export function createGovernancePublicClient(rpcUrl = DEFAULT_CELO_RPC_URL): PublicClient {
+ return createPublicClient({ chain: CELO_CHAIN, transport: http(rpcUrl) })
+}
+
+export function createGovernanceWalletClient({
+ provider,
+ account,
+}: GovernanceClientOptions): WalletClient | null {
+ if (!provider || !account) return null
+
+ return createWalletClient({
+ account,
+ chain: CELO_CHAIN,
+ transport: custom(provider as ViemEip1193Provider),
+ })
+}
+
+export function houseToContractValue(house: GovernanceHouse): 0 | 1 {
+ return HOUSE_TO_CONTRACT[house]
+}
+
+export function encodeGovernanceRegistrationData(
+ house: GovernanceHouse,
+ profileDraft: GovernanceProfileDraft,
+): Hex {
+ return encodeAbiParameters(
+ [
+ { type: 'uint8' },
+ { type: 'string' },
+ { type: 'string' },
+ { type: 'string' },
+ { type: 'string' },
+ { type: 'string' },
+ ],
+ [
+ houseToContractValue(house),
+ profileDraft.name ?? '',
+ profileDraft.socialLinks ?? '',
+ profileDraft.projectWebpage ?? '',
+ profileDraft.missionStatement ?? '',
+ profileDraft.distributionStrategy ?? '',
+ ],
+ )
+}
+
+function timestampFromSeconds(value: bigint): number | null {
+ return value > 0n ? Number(value) * 1000 : null
+}
+
+export function mapMemberRecord(rawMember: readonly unknown[]): GovernanceMemberRecord {
+ const houseValue = Number(rawMember[0] ?? 0)
+ const statusValue = Number(rawMember[1] ?? 0)
+
+ return {
+ house: CONTRACT_TO_HOUSE[houseValue] ?? 'citizenship',
+ status: CONTRACT_TO_STATUS[statusValue] ?? 'none',
+ stakedAmount: BigInt(String(rawMember[2] ?? 0)),
+ joinedAt: timestampFromSeconds(BigInt(String(rawMember[3] ?? 0))),
+ updatedAt: timestampFromSeconds(BigInt(String(rawMember[4] ?? 0))),
+ unstakedAt: timestampFromSeconds(BigInt(String(rawMember[5] ?? 0))),
+ name: String(rawMember[6] ?? ''),
+ socialLinks: String(rawMember[7] ?? ''),
+ projectWebpage: String(rawMember[8] ?? ''),
+ missionStatement: String(rawMember[9] ?? ''),
+ distributionStrategy: String(rawMember[10] ?? ''),
+ }
+}
+
+export async function readMinimumStakeLabel(
+ publicClient: PublicClient,
+ housesAddress: Address,
+ house: GovernanceHouse,
+): Promise<{ amountWei: bigint; label: string }> {
+ const amountWei = await publicClient.readContract({
+ address: housesAddress,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'minimumStake',
+ args: [houseToContractValue(house)],
+ })
+
+ return { amountWei, label: `${formatUnits(amountWei, 18)} G$` }
+}
+
+export async function readGoodIdVerification(
+ publicClient: PublicClient,
+ goodIdAddress: Address,
+ account: Address,
+): Promise {
+ const root = await publicClient.readContract({
+ address: goodIdAddress,
+ abi: GOODID_ABI,
+ functionName: 'getWhitelistedRoot',
+ args: [account],
+ })
+ return root.toLowerCase() !== '0x0000000000000000000000000000000000000000'
+}
+
+export async function registerWithTransferAndCall(params: {
+ walletClient: WalletClient
+ account: Address
+ addresses: GovernanceContractAddresses & { houses: Address }
+ selectedHouse: GovernanceHouse
+ profileDraft: GovernanceProfileDraft
+ stakeAmountWei: bigint
+}): Promise {
+ const registrationData = encodeGovernanceRegistrationData(params.selectedHouse, params.profileDraft)
+
+ return params.walletClient.writeContract({
+ account: params.account,
+ chain: CELO_CHAIN,
+ address: params.addresses.gToken,
+ abi: G_TOKEN_ABI,
+ functionName: 'transferAndCall',
+ args: [params.addresses.houses, params.stakeAmountWei, registrationData],
+ })
+}
+
+export async function registerAndStakeFallback(params: {
+ walletClient: WalletClient
+ account: Address
+ housesAddress: Address
+ selectedHouse: GovernanceHouse
+ profileDraft: GovernanceProfileDraft
+}): Promise {
+ return params.walletClient.writeContract({
+ account: params.account,
+ chain: CELO_CHAIN,
+ address: params.housesAddress,
+ abi: GOODDAO_HOUSES_ABI,
+ functionName: 'registerAndStake',
+ args: [
+ houseToContractValue(params.selectedHouse),
+ params.profileDraft.name ?? '',
+ params.profileDraft.socialLinks ?? '',
+ params.profileDraft.projectWebpage ?? '',
+ params.profileDraft.missionStatement ?? '',
+ params.profileDraft.distributionStrategy ?? '',
+ ],
+ })
+}
+
+export async function requestCeloMainnetSwitch(provider: EIP1193Provider | null): Promise {
+ if (!provider) return
+
+ await provider.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: `0x${CELO_CHAIN_ID.toString(16)}` }],
+ })
+}
+
+export function parseStakeAmountLabelToWei(stakeAmountLabel: string): bigint {
+ const numericValue = stakeAmountLabel.replace(/[^0-9.]/g, '') || '0'
+ return parseUnits(numericValue, 18)
+}
diff --git a/packages/governance-widget/src/sdks/identity.ts b/packages/governance-widget/src/sdks/identity.ts
new file mode 100644
index 00000000..6df5a0b1
--- /dev/null
+++ b/packages/governance-widget/src/sdks/identity.ts
@@ -0,0 +1,30 @@
+import { IdentitySDK } from '@goodsdks/citizen-sdk'
+import type { PublicClient, WalletClient } from 'viem'
+
+export type GovernanceIdentityEnvironment = 'production' | 'staging' | 'development'
+
+export interface GovernanceIdentitySdkInput {
+ publicClient: PublicClient
+ walletClient: WalletClient
+ environment: GovernanceIdentityEnvironment
+}
+
+export function createGovernanceIdentitySdk({
+ publicClient,
+ walletClient,
+ environment,
+}: GovernanceIdentitySdkInput): IdentitySDK {
+ return new IdentitySDK({ publicClient, walletClient, env: environment })
+}
+
+export async function createGovernanceIdentityVerificationLink({
+ identitySdk,
+ returnUrl,
+ chainId,
+}: {
+ identitySdk: IdentitySDK
+ returnUrl?: string
+ chainId?: number
+}): Promise {
+ return identitySdk.generateFVLink(false, returnUrl, chainId)
+}
diff --git a/packages/governance-widget/src/types.ts b/packages/governance-widget/src/types.ts
index ec010f84..3a4bbe15 100644
--- a/packages/governance-widget/src/types.ts
+++ b/packages/governance-widget/src/types.ts
@@ -58,6 +58,8 @@ export interface GovernanceOnboardingWidgetProps {
transactionSteps?: StepperStepItem[]
finalActions?: GovernanceOnboardingAction[]
dataTestId?: string
+ onHouseChange?: (house: GovernanceHouse) => void
+ onProfileSubmit?: (profileDraft: GovernanceProfileDraft, house: GovernanceHouse) => void
onStepChange?: (stepId: GovernanceOnboardingStepId) => void
onFinalActionPress?: (actionId: string) => void
}
diff --git a/packages/governance-widget/src/widgetRuntimeContract.ts b/packages/governance-widget/src/widgetRuntimeContract.ts
new file mode 100644
index 00000000..909d1091
--- /dev/null
+++ b/packages/governance-widget/src/widgetRuntimeContract.ts
@@ -0,0 +1,106 @@
+import type { Address, Hex } from 'viem'
+import type { StepperStepItem } from '@goodwidget/ui'
+import type { EIP1193Provider, GoodWidgetConfig, GoodWidgetThemeOverrides } from '@goodwidget/core'
+import type {
+ BalanceCardProps,
+ FundingDistributionChartProps,
+ GovernanceHouse,
+ GovernanceIdentityStatus,
+ GovernanceOnboardingStepId,
+ GovernanceProfileDraft,
+ ImpactCardProps,
+ RankedVotingOption,
+} from './types'
+import type { GovernanceEnvironmentConfig, GovernanceMemberRecord } from './sdks/contracts'
+import type { GovernanceIdentityEnvironment } from './sdks/identity'
+
+export type GovernanceWidgetStatus =
+ | 'disconnected'
+ | 'loading'
+ | 'unsupported_chain'
+ | 'onboarding_required'
+ | 'pending_alignment'
+ | 'active_citizenship'
+ | 'active_alignment'
+ | 'restake_required'
+ | 'vote_detail'
+ | 'friendly_error'
+
+export interface GovernanceVotingState {
+ voteId: string
+ title: string
+ summaryLabel: string
+ options: RankedVotingOption[]
+ recipients: Address[]
+ allocationsBps: Record
+ allocationTotalBps: number
+ canVote: boolean
+ hasVoted: boolean
+ isVotingOpen: boolean
+ executed: boolean
+ disabledReason?: string
+}
+
+export interface GovernanceDashboardState {
+ impact: ImpactCardProps
+ activeMembers: BalanceCardProps
+ alignmentVoting: GovernanceVotingState
+ fundingDistribution: FundingDistributionChartProps
+}
+
+export interface GovernanceWidgetAdapterState {
+ status: GovernanceWidgetStatus
+ address: Address | null
+ chainId: number | null
+ identityStatus: GovernanceIdentityStatus
+ identityVerificationUrl: string | null
+ member: GovernanceMemberRecord | null
+ dashboard: GovernanceDashboardState
+ selectedHouse: GovernanceHouse
+ disabledHouseOptions: GovernanceHouse[]
+ onboardingStepId?: GovernanceOnboardingStepId
+ profileDraft: GovernanceProfileDraft
+ stakeAmountLabel: string
+ transactionSteps: StepperStepItem[]
+ registrationHash: Hex | null
+ error: string | null
+}
+
+export interface GovernanceWidgetAdapterActions {
+ connect: () => Promise
+ switchToCelo: () => Promise
+ refresh: () => Promise
+ retry: () => Promise
+ selectHouse: (house: GovernanceHouse) => void
+ register: (profileDraft: GovernanceProfileDraft) => Promise
+ restake: () => Promise
+ openVote: () => void
+ closeVote: () => void
+ setVoteAllocation: (recipientId: string, basisPoints: number) => void
+ submitVote: () => Promise
+ startIdentityVerification: () => Promise
+}
+
+export interface GovernanceWidgetAdapterResult {
+ state: GovernanceWidgetAdapterState
+ actions: GovernanceWidgetAdapterActions
+}
+
+export interface GovernanceWidgetAdapterFactoryInput {
+ environment?: GovernanceIdentityEnvironment
+ celoRpcUrl?: string
+ addresses?: GovernanceEnvironmentConfig
+}
+
+export type GovernanceWidgetAdapterFactory = (
+ input: GovernanceWidgetAdapterFactoryInput,
+) => GovernanceWidgetAdapterResult
+
+export interface GovernanceWidgetProps extends GovernanceWidgetAdapterFactoryInput {
+ provider?: EIP1193Provider
+ themeOverrides?: GoodWidgetThemeOverrides
+ config?: GoodWidgetConfig
+ defaultTheme?: 'light' | 'dark'
+ adapterFactory?: GovernanceWidgetAdapterFactory
+ testId?: string
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 979627a2..d1ca3171 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -475,6 +475,9 @@ importers:
packages/governance-widget:
dependencies:
+ '@goodsdks/citizen-sdk':
+ specifier: 1.2.5
+ version: 1.2.5(@swc/core@1.15.30)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11))(wagmi@3.6.8(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)))(yaml@2.8.3)
'@goodwidget/core':
specifier: workspace:*
version: link:../core
@@ -490,6 +493,9 @@ importers:
tamagui:
specifier: 1.121.0
version: 1.121.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)
+ viem:
+ specifier: ^2.0.0
+ version: 2.48.4(typescript@5.9.3)(zod@4.1.11)
devDependencies:
'@types/react':
specifier: ^18.3.0
From 4fdcabd14ecd63dd7f6e271e5fdc34f310dea039 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 19:21:49 +0000
Subject: [PATCH 3/6] Add governance runtime QA stories
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
.../GovernanceRuntime.stories.tsx | 339 ++++++++++++++++++
packages/governance-widget/src/adapter.ts | 2 +-
.../widgets/governance-widget/runtime.spec.ts | 150 ++++++++
3 files changed, 490 insertions(+), 1 deletion(-)
create mode 100644 examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx
create mode 100644 tests/widgets/governance-widget/runtime.spec.ts
diff --git a/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx b/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx
new file mode 100644
index 00000000..939c67af
--- /dev/null
+++ b/examples/storybook/src/stories/governance-widget/GovernanceRuntime.stories.tsx
@@ -0,0 +1,339 @@
+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
+
+function createDashboard(overrides: Partial = {}) {
+ 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: 'food-chain', label: 'Local Food Chain', percentage: 42 },
+ { id: 'web3-literacy', label: 'Web3 Literacy', percentage: 31 },
+ { id: 'civic-onboarding', label: 'Civic Onboarding', percentage: 27 },
+ ],
+ recipients: [],
+ allocationsBps: { 'food-chain': 4200, 'web3-literacy': 3100, 'civic-onboarding': 2700 },
+ allocationTotalBps: 10000,
+ canVote: false,
+ hasVoted: false,
+ isVotingOpen: true,
+ executed: false,
+ 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 =
+ status === 'active_citizenship' || status === 'active_alignment' || status === 'restake_required'
+ ? {
+ house: status === 'active_alignment' ? 'alignment' : 'citizenship',
+ status: status === 'restake_required' ? 'unstaked' : '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,
+ 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',
+ disabledHouseOptions: status === 'onboarding_required' ? ['alignment'] : [],
+ onboardingStepId: undefined,
+ profileDraft: {},
+ stakeAmountLabel: '250 G$',
+ 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,
+ error: null,
+ ...overrides,
+ }
+}
+
+function createAdapterFactory(state: GovernanceWidgetAdapterState): GovernanceWidgetAdapterFactory {
+ return () => ({
+ state,
+ actions: {
+ connect: async () => {},
+ switchToCelo: async () => {},
+ refresh: async () => {},
+ retry: async () => {},
+ selectHouse: () => {},
+ register: async () => {},
+ restake: 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 OnboardingRequiredHoaUnavailable: Story = {
+ render: () => (
+
+ ),
+}
+
+export const PendingAlignment: Story = {
+ render: () => ,
+}
+
+export const ActiveCitizenship: 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 RestakeRequired: Story = {
+ render: () => ,
+}
+
+export const FriendlyContractError: Story = {
+ render: () => (
+
+ ),
+}
diff --git a/packages/governance-widget/src/adapter.ts b/packages/governance-widget/src/adapter.ts
index 522d74f7..0ab23a89 100644
--- a/packages/governance-widget/src/adapter.ts
+++ b/packages/governance-widget/src/adapter.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useWallet } from '@goodwidget/core'
-import type { Address, Hex } from 'viem'
+import type { Address } from 'viem'
import type { StepperStepItem } from '@goodwidget/ui'
import type { GovernanceHouse, GovernanceProfileDraft } from './types'
import type {
diff --git a/tests/widgets/governance-widget/runtime.spec.ts b/tests/widgets/governance-widget/runtime.spec.ts
new file mode 100644
index 00000000..bc2c42f6
--- /dev/null
+++ b/tests/widgets/governance-widget/runtime.spec.ts
@@ -0,0 +1,150 @@
+import { expect, test, type Page } from '@playwright/test'
+
+type RuntimeStoryCase = {
+ id: string
+ testId: string
+ expectedText: string
+ screenshot: string
+ mockInjected?: boolean
+}
+
+const RUNTIME_STORIES: RuntimeStoryCase[] = [
+ {
+ id: 'qa-governancewidget-runtime-fixtures--disconnected-dashboard',
+ testId: 'GovernanceWidget-disconnected',
+ expectedText: 'Connect Wallet',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-01-disconnected-dashboard.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--loading-connected',
+ testId: 'GovernanceWidget-loading',
+ expectedText: 'Loading wallet, identity, membership, and governance data…',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-02-loading-connected.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--onboarding-required-hoa-unavailable',
+ testId: 'GovernanceWidget-onboarding',
+ expectedText: 'House of Alignment',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-03-onboarding-hoa-unavailable.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--pending-alignment',
+ testId: 'GovernanceWidget-pending-alignment',
+ expectedText: 'Alignment membership pending',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-04-pending-alignment.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--active-citizenship',
+ testId: 'GovernanceWidget-active_citizenship',
+ expectedText: 'House of Citizenship',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-05-active-citizenship.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--active-alignment-injected',
+ testId: 'GovernanceWidget-active_alignment',
+ expectedText: 'House of Alignment',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-06-active-alignment-injected.png',
+ mockInjected: true,
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--vote-detail-open',
+ testId: 'GovernanceWidget-vote-detail',
+ expectedText: 'Allocation total: 10000 / 10,000 bps',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-07-vote-detail-open.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--already-voted',
+ testId: 'GovernanceWidget-already-voted',
+ expectedText: 'Ballot updates are not available',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-08-already-voted.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--vote-closed-executed',
+ testId: 'GovernanceWidget-active-governance',
+ expectedText: 'Final units executed',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-09-vote-closed-executed.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--empty-recipients',
+ testId: 'GovernanceWidget-active-governance',
+ expectedText: 'No active Alignment recipients are available',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-10-empty-recipients.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--pool-unavailable-mocked',
+ testId: 'GovernanceWidget-funding-distribution',
+ expectedText: 'No active funding distribution yet.',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-11-pool-unavailable-mocked.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--unsupported-chain',
+ testId: 'GovernanceWidget-unsupported-chain',
+ expectedText: 'Switch to Celo Mainnet',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-12-unsupported-chain.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--restake-required',
+ testId: 'GovernanceWidget-restake',
+ expectedText: 'Restore governance membership',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-13-restake-required.png',
+ },
+ {
+ id: 'qa-governancewidget-runtime-fixtures--friendly-contract-error',
+ testId: 'GovernanceWidget-friendly-error',
+ expectedText: 'Governance data unavailable',
+ screenshot: 'tests/widgets/governance-widget/test-results/gwr-14-friendly-contract-error.png',
+ },
+]
+
+async function installInjectedProvider(page: Page): Promise {
+ await page.addInitScript(() => {
+ window.ethereum = {
+ request: async ({ method }: { method: string }) => {
+ if (method === 'eth_accounts' || method === 'eth_requestAccounts') {
+ return ['0x4E5B2D7a45C2e31a8F0d09b4bE1fA11aD3aC9F08']
+ }
+ if (method === 'eth_chainId') return '0xa4ec'
+ if (method === 'wallet_switchEthereumChain') return null
+ return null
+ },
+ on: () => {},
+ removeListener: () => {},
+ }
+ })
+}
+
+async function gotoStory(page: Page, storyId: string): Promise {
+ await page.goto(`/iframe.html?id=${storyId}&viewMode=story`)
+ await page.waitForLoadState('domcontentloaded')
+ await page.locator('#storybook-root').waitFor({ state: 'attached' })
+ await page.waitForLoadState('networkidle')
+}
+
+for (const storyCase of RUNTIME_STORIES) {
+ test(`${storyCase.id} renders runtime state`, async ({ page }) => {
+ await page.setViewportSize({ width: 430, height: 900 })
+ if (storyCase.mockInjected) {
+ await installInjectedProvider(page)
+ }
+
+ await gotoStory(page, storyCase.id)
+
+ await expect(page.getByTestId(storyCase.testId)).toBeVisible({ timeout: 15_000 })
+ await expect(page.getByText(storyCase.expectedText).first()).toBeVisible()
+ await page.screenshot({ path: storyCase.screenshot, fullPage: true })
+ })
+}
+
+test('onboarding runtime fixture greys out House of Alignment for non-whitelisted wallets', async ({
+ page,
+}) => {
+ await gotoStory(page, 'qa-governancewidget-runtime-fixtures--onboarding-required-hoa-unavailable')
+
+ const alignmentCard = page.getByTestId('GovernanceOnboardingWidget-house-alignment')
+ await expect(alignmentCard).toBeVisible()
+ await expect(alignmentCard).toHaveCSS('pointer-events', 'none')
+ await page.screenshot({
+ path: 'tests/widgets/governance-widget/test-results/gwr-15-hoa-greyed-out.png',
+ fullPage: true,
+ })
+})
From 146c1b8a67a322db7a1e34af98a3c90cd134a534 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 8 Jul 2026 19:24:56 +0000
Subject: [PATCH 4/6] Fix governance runtime empty state
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
.../governance-widget/src/GovernanceWidget.tsx | 8 ++++++++
.../widgets/governance-widget/runtime.spec.ts | 2 +-
.../gwr-01-disconnected-dashboard.png | Bin 0 -> 130894 bytes
.../test-results/gwr-02-loading-connected.png | Bin 0 -> 139636 bytes
.../gwr-03-onboarding-hoa-unavailable.png | Bin 0 -> 89814 bytes
.../test-results/gwr-04-pending-alignment.png | Bin 0 -> 51168 bytes
.../test-results/gwr-05-active-citizenship.png | Bin 0 -> 137433 bytes
.../gwr-06-active-alignment-injected.png | Bin 0 -> 147160 bytes
.../test-results/gwr-07-vote-detail-open.png | Bin 0 -> 66317 bytes
.../test-results/gwr-08-already-voted.png | Bin 0 -> 147695 bytes
.../gwr-09-vote-closed-executed.png | Bin 0 -> 137252 bytes
.../test-results/gwr-10-empty-recipients.png | Bin 0 -> 131887 bytes
.../gwr-11-pool-unavailable-mocked.png | Bin 0 -> 124011 bytes
.../test-results/gwr-12-unsupported-chain.png | Bin 0 -> 148548 bytes
.../test-results/gwr-13-restake-required.png | Bin 0 -> 47802 bytes
.../gwr-14-friendly-contract-error.png | Bin 0 -> 145365 bytes
.../test-results/gwr-15-hoa-greyed-out.png | Bin 0 -> 98301 bytes
17 files changed, 9 insertions(+), 1 deletion(-)
create mode 100644 tests/widgets/governance-widget/test-results/gwr-01-disconnected-dashboard.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-02-loading-connected.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-03-onboarding-hoa-unavailable.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-04-pending-alignment.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-05-active-citizenship.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-06-active-alignment-injected.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-07-vote-detail-open.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-08-already-voted.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-09-vote-closed-executed.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-10-empty-recipients.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-11-pool-unavailable-mocked.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-12-unsupported-chain.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-13-restake-required.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-14-friendly-contract-error.png
create mode 100644 tests/widgets/governance-widget/test-results/gwr-15-hoa-greyed-out.png
diff --git a/packages/governance-widget/src/GovernanceWidget.tsx b/packages/governance-widget/src/GovernanceWidget.tsx
index 291e2d5e..9256737d 100644
--- a/packages/governance-widget/src/GovernanceWidget.tsx
+++ b/packages/governance-widget/src/GovernanceWidget.tsx
@@ -145,6 +145,14 @@ function GovernanceDashboard({
testID="GovernanceWidget-active-governance"
onPress={() => actions.openVote()}
/>
+ {state.dashboard.alignmentVoting.options.length === 0 ? (
+
+
+ {state.dashboard.alignmentVoting.disabledReason ??
+ 'No active Alignment recipients are available for this vote.'}
+
+
+ ) : null}
{state.dashboard.alignmentVoting.hasVoted ? (
diff --git a/tests/widgets/governance-widget/runtime.spec.ts b/tests/widgets/governance-widget/runtime.spec.ts
index bc2c42f6..2bd108a3 100644
--- a/tests/widgets/governance-widget/runtime.spec.ts
+++ b/tests/widgets/governance-widget/runtime.spec.ts
@@ -129,7 +129,7 @@ for (const storyCase of RUNTIME_STORIES) {
await gotoStory(page, storyCase.id)
- await expect(page.getByTestId(storyCase.testId)).toBeVisible({ timeout: 15_000 })
+ await expect(page.getByTestId(storyCase.testId).first()).toBeVisible({ timeout: 15_000 })
await expect(page.getByText(storyCase.expectedText).first()).toBeVisible()
await page.screenshot({ path: storyCase.screenshot, fullPage: true })
})
diff --git a/tests/widgets/governance-widget/test-results/gwr-01-disconnected-dashboard.png b/tests/widgets/governance-widget/test-results/gwr-01-disconnected-dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..69f5834ac12af412fbb9240c97c9c484fd260fbc
GIT binary patch
literal 130894
zcmb^2cTiK^-za(&9}yG~5D*Yhs&wfcM0!`Mbfov*LRBf!dk;l=2}ODd0hQi+4N0We
zgc<^Yz~TAbdGCA9xifd}y>tKBvnTUiD|@fCXYI8<-?gJZXetpA(h%OecaKO#Szi0z
zz5BKI?%{7fesK4T>|)2Kd-tB*Q<0a|_0K(k5SgTILvC+Z`96M?kN$FE;aq4Pqe@|H
ztN*R#KUMvIs_%dQ>t7GIIL`=zad>52
z_q`adS_>f5FJC^myOZ?EBnuV)3PH)ZTTTKZlDzw-gf#T@=Z+CWTCjl1{b4|
z4<7uUsi86`?G1bN_>VwBfq1A(k)>rzVZpqx1K8e#zV6;#sew1^#S`X&mPT2`?42t<
zHeG^o*OuR2buxN(h1=-!`k^iNvOU?YU%$_IQ6xzq`;7e6Yel%zo)!N$cH807No)GY
z#ARPs9?GgG{r&rjl_|@z%%tQ~j!i!E)4Q%Bx6EhmtaGFM6cLQm{H(t+B7u@MPQ4A`5_1N<1eV&N^7Av;#CU0*Va7Vl%gdn-$MFbazxUk}HGrm{~
z&%JXmIg{_W`?GK}+ZdDel?wjL|V^Dm|qilk548`J&Yp
z>R|38V<5H~CM-k&BlTf!cg;_y^$>?6nJ#)O+mWD$!Xi<4&G~7eI)4;gle#^C*vh~P|`u(lZGBsE7n#t!tGC6=C1-O-<^BKQ_
zTZdgJ8@AuPNpECX5iQ}?m1fYnK7fIxC@Ze5u((gN_6rUQu>;=t1*O4d(%%dw~o(b`&ZluHByxf8Ro017ZgPS;d?=sy<=
zbZxI|JBcIceaZ5+WODLiya#wn4=8rXi7_Y5x0>(5&CWv^>af$C6nuGg@L>B~&{B}a
z1gVrHVxv7Vzhhf^UU@X@nJu!`1rxkJL^7W8a$M!XJ3^v@jF8j(?Q_AfBV$d->6u6@
zDShTIH$I}x?0J3;>5RkS3fSkuH`9p@;}Pk1%<9xYV_%qM0!07O{8dGz3E*C
z@4>LmDdD=5eG^96IcXJ1zgIl!8*wYy>+u5#dG_Kyq`ha8+UPTpuNbw{jQE0l{?PXB
zDmdPHjZNrWVFFY*krpnL2iPClhWmOonpCf(1wy&^wNr~MBp1AQ{(gn*l@S}r7@<$u
zTs4EQUQ?7VZ9Na+iNNleL)yyc=2}-lCoR&)!G60kJ(bH+Zk>Jn(dBQHcPKv6`UW?g
zn#qQ#D+Z6WpCY%Hjk2N+47&Ba;OSA#x{!-J7tOkO+#qSI~QCy=H1uAAZ)oZ#9FF5PSk6FscNU1YAJU2xGeZs$e`)*03%>AJXLUMfZeDR8y;d)&Hl3IT7^
zpZ17_%cy-eMuWg6Z68++Cr~T9?N6BEyEhBBT>*p6{Owdvq%DtS%m7{!H+P+sp*1^Q
z9p8?)x?uuJ(VdjZ=wEKB)#)Rprgt4vM}JQYA+TYG8?_kakq%HiMWVz~yNU=5
z*TX<1)8^{~%H>%anM0E(C<
zK*;V0(GE-oE
z%NQp3RHPZHLx;|+xs^5CW#a7Xmc025ZlT7cBgohF6g*zbXKA9vISv$>od|2sOM+)xdOKbWuegj$=YEcSK@Lwm(Za
z762^vTdGb`BHFl-H8l1>F5^_mf
zx%FK;{YrU*-{qHtT!<|UQD=alqzv7ym_^sC1a
zNgHQ1m&$YWR95R6ban{^VL1ycs$x~tE8D$
zoAtaT^9}2m8$;-FJh3XJ5?gx$VNPOaGqT(Ye=|o@k3!}K<&nuLoAC^}^;i>fd`5m4
z=R+`2WzJK1@*Wna<3ho2J>oU57#lJ(?XNhivMrmarSOe@|5W(X==EZ=I(c6A0;n|ES`W8Jm;16>GO#zA8v|^R>H7Lmh^sEBgB9sAR+6V|#J-
ze8CT1BemUi%%Q&t^Q$xMU#||>c2nrNO)14(LsPY!mn>99a>nVPSB?%tAoMt;hT7aX
zLb_=+0;tqddAl)}cNm*sr#|SvHqe*2QA>GJxOLu-=Z3>>Lls6_QG1Ak>hWL?Z^^t}
zbu@#FCuwNxICi@xal@_6ru5HZ&;hy6oSQi;)Ge)H1F6&MeSOvRdSbxnKG$9BTa7b2BVK%1W$M5}gV)M_Eww4aW%
zBqPWD8b@Tpg{s=8d*?7W)n^^yYZ?=?5@;zTw63bV^*m(c3yDrKw$!ip4R*aGq-_-
zt60!qwPb~)lL@UG_iFKXU8pYPZHr?-N1goMT8lM5Fjw|8#+E$wgs&
zEs~L~y)mFwIUui@Y|1`gzUYXtjy3wZ`JyxHtv~8-tM(g@sqCv!=_g^?5;?%${?GDT
zE8Vg|>UCV`{Hs3Q{-CW7puW6?P{4hprZn^9&5
zS?R#msPWG%?Wo!-FG?$#w?J&~DWP_>_kqKnxEag&Pt#d!psU%QyRCI4##L9`tzlgU
z&8A6B7A?;hpg%Sln0)jWEJ_$1{`60KShs2+^Rzw+u
z%{_BGnYA>#Ss&`?9N^YH50G6NYd4G?;G#)S350TNEAY}Uo;m>6OYOeV@`M}TJ6PZ!=47(^XW(7NR>D$SN_#3(&J{E(
z3nlNwL1&=tXUv76AOhFT9)@)8Y
z;b>I8!nnT=Y5m+vWKT7}czQ1EcFXM8&Y{R4p5WH!WgxU~1;=Hwzx5hYpOx6%dOOPG
zP&{FxYL!vm`;N=BR_d&BzG*ycFQiPMAgGE0$I5OlCR=qF
z-L{Fk(I%(3bbw0G%jtWX-~P)2$evi%A`UJ@CzrH9(#JQS=|Ap)W{DfG!Sq|Ex0-XW
zNp$DkOZIA*iV*U3h>DZ0213$`i({^NJ`lrVo9-Dfp^8eaiq$-lk;7rrGg&$HQoUwG
z$sTMZP2p4aTxDo#KE!i@;MmOo=3}0Lo1rahS*{A;RhPl!UhGJH|9;e4Ft;w)eRb_R
zCK}&c0=P0b%qPQfeD0CjuXqZ7`KN&G%%B>P0(?zhy&G~{IiMzO4=kbSuN{2`yskX_
zuzBADG*bcN%y`tdHk)f5D^&5xr%_ei%n@a(FQ}rYCup)RguV{3%R`E{fOTh|H>7eKoWx)a%;D?T?{arX^UZI&^ba0u
zyP8R*0HRF69>Hll!hSK%jm77s=7*SU3f56eoXvWDH!9?`vPfyZrrk;B7+ky0DesEK
ztc?+$wsiO}x{~E$-y#2As_K!1+zNZl@ryvtj#}1Wmj7JF<+SDpoqaVh8O^HqU&ov{
zz~|c|3bc0W+BOmS@blebiN4@^)UrrE^sL{Dk5j<49e~DeY^p>jzx8b`n0YDVagd4|
z7zllCkb(UxQ@4zc^9mKcJ#SGOvz&Mp8&4MvKbzc1a&|azNpxM&tMhFLDgW~qCc8M@N4%3l8Q-XHaEek($Ikbc*d
z^PU4dt*G%`#9R}|f4|n;|06V9ChzhPDm>$S;}!Q~b@g+d{#y)@e{-iIv}>!wU;Tm3
zI*>&2$gKPv0sK%ijaa<%Fw*bM{Egj
zV0M%Ww)-mO5PrF~c*Cm!t~-lZVP4>*Hiqq5Xrr`-y9Cao#V^N;H(aomvY
z2oaeeu?YuhSnawVL$roIWnQ1URa~MVpH<_hmp`p;&8z6}n5KKdst6ufi*HIXdyl})
zdr%CYXO`~ds^8O
zmQirKFb>w~TqsWKEVwn;#aViJbD}OVY^Gy{hsyr_81pw{vHeZYyk>&IH`s9oda^*8
z<;4xgk*1Kw??Z;WC)M#I3geawg;G}oonWWP
zy5YbRoEDsW%v_US)b1FQazs3s;dZl-DH5s_s%(nZpHnIb3RnsDu^L({1BDHqmB*GA^%efjz?I1grXg6Xr@`ea^DH@=s$15)So@%qx+8>|M%g4hur@gL;jya
zAOEa0&f{yLlkg}jEuLANa}nIrh)U~wWBqZeo{vzP_OE3`t}P4e<3D&0J`#>0zt45!
ztn|?30|~9??dG}-qUelxTH(!Y*De(bwFc#9;ZGho@6Dh0=-nR2PUqoEK+JR+1h)&7
zLN0Ju<%N59(%WT2Q|3za5;W<*1u>Vr?4{LERDXPWxo6uVCAdt-e@$Md7AIK3B
z#UD!6Bip6A-dt!rd$_88Pq}b)X4*}<6V&zOPG5DxyVVMJSMaqYr4xu%$+X?t0Y3A?
zvnBEoPi@V#QU8-?-370&ZEIK5-OR-X8kq*uc5%4vlOy~LdgtA%d?#q#aK4zYI@Y>m
z4_9frGv4EkP#Lt}tu7I#f5tQEwTED?viT|Korwl|+(#(e;X%;NoK-$adGCk@j(4Gdvo8=`>G=>EYSAuHh%y9bJ+iL#wk3zWTnYHE#VpGd8lBUkkcD8LYS3
z9&h!|vT?3U$u!UG49XsD)Ulf_uqnqC!7UCbBV*60#{!Pjo((xo^Cl#&rtcO93GxrG
z!2J)N$;88w*e^m@Ia0~ketI;16=N(WV%6r*>6KR)Cd;fdYhQ`|Xi`?&IHqBqFM;r=
z2gB^QXboRNxD1;)6l9rxm(he~{Asys>N%i7H{nEp@Amk2-TW33hih&UDZddp+ti_8
zw+$xB+x2u2%z@k9Tkt1lM~C_TmBZlM=v83S?kxlpIYDw
zJFD_N7~o{Mbk{l#D0e(AORx$wTe^e};|=h3z>j=L>k4z-;l^RHEgIhoG2r*oZ%~zWX$HOd7f)5Xav6R_KJE5C>+qZ8l
zBL`NzF^w{TC%*+fN`>>V_6wlHu2SD%6XJo2&h4u{nV_APee0~heDCjO$m4b)XxEy?
ziR7f?-iiF1RnrkEdsEW^X@5*>UDwsk@6%d*;qdYmZ)43H7y>Nglnvs}xv0UG8ej}W
z{J@_WJ@tH1n44gz)V?x#IpJ;a(YSZ*4HCD;Eti3_uG|(Ejgj9Apwm%8h6Ztp*qqKt
z%+>lNP5E@SI43W^#Bh;H}vY0z5zOYNV+d;ayymG
zNJ*5u|3$077Tmz+sSgpCBe?F+EusnUq^VNa>Bw4~F?~>#w$+!`tO%z(zrgw|Vt%82
z^MH-y?|x3|uC3EIyFi3dK{|SH#0@+ntB9x!+sl)IU4}I7>xj~?s*Ht%}$p87q?x!+W@GKG(@c7rrTu()3V^t=Xs?&
z=hcchJ%wx^K>cR1SEu~B>U^i9K0b|V6nrl$0}z0u53^YOfln;`R^2dPgjC(E-!Hii
zQ0OJZ?YUmC!#66E>d>ym%^xhSs`jo3bL<$tSy1;J?EQ{-7v$m&0U3HkO(eU#p!4`Q
zr&$J$_L8mL8~1%{z%ZV<%NrdnncGUxJ+Fm&8R?KKsdc)8iyfKEKBiWgliKZKCZk#2
z5G!X8Q&1Q}^|Qf9vcQ_D?PhDCA2IU1wpMY
zfg{B{um0f{np{v#nJze{#<^#bw9qUj#FJ|L!dIUj(y0qW)h9Y+NquKLo~*OSk)WdQ;3e
zcRf(F>lW{r72$D!)XF0f2sq6^`;t)m1fT4)#rFBUm9%l`FS|Mn*(mTj9MaSvizTAJ
zle;yToumMfgi~VMM`KKea=~`wa3#s?_kMw=OAcCwU=B7BO!DChW)Rm7Y%r)%ee73X
zeU3y_NQs2R(;rq`4k`s|#_9RpP4-A+nh`kE2w6rM7qK7s@iM0S)0sNNzBo$7EIl>z
zYETA&d*|9;)%OfqZ>rFJF?h9uze*gc0nF-vO30gL{V$?MH{kc7dKi3Y1|Gy2qQ?>R
z&w(?t4QKhGfhW=2wE9ojt!CP-0?oO(0eZbME|trd(C@;X?u|ac5n*^{s=GyXOa9_5
zJlbX7jUH5q{0@@#&|gh&C#j_q3e+f{<;B$xT~=7S25gYnKwax6?EPK_?o-LIhbZm0
zxnr*CIstDC1|9tI_LI1-vYB_+!vwVi0gey3HDPJs*V|ij^;@Uk&kc}OWPu+Vn!7sM
z_le8ZTe&{#X@z(WT+aS(FYS(ekgH_P;D(`~_dm
z4<|(TNMF28JCa1A04BQiRH?758a`@9;u2_id%qZV*9SL<+w-VXMSU0Ez|}yjSq+-d
zk?!jtJJ5`Sb^Z;*K7)xL&`;<2v)W^0{H23G9Zxccf`l6;+}DaBmY5sbU)aYI*+WfrbTsw@cBc7
z3(v#43Fk9C?C8oA*B(aUKLYtU>tBI1z@1aGkqq4_q!Xr?ecm*qZWL^XG#(gOIXAw`
zhE#X2*ODRzMsqxCPXB^|rw8P$qkJ+vw%au4-Iqa){({4cZ&Kvm(%Q^vWh?^0Pg4A_d`>?|(AzTmbP+n!3RX8gq9>BU)(j|h07o0wl0
zZtdc$@=TwmZB1aqHXm)P^jfuOL+O4hlVAReKs$cm<&n>)ap3Xw>#1WNf_ANqK!K(a
zf5jjPy1m?Au#eAG|07`mtAG`5g}QEs%W(DgH&L+{OwlRs6Km94yquv8y9wp2
zWnn?VjZ2OCg~}>%lyk(p{xO#PdqkGyZ&^Ls4O&SIpvU>rsnL&KV4sGjzjxU1aZpp}
zb`T4yR%n_V_?JiLeSTy2F80@{*S+7f3KqRSa<2qZup}4Nf_E)jdo)>G{oRXol=m5*
z5VshHU`KEaFFhUmL7u1mFKk91ou{J$C!H9rtp4!vStr*rPmlxq-2JzE`a;E|>2Tuw
zzD0$$sfN4(BFQ|&m|-5T<~)LTsylMm!ul`LoAlEk)_8GE1j$3TdxJs2Z{yxEQV2*m4F6O
zJ}$@l4?BBwW&!l`-SBGQ)-f$>cv6KPI#!9E60SsEY2#2=$~h*S5%wa@|PK$sKg8pb7RUlS^c0jO$RL)}{C=Up(BCgQ=N)vo01bdmuvKL6i^QKMUT`VXJt<8qc59JM7gc&Nse^jSU`X6J`&-(#LJaBa_+-na5WZO~FdQ_?Ynkw~Yf|p%f*ux{cWLZul0Cxf-O?`(^nKZNg+
zs@t=ePR(vv3@7Fj(f6KFtXbsZzb1{%wYK}7$D)PmwmBFGZgx8es??V7T?&sUS>e*A
z`jFEWa<|D~0M@*RmoMXJE9!Hgc8j#OUGH;Eh8TCAb;u{bh`LKQ!=t5GqyW}xUiyl$
zK_SP#YKN`N1Qn=9T4J5cH)oCulwSUxEfn)jC}TQjx#5aP`oAKlHCdy#dm$-gV&Nbl
zq`Pt4mkw&mfo1M?-i*|;_p2z
z#bisGppFfyySdWEnzR1d{C$246@jtdn)+jO24M9%1bYJdd)m+}C
zG6N6G`w-7VL!<*{j8*zvRK7#evD#1rLx>UuVp6@dZlSBJZm{9CR%dI5yve(n?
z1SD>Kq8l(0sRt~88_w(^INl_4Q_>b#yJ8c~yPag1N_JP2CL}x2$$p)@Uy%|Supk89
zkfnK6vjjuiEp1}=8P%P4h_iA9BL)JkS`sQHgI>?Ms)iEBdkfJLK(sW>D+%&_lXy
zJ0pOk>jZBs<~l_ynam00?179OY-q(<6p!R!>YYx7Yp!Fjvg?@d&e*`}Q35mmSGc`E
zxO>3$cs1;Fv^X%NW^}1doP|%@5#@9!itwyOj*u@WeA#1dXd(g?&yG(R`c;^Z8l7In
z0rkzPn07@yc2dbhe(drY%3Ms{J=iBy_21Jv_7Q;Nd_S`^7EYa_3&@D`e5hE%L`ppr
zu!ayu!M3ii1I=-?6R?~95m19jwN`Y-@p08P0N6OD`_p-t;LBRUbW@GQ$QcP0vMMiT
z{+02q{BG)isanmYWjR>I@LvP9%m)LkZ9oj8YA`_25SonZbHfx;y(vZ_YDI>rSKhze>&}ScyzskU-2v4?Vj_eDbDY6L^c>979YoXd6KE
z&{J#$^nT{BPYwRNRX)_f(-YKETgZ}sVzjE^iyf^!d^Vlq@y)VzFBIZgv3_iP+6Py7
z!}9t-2*v*G=4KDcZX(^ay;f;JT?ZVts-#e7)EpSYadJPttdwrEVF!4m$QiSq$q0j{xkAL2!
z(`i3uY6@#?H(nFCyA9RV=MP;q3D=&Ye8U%S
z{dan^&l6mszKf>uBAt2;l?yE6FctfRkfk#101=HE<`oGE-k+sOucF$22`Yd7)%)*8
z+%n?--Y@(A%cj}?!&Wo;M?c7w=Y4@|1PzBctQ0vBfdOjZyc5O#y~J_9#rN
z?I5m%CC8fx9gjzlO+EZLabb6Az7n(#go=-SO6n-Bc(-|3wQgx2)L7lXwq8Hrx%QrK
zvWkb(O?QKeqU7?3?eYCv?$hVr{u%Of9sdyi$9hs&L|B-9ai(g(3R8BeR{ok*%U<5(
z{R77Lxa|~v_Uak|o6=HFCdrk``=((X*l>1$oLPc4{p&=aYH|w48rj+g=`3E^y-?{&
zZNN8vv9G|dLk_#t1Qd0hbuP|GmDE6!*9k{n?>8NQPiCGiK0VtBe5GS!Tgq9!Y(t=D
zy<(ho-;(2T+LfAp#jK_HC*Q?c6T@?Tua!JQ(wvTd{bIuoPqirsN2Qq#q_v~4c}DU@
z!z-ig{6;b3J{M)3Q+NYme8Z17GGdm!R~a=|zEieve~p^Cx^$5@Ck&j*sU$CQ_MIH}
z1?}Ruo<(zMX6NQ-yLhE9`UyGu$a!t2%WnPTRht-^@Ng*1@kR%7Itd~T-WS(tjEt(!
zgAk1vr>xyVxN)(AKn`icD`7DKKQC61_lx=I-TDGaxJ4^Nj}{`5BM9ip7@+!XYN+rJ
zw`#cY%p%8u{8=+~JFqb|BJaiILS|+qKEG!w5t?WWY49v`+v=Z~j)e=u;S487+KboR
zQF=Bm@Z4;R-G;LH$ea#E7bhpMdlR9Jk!G!rle0mUd0>rMP9E?_jmu<%xs^;tI8hy$lERmh~G!
zw+y5c?L+Bn7_YSTNO&*8P&cd69j0=@bLg<B>iSIc;kW0+XFEBxPEI*vPf}*c4rrstTb?CqKL8Ox<%}xJh0YcU;XOI(&mRe>FSb*)dF2cmzu(F%~Mb(Q_bn`xH
zl(MV#>S#4V+oi=U@-a%NBQ5Ys;K)q6_UCG6WU%bQsln|Q1Nn3E;gH8nAwjja<7z+%`X_I_mI9j89HJj<|A<6BH}|ORq2n$HRt0MeO=jN>4;9RU}Zd9n$q>
zwrC)j1k+o)lGLYZ<`mXZ1h=R6Hr;di2`N;}$u~X1Z9EQBG`+kDJRGVK(MYXrSsNqq
z@l*?&6PoW@uxR5~eyx~!hs?x9r;Uj2qoqN_lUi7lPu!7mir0lk5@emftad6Zi{0Ac
zv9gw0!kf^V+Ak{Q|)*Le~95fm!nJ^OIkZ(7oO9mU?}>
z)Gj70_eyLDtH$c$f{x~#5(xoMPERb(ufu~hOpWVw_?o>|Jan9b^C(AzU0^w{omovw
ztAmg=#jN`a2l}RgGz(i4t{PP0-T^t<*kxlPp?GR%fEf7|O>^1D%yf(0i4H}La`|kQ
zPXePR6=G5rp;4-RPMbo_@!7-bz;O=#6A`<|kUXJ)5iP|EPfvk==(KIN%S19ogqzNp
zPY1h9ngO8U4l^So#S9Axk;M?21InN8vLQ>J-!|2?1?%Pn1!<=jKYBFQwQyUGh3S8^
z*|)Pc6yES370+uivGFhnQ94K3
z(CG78fg_8ApQ(5?>optL-Ud35(?OisfZNM?+p;&IF6KaBd_=E}>$u7`+q?W^U=2GI
zI$1pt75l_yth!dC<0QWZV}zo0sA|IuSLM8GV|+Hv%H>ux<48jT=dF9|!aYdC_mqWz
zqqIit>5C?#o*#_HV$-m!=yWcb59M=v_-kv7WGR~!Hoj_QE0ZgZ+9ft6`{(n5`|7*e
zrj4$z-uE>}F5MdyysqAY4CjwNWwnSQeKK1zBS@RwnkIb$E%dZ8GN~|vRxv2M9|+(@
z>Z71GZ{;inrV23C=J&hir(fFWBzPRvRC}*CYHR5FBEPRrqEMiG@zCO!v
z(m9QJ<1ut^%E72kIqh!HLp+IB)6|)r6+RW>2FB{Ma`yYnkVR;JzT(a(P5M4h`?T!7
z{NqSGffmCrvUb+_)jkgAEKrtxmL$$~MT7K_Qao{~OaohGk-gD|rn84^Og%WwfTUxi
zU*jMjkh&cs^IH~XXe80kC(nOZ`$HToN>({^i?_?q|382>Yoe)=ni
z=`eR$ixLfgxP0H}PmGfjaK;%$y3bMk6?8xKv&9Fp*Uby}9l`~zlY*sRIdiMZ@zs%o$e_T07ji34cJ~4=r
z-#g;IRTKT~n&|q=lZerlgLMC-m+&?`PKQ4u~4p1@Gc=_t>QANnA;&Txnz9)VOWFf
zeTf6bg9s+y@B9MYZv+ameRKM;9m`dObTNd}+1@IoWFG5VF9*I*H5#wIS2(wpU*uxQ
zR+qvnHgA07u(w-JWBX?zNJ(yU9!^?lZPW&ID3s-!`~hkBf}{RytTXJ~Nd#c1ulV*2za)drhNj-`WJzT^C-VPp5B=_$X_M6co9R*UU%};$M~zw(DYB-;u!WlV
zWzM95rIp2~@H6Wr3YTCjro*uzVHWFxa(1dLH1>X9R$de^3DEQ4LE-D^sna3f;-#I{
zDrbW=W<}SI1l;g|gc#4k9dOtBgB0BfFN4RBQv{?>1g?6Q%C^PXqo`O3!4$&)S66v^
z4Xi$7NkGU7$))*c+CKpJqQTg+G30^jn-wfomSK-ToO3h`J@(3EZ9=~=S^<`$L0OFW
zQCTA`@uGoCYDwWBFWYNrLAcwz)vrzxm>rLYjoibI|5FJN-Vb*u)&4r;(w}5w_&!&0
zvcFiv;8|XV1?@X+gO8QT95bIbJ`4}U%-S1H8?kTGez4sv5juopj10ZR)gJyN`k66T
zA;hu`2d2Me6=l;H9PMm5#_g&4*k^Ds&X$1cT&*GQU(qHbdP|0Aw~$Y!hIBdUqusSS
zjp3s?W7Kyrm6~e3G}XfpGWhbyr+8IGp4o1@B*Q>2J>6ki!2gZcyE>Oy`()ok2sNR9
zto!w$$h>gbcDcffCiLELk0N~`I{brzk*2=f<@ZCq@T~@`4HbZfS0X>gEaSUhYpS%X
z%ZlmzT*K+;fco)i(BiY;b=%KxqpylnL7okD!AKm0B2Z(=f$(gnd5i`kc?Ex2DyUnV
zRBWh&NV2f>4G3ra>d+?J*xE3kl+@GB5L&k}nvjXAHCSNYuw#AtQ@^D4@S*k5mk$vf
zoRD)g-l3l2Mx%9-im^^R=B)RO^L=(Yb=~C$Q+9^G9=rKl%X_w`%Fnah`wIjH4RX$r
zEouPnPc2`4*fIf-$+oy}f1>svb90Au&2gNgE0%Ny_LupFPE+>#x{GV0qgn}19K<=}
zX@?S(DPp3&Zf)gkP1I(|F;966Eg3fm=w=Kut2t9j)0FEk2+ix#*CHCscc-|8lZ99ofHzpjf6){t!NdUbBpLjHd3mylB-8LqeA^RFA<$PxgrL^9?)B
zQu>#4VEKO%RX~_33{slopv$DTDek}uzeTK7$h4Vkq_^Q@L
zS7pluYn!5$ab|QeTi|udf2YY(zMne;%aAyb3qv-pkSX
zb&r95o+&ZaIP%f_!fdlri49vS@grs_tSv-Pj7@6NJLz)EXY9jc+2ybFF2U$2`pbCu
zC_mr854af1QF*!a^(5IQL2_TY%kUUcZy?`&`l7Mh8w}}}@b?QpZhp>86S7P#z_SW~
zauX}<;V-Sfy}HhGa8GjB)Bk_^q2>ccLi{tIIw_0^0Ai!hqT{t$m(K|U`P`;qzZACD
zc4!te99fP?#ZyS+2P(kCleZBO9m#X(WqJczl4p;M=ISIdpqPI8i=_LIvX+kV&-z?)
z>p+QoPSCu@So^9%Qhwn1lAU=ay%!%Hjh2Pkt3(>%RhF*~6si;i1RgOStecx!4;U(3
zCKH*Q-m{xU@TwjYvUu+sBkdldVf}b7Q@iSGYc;KS#R@Y(qC(}7Lke=71v_;*GZkJl
z4;~yp04)9_ID9-P&7m>K@h03YYg)Urv1t5ImozySEJUq?C-9!uJ1k|<-yslKRw!hg
z&2@9YZTjj@Btmud9`9C#t38;E!?V1I|=SKHOX#EFk}r=)RihQYGlru9?7d7)P7u_+O0qpT?-KBJfgRV^nRyrZLk
zCJ*n4rQXsdP=KrP5pk1#X8RitAnnu#l*xM$JoF|pEra{VNuJ
z`5)BhMr~n+eVx*c54IE61gwx#EKtWiw0*=_%aiotfrG9=vppKxWqh0c2Ct1~w~)n^
zAI)hOH7C?m-uCd_j^_^t*PIXmFP-xXwE-sKZ@YIb?5s5Coct0g
z-dKDy+0(5~&Rnb6DE0T6d1OTENYZWYN=WBdC2MOIl|Cu7$Sd%-L%Y$3U_mH$;J5YU
zF8)Rp*F3LAhpk4jt+|5Mj+L>}cKntSEGr}L_S4r~^cdzG6|7a|&z0#n!lwrmvZ@kJ
z{QP6!hrlZyDPVM1rQXb<{~1mMza-I+Q~g}~s)_wEY{Tq9RtPSU?&E$#WBL|0X!k?RG?d$GE{-56n%-COc)SN>ggnEj$wK4xZ
z+>@z(yF;d4Qx+aqy&`z?y9-lAk!VQx-k4COwSFc)FYgudE_nkpFys5sHYd#90Wum|
zBLZeghOkE*l$n+D^||_2za21A6OqL&?-x+ZQwmWVq4D^A{l@Ft-w6X1lYfD7b{NUh
zhL|iZcY{f*#(<6C6O3(SqHf+z&+BU79;P6U1>Ch_At@t^H{Am$t7+c8>R4fmx$nou
zMXAD=v+J7Od70^H0f8(PJ;S=Ga;S!ImGT5E_w}r^=ETsPuz{e+jyi$ruBaYxfKg+x
z)6Z#Zkk`g~7Q+7gSdY)aS|nlbk=#M#RMiR7T(aRiO&+3MYIaJ=N3{`CO0?~k1g*LE
zrDD&ri1A3C>GI
z<29OwxaC8VI0qjd&0?ezpdVt)tyC97`z8?gvtQ5?qDQD>_)o0K7Bia~;Pq}!%cRq3
zb^2cbd1lwSt*MJwN8h@;DW@UF{Lkp2^+fGKC+NE{sI!r}@-lp|5f<0~bn(xc|Y}TSdhcblsx3ySux)ySuwP1P#HR
zG>y9i8h1%>4K9s41oz+sx8RV|-}jGy-1~6H9p_~~RP9|=y{fuuE~&YWUUE94j+P%-
zY2^h={<7)QMO$oCQR>-M3hW8k*F*rnH%weirzg^MB7{ifGy^7Gt*P|hTuMp*bos%KCKA`myul5Z@pCDYW@
zI`#m!XD#<$eiTB!48k-Dv!V&LHrBnoiUg-QCvWjj1R;L{gas0vTC+6pQiro%cze
zEc~XSQ}WY$Hv+XD{#VnFK5^gv82>>LKI5NTx_H-ewEXg`v6I7pWKkz@rNg{o;yL?xdm#zskxk7m2I_`4@s>dtDLh(
z0Ln3+&D5U;Jx_tZpsDsR_qCRkK5qsk4HOxcRfaD8Hp%QSTl{AP%OW(E<5hbwIr
zhiD-0^^vHT&eJWE!Jm{~XEU9WC(_X<9W@b;XplgVJ$m=i+C{NgX&VAs7J(qI-f7B9
z?xmpix^m}cTSn5)0cunA3oX$clao8He+3ZHqUNAq)zL-BiU@%t<>kz$&LsRVeD<`u
z;C9#COyv#=c?eqYtO=Wru%lTq(vm^5XmqGoaB`VD;oT%)+-*HZ3?i%7oRxFN3JT|=
zkpWN~=I4^L>r+@W6#7M7D>B<@cr?MZPI-|wV2Ikz&~u)gwtWMt?5FX5DT_L)5GfGW
zZVLLb_-hhE>S2)B#yFiu=b+7+SkR$PpsB#*=|WVia%Hq9`
zII%Wj`3&cWRt2++^D4B%u<&8eGY0sg6-mG9*S_-)(lb(Y6LFj`u1U^SG{Wq46$*|i
zM-B<1k-&ePO4g6=bI@uiJZbK4>6=!uJ{k`aVHFSRYjGjMFdY^$eF=(Bqa*F6Pf`
ztN4e$8wJt=%9C@?PDN$$eh5hu(Cp#qc)7@ilxF;)X{N>zVR1LVMRVd{}-TpXu?Aakc@IEm8p`~BB%SNhTJGdANB3V(wN8*S5KILKHu4JOV`&rVw=&f!i-s#%vD6{joa
zfcJU#QE{;NSJaHU!~;$1Y!R+18+nCwNcJ+cnBqTbJ?5maS@K9b#zq!!owp^s>&esX
zeYIO4T1Vd0h#+S7hsqfV1;!D5vT@R8(O9pbUqTYnpG;}xFAfHsgM!G#>n|0ve2KNj
z$SUpN_0!c)Nn_SQY-W_O6;WCNCK|r7&%Hkg(L{#4S;RVXVs($!Fi@NJmU3StkW!J
zKQ^aH7x~e@d5{yeB(dHf{FE(Ov8&8IW!*U?JJ65Q&uvTkk-UNPbB5lVm+a9ePi5NO
zIyQA#AYV*<3eT!dJ~TABv*Py95)_&Cb1fI|@2P|TkkZo8W9VbHR;%#q4FBIH4uedV
z<~uL-({;hHX$@uqHq)%2N^sqXhurNExHhgQsG7aik5F!MAEH{Z)vIyS`l6`q`g|P^IdBht3#tW$1#C*%>XYxN1O5>^tCC%$0
z1c>Z1R0_oOj+nWy6#N8-4hgq%6>39sh~}2QWf@}Hi9Z-g5lR){YmwW5!QsWNK2#jM
z@hmYK%xwKSmC0Ht9~H_wMy6W?Kc-Z5wHUKe=kzgV%{4;oG^Db!sa)mgj`2Q%Fk(88)VEY1{5}8S0Q&Fa|Az>u|2!T1ss5)b){v`J
zoAR#r$_71>jR6-ToDIv_1r8jy=(Q{}85f^k#>G;l@g$k4#KKCCvB6flw$t|(r&_8F
z2EE}#^_i)yyzdD}hjk4bfnA=(`&p#EhR9h|F>7&@4<5x_$=;e?ven9;*e`{OJltI)
z!3HqXI_;0^J~H~7M=zb2JHgG4F4lECHhFNf7Cjr+8DFh@`w;2U4WRI8S6d55+OR)$t7X6v^uA};RvG!jX&+e#WlxYcvp*3Zy%dwH>aW!heI0c
z1GtmKn!yt-6ewl7Gq+mA&0!Ub=Yn9!qN$~X;4@{>@`-vun;I
zC;V%~G8M8_KZ7DyHtcsk`5@HuX;%SR-Ri41O-Bt^7B6bOtALYVRt{&yU#lNU#FJET
z;IN6FZmc>W_8Aj1^B~Ay!7lo?;TN!`HWK@^hyQe|(0BxY`@?OrUB=(QV+DP|7&6;P
z|Gu75bwxlV50sShNvS0N0aV5E!a3cyEmmoV<3=pjxJ1$i)5i!S|KG39|GO(jpshQ6
zU(n5VK=2xD2qi~OWY}ww;QlFA3^Erd5ehOvv@CYgFwV!74|cmBvke_pjFcmR3bk&(
z-|YUDK=3bQ9C1ppJN0qhZI<}IK(wE7`yc+lv;`y`J8U-x65}HEGWs?mQX8
z-s^4K`AK$+qiSQRV!~}RA?DnY!5_)j7qn#&yvU-@|AOJFANSvVvE;0rF7`WYNxCFdQA^Dq%wSLj#=W;rvFj}s%&z6Xx-Z(?&;pP*VPG|zVj&R|4
z0z?6GAw)xLU(F`~fx+%j(odMsRFVIJiMC*=9B|EH{kyT@b|s&>@C;h!aebQ8HASAE
zv;fz*Zzd>+(KoUv*fSOd^Vh0sSRdh*s=K7eMHlegrozj^oZQk+un=ah3Td$oI2+%)
zW`@vWWSfy2OJ=r(G-aVz;e|;~KIti;P?pHS!6zSHNW_I||s!O!ly5`8BP$i8|F0
z47;Ir5HcKh^V4@cijte3Ua^DT9kJOXn&n#Rm=4qQi{w<0*D1^A#9|`&6#tpo^c7fo
z13=Pm+9;2HI?@Rp@LikUceD-!VHq;ha)mAFQ-gjB*8}YB4a9>1?ZJ!~+u74`-+dl&SPwpk$casAIL%EYK`QsOupL;!R^G!iq>R+n=SNZ
z_HMjs$&}&W3D?JmYGr}T3Sw&kf_z#52sR2gqiNT;q#7WSI%`M(^;n9y=|ByZtn!REeFJS{4$|O3%y?p`8b7PLc|1
z{|ZOZD+Cf*6-dM!{>QqaP(6-UZ7|R7ZLpTv6lNc--G4-DmR__
ztn8I3jQI+97aZ*z@HBk@lG^*+iS`$=*Sgz!ke7KVD@vJ~iuNXO(5WA~yLf#4i~hWKDG({4Bv)(bgEwZcZ}*TR+Lk|8dnNHr-pbT5sQr~C1GOVH
zCX@(Fl|>aK@sM3?g4EFbl@joIYN2jHCzza*j4?JU4rqL>D=&gTq)8D2Ur%%tc@Fd46}b{(o56hw^70@(clfs9zFXI$>g)wWOD4Y
zR&efJ@e>wZ9+KKo^8B5(urrSu{=K?8I<44N6oG)d!|Ho}?&r4|PR?EMao!|U=NE}m
z7xDJpy6w2JsvSR>6R1g5odPn3toEJ@d^U!v77*S0rVUPHw!CNt;03MZ4M~r(WQfOG
zJMMzDMuZ9%UWI$f+BwNo6WNV|`e?l!M*B~dBSvioCBrt^oD8|G9-tz0
zjnkIj^LD_8c56DE#8(yAvl?KSqOSy5>nBvI5!Mer{_(`&nMx&X==8_^oo;`W4_H#j
z0YVS;aQL+G7$q&puYv9KRgH3oWihCO+M-{V+iZeJLARe_)#oG*gOvcO
zr5pO#>_(_Y#%fv4G=r0};0s?|NhR9Yc5lp;SJ0=ae$E)#rL2Wd;X0z|+sZ}2RnUv&
zgC+4oR~91>tbZno&nam&k^D(D6ECM;ez#k;G2_x~ZjbI5>A0&EHEuzO1e;D?D3Zjg
zF(>K#ia`1dnUrj8+j|E1@jQvGqB-8d6yn&G`ZzY)pUOC@v_=KDJdd&~v+T6XJpI~$p^Lfl7hhvDdtyEZC6@}A!K_2K!YgbUH2jda7lJi{1v}0^^
zk=d2Aiy~pMRyBWb(VY`F6ZGY&BsF3v0Ejd`fT<91^i!AG+heJQ)o2Ya>7N_~F!l$l
z>UaV`aq^h}mGKt~VDg-*IZQu>}TLuGI_<4nRC!GYY-+
z=JGz;z;7plL{{BBR5avumj1jUHx116Mk(rl)Y450o9f3_Y&}WaOx9>$#yv^6lN}?u8Gc;aVP^^sPy@%+kM4WMpes0|74${*
zH5%$%x_7-Zt5@phmVv1oO7|=3vUhM4Bo0z_H^}9kn-yF*?#u+@Lj&s~&}1Mt*#~C2)3&W{_;j}Ku
zSGU2sKh+peR!mCG!7HnwIz&}ylv;iy<*3@-NIJwU9>*0YF?VB(Oyn}cs9M|MX3RU|
zzeu&6*Wkq`S~s=qpRr_N1WmkF`dbiA6drc28;x2e^RYcF)$fM-eXta~Sx>kq59Qu3+l%c4Xpgbb
zIb<4J;{&Sudll@KOMf{X_h5XPZQRS_8u3S}4@;V&t@_gz1(Ut7@)^1#Y!Eg78vm%;
ze-G-5=52TFI8G7X$j-NXxehB#5>DDa1bOy3kn8I>@H~w_IG_c8*wKH}%PiqxHU*@(
zXYdn*%mb563&D7^Kgwb_g>nS6fM0wviJ&ASKFf+=iGuoVOEmo<#SCXC&O
z;A461!0$6~yQYAso;>N*&0}+QoBsxj-dNO@T=ttIN>4FMv?F&=V1A)HB2nyf)n&Hr
zF_DMRIZU1<8|;oa$vv0uYpCI;7PqTI{|^|Tr*5C@B89?U4W=d74KP^3-Wwh#GW=rv$_1ts7%eh0GU$|7pCCLiEtQr)|xO7866@ol7ESFDGPjxW0o)g
z$Oncfsya?JYv+z@VtAIY|5H*^xMe)AS8)~Z}ghI6zDb&101aOBS`wdfd>UtTvEf!h?2&(>1PGmv!%w34!%PE{b{j2Yk%m%-;;hR@m|q}
z-rpKtV>^i-Vyl7q+F!@iNCo##l-ThD`ri%Y-<%Yo4l*YmiYC12#rU_7WPlsP
zOfNlgwiH5(@vi}dV=M>7i+U_lXfCvt*lg(8M#v8CJGZ~Ds}i@CCy2AZ
z3t#}gy96}z;C#}iiZSkIE4+>L(L1e%lTJXz-(x@m{Fid~|HZpoA&nv{3wT9DfN=0~
zQ-i{}hzIa^4<)J4F?*aZAruBfKr0v;A%qR&QxsxeAHP~};^yX%b6mEQ`m!aM;V{E6^81`J
zRKPek2UJJ^tcYR6Y|E+hf#1JlyJ|z(BMCVC9v^aE01<#AW$>}`Amj;_`81;m0CjO?
z-Tde%w8yK{|y_Bjza9
ze=A|gsyo+S+GE6pXz(Dl2-0~b%q0+~TDJ^MKZ|R=)llsSptVxZC5o8A0
zI?{P8%w)F6ajZq}ZUY+;Wk|HrPxKb1%!3ArURctO>UlP(o+@Q?+e9sd1J+&tRwsA3
z$Yu)}L;0k!pyg9+`LxKS2^8h$ZnJw}Pyj;s&L=_4cuND;uO1Pug=q1wV}lQOiz$B<
zv6^9CL&X&)Yfc?F;R)1R*j)S6=JdW7CzsC|BaX)Wu7WX<^x?xCN1%%l?l&^@QyZn!
zM@N*-)8W4_$4ff-O=LS~0asgyCY+sULnE~8$$&(YLR@P801%>aQzIKG11Z9BkA>n}
zRu|7g#D{^fW*i9)3Nq$H`yIxC@cxtINzWr?Cc&L@3PBZ%SJs;dT>1MMGEB46(f&;?
zcpBM8IY0*d;6EZ!@0C640ML@(!Rpy>BJBI`BrgsGXN{qa`4~xWp8^)#!C_7jle6%-
zPJ7NY*Vg&I+ju8bXXH^I!l&5E5jUUr=5Qy9jlSz6vi}&}K|)P<8~4oDEyzZGS4^k&
zy^%&F$J?&vM0lUPN#SUMjx`W8lWs7Dm4nE)2*H+781#
zC?kyurVl8Ngv<#&{1!cqun;0wW5%-a#UcSlT3@ptQP)MhFDkn>@ZQ8P40yD{qjwp~
zQVydqvPRBy*%S&ilJ$>f0X|lcS+10)aEuC#@s9SP>kXs*(|~)&Gh&`R2c;T_{BUv(
zKXbaRPe`lhtdWY@m2K9195h`0%@9;{+CBO4r37?qK5)D)LC&B%Q`1>&=Zq=06z!NlFX%e*~3I-}dHH;rTV&`1W!(cjo
z&tHZCy?e=e*0HAvP=N7*1JW
z*2~cZu1_yud@QC6Eq;2JCx9}biJt2p{G+}#f6SUqlJvRH_XVWyo2@4Ime_z#;$_LO(u#O+llQuQdz0=
zdnMpI0d67wzm_O2W)&qkHtOpEx}~0o0}@SCX${>I%_Qb3Cv~i^;|JB
z39i)6?(&^mj1{30;j$Zv6JAyA;mxvpsD>2M1Hz!l$Fu+@gJLP@rC_B<_LIPQE4gX-
zj5Dj8=vz~5Dso572}#+;fb$unhv*A6by8m^ML&mW{wmYqFw_X94^oTEy_xY$G(9#?
zvj8j+UcM}?I(hj@9}nsQN_Y9);17chz|`!#yJ%dTX)5P=Hg|6f*05AvXx`aU@tdk`((mk*0lyHhOen#eNec~;a3#p-5ME^D4~a(ZKa1>=
zOtCZ1o$g)Bub(JxD8PsqKKt6(M9dfZX9$LPjMRU|H8%&KiJv7+sJD!tzz1YT7C({b
zweySuQaPG??KSKjXcK%b*>f@Tu^!e#P`cbGk0G3Wbi4V)<&2H98jK*=%Whs59j%`-
zDa}?x0ie_qMRidLQYn2=wVx$IX<;KZs=h)O*C1$3zubULS~I@AEJn}y_snVkLsT;C
znFKp%U$bO(?6(jbGKxenCT9eaR)sGYIyWrZ?*0N((dJSAX84q&zX3>Urb9g8E<=o)@6KnYARljD0S?+Dpb0~oaKd94C~;W#UG#77
zP+D~R@+1`m9m&|kjof25M5dH5I`7}b`J(;Cyxno?zEwvN9Ed*-V4Q)enY!iw^@jQt
z**LNp*WohT_3E&W^NTKN+)M(;cuf33uU!BE$+GpdoJU6Xq;~38^2uTxm`Hxdl(5Cg
zyjXG_Df`3sOeAU?g9o|L)*54QU3D^j8qqxLMv6x4
zwj9h!(HfVFW$b^JXko&d+NVyV2<#79u;m^%jDu325hJiz$-jjj5wjlI_D)MD8OIzO
zl+L;jqh?(*ioXCFKii?|yakbne05@89>@Ol`gCUyyeRqL=NR|Z+QPrr1IZ9^UNgDR
zA3=GSP5Qf3G5Ds`B7;o6oUt@#@M(cL+g9RyBx!jZ0VOd@d;RN|-B^)B9K1XNie|55
zJW-yJP=iYO$P1|)j%&yW37Q3gR~_R2LQwwA+EyE!3dvX7Nqo%TAa~#OQ}jd#HBk%#
zFl~j_q9_Xo+7$)dfC$ulaHw#)oi5=F3lB(X;I*g$xJ2y(VmoYzvzaV%Ys@jqjxK|?
zvCV>_c4Hox%vi{6E$K;u&JD08in5_+NeXCe8;5h_T(qH^SaU2h8qi&^mbR#-jMv1K4ai@eS$9u@!B4Dfqob9BK!
zlevRUK8O7(n(fLuWbmW4J2P0xSyc_WrjRpLhO-e1Rv3>89epyKPbFBHfGr`+dzhuQ4;)6ykXmOgX6%N^E$U
z#}^wsfyZuJwU@GK_y3gG@j$kJM#C}zsUF*K1>>%WvJ@nxKn877_cZF}52(LMk?9KU
zfd3RCL|NyZl(wd)%6y>V!{EqDF8%OBRHnwRB0^*kx==Zx+uVoBUkjH<{%+6{1Zf(o
z!b?UIT9d&MCnh_3H@2Wk7(S7e4uMJ+1Y86qDIwP%l(pbgmfl6L`T_ihgh95UdZPUC>_dn
zih{rKp@nnY;lr!CQ&EG5^h9REv)wqnlP3+1;F9!iiukI4gnRG3&869^n@J3*&$H3_
z3eM&&0==;#Wv8tntYz9;Z_0G@2(3^~(lXN>VWp>nvvZQ8Vt;ot7sM3C)`)>HWx8=1
z(l6C0(qwEfu#F6;n_)`9!AIE1sJLbirdzOIMRtbhiXBs8h2^dfY$6ws5CLVR$gk?1Ws04sP0@yKU+a;rh_#guWCZ694h?yEjTJ$D(!;kA`=CxWpvV&ofZm&d{yteaIZyF8o7>D1m
zTCyu%3nm^M!|gMV6*ka`%&iUW*Tj@6{q+X)`Y%sik7q5)Q{U6UjOhJ^5dN6}ofXe1
zregejUiHV#Mm2!((-)3}0gME+FELgaa3vTfRzWill2(3UM>qPgpaeV&^>Q*e)RO&e
zr*A+i&u#BVqc&Bx6+XfocPJ(6lsR-dXo>Ua3rW#9VPO^Prl%%owLKWM@76;lUkVb!
ztQe_3NmOM2E|Q8cpznbOSFhr$J7hXg`YN;plpJ#s$j1MN!`?mSH?(A(K
z{Em*TZm_DyOu<~r@f}Rr0>g?c9a8j32(@{k4q|G&I=k(UZH_iOvzn{mGQjZ3v4Sf9-}j&PaA5FlH5-D7YM
zzkiAU^lanpvsXwuPaG*&H~M9x$=q;$G1A;}S2ukj&NtWW8*s-GHFnG7FS&mQyon_y
zcPm`+pK&qG&r47=hs&znXI|Z|;m2wWe+M4yJ`+s{b>HyInGcjIYF&9__qfUL4&b%~
zL`2@qF1*=U#Bgnn@~MsjpIg7;2n!c4o5?N7-%k&|7`@5GI?XQsvXnQKEIHb3v-hV)
zPQ4aS^`fFY#tjZFVW5q)P|l35b>iuWiOFu5cW6vLU&;THw$Ku?9+%ZsgJ4^RmpX7l
z=9gdXe~I*fX|$rl4rqcO{n}M8t&O%f_!JTI`5WaM0bvfFI@m5$r
zcQB9`NRh~OkCv>V$3J