Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { Metadata } from "next";

import { ProposalSection } from "@/features/governance/components/proposal-overview/ProposalSection";
import { Footer } from "@/shared/components/design-system/footer/Footer";
import type { DaoIdEnum } from "@/shared/types/daos";
import { HeaderSidebar, StickyPageHeader } from "@/widgets";
import { HeaderMobile } from "@/widgets/HeaderMobile";

type Props = {
params: Promise<{ daoId: string; proposalId: string }>;
Expand Down Expand Up @@ -35,24 +32,8 @@ export async function generateMetadata(props: Props): Promise<Metadata> {

export default function OffchainProposalPage() {
return (
<div className="bg-surface-background dark flex h-screen overflow-hidden">
<div className="active relative hidden h-screen lg:flex">
<div className="w-17 h-full shrink-0 overflow-y-auto">
<HeaderSidebar />
</div>
</div>
<main className="relative flex-1 overflow-auto">
<div className="lg:hidden">
<HeaderMobile withMobileMenu={false} />
<StickyPageHeader withMobileMenu={false} />
</div>
<div className="flex min-h-screen w-full flex-col items-center">
<div className="w-full flex-1">
<ProposalSection isOffchain />
</div>
<Footer />
</div>
</main>
<div>
<ProposalSection isOffchain />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { Metadata } from "next";

import { GovernanceSection } from "@/features/governance";
import { Footer } from "@/shared/components/design-system/footer/Footer";
import type { DaoIdEnum } from "@/shared/types/daos";
import { HeaderDAOSidebar, HeaderSidebar, StickyPageHeader } from "@/widgets";
import { HeaderMobile } from "@/widgets/HeaderMobile";

type Props = {
params: Promise<{ daoId: string }>;
Expand Down Expand Up @@ -33,29 +30,10 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
};
}

export default function DaoPage() {
export default function GovernancePage() {
return (
<div className="bg-surface-background dark flex h-screen overflow-hidden">
<div className="active relative hidden h-screen lg:flex">
<div className="h-full w-[68px] shrink-0 overflow-y-auto">
<HeaderSidebar />
</div>
<div className="h-full shrink-0">
<HeaderDAOSidebar />
</div>
</div>
<main className="relative flex-1 overflow-auto">
<div className="lg:hidden">
<HeaderMobile />
<StickyPageHeader />
</div>
<div className="flex min-h-screen w-full flex-col items-center">
<div className="xl4k:max-w-7xl w-full flex-1">
<GovernanceSection />
</div>
<Footer />
</div>
</main>
<div>
<GovernanceSection />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { Metadata } from "next";

import { ProposalSection } from "@/features/governance/components/proposal-overview/ProposalSection";
import { Footer } from "@/shared/components/design-system/footer/Footer";
import type { DaoIdEnum } from "@/shared/types/daos";
import { HeaderSidebar, StickyPageHeader } from "@/widgets";
import { HeaderMobile } from "@/widgets/HeaderMobile";

type Props = {
params: Promise<{ daoId: string; proposalId: string }>;
Expand Down Expand Up @@ -35,24 +32,8 @@ export async function generateMetadata(props: Props): Promise<Metadata> {

export default function ProposalPage() {
return (
<div className="bg-surface-background dark flex h-screen overflow-hidden">
<div className="active relative hidden h-screen lg:flex">
<div className="h-full w-[68px] shrink-0 overflow-y-auto">
<HeaderSidebar />
</div>
</div>
<main className="relative flex-1 overflow-auto">
<div className="lg:hidden">
<HeaderMobile withMobileMenu={false} />
<StickyPageHeader withMobileMenu={false} />
</div>
<div className="flex min-h-screen w-full flex-col items-center">
<div className="w-full flex-1">
<ProposalSection />
</div>
<Footer />
</div>
</main>
<div>
<ProposalSection />
</div>
);
}
2 changes: 0 additions & 2 deletions apps/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Toaster } from "react-hot-toast";

import { CookieConsent } from "@/features/cookie";
import { HelpPopover } from "@/shared/components";
import { ShutterAttackBanner } from "@/shared/components/banners/ShutterAttackBanner";
import { GlobalProviders } from "@/shared/providers/GlobalProviders";
import ConditionalPostHog from "@/shared/services/posthog/ConditionalPostHog";
import UmamiScript from "@/shared/services/umami";
Expand Down Expand Up @@ -73,7 +72,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
className="border-light-dark mx-auto max-w-screen-2xl overflow-x-hidden border xl:overflow-hidden"
>
<GlobalProviders>
<ShutterAttackBanner />
{children}
<CookieConsent />
<HelpPopover />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const GovernanceSection = () => {
}

return (
<div className="bg-background flex min-h-screen flex-col">
<div>
<TheSectionLayout
title="Proposals"
icon={<Landmark className="section-layout-icon" />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"use client";

import { Check, Hourglass, PenLine } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useAccount, useWalletClient } from "wagmi";

import type { ProposalViewData } from "@/features/governance/types";
import { showCustomToast } from "@/features/governance/utils/showCustomToast";
import {
executeProposal,
queueProposal,
type GovernanceAction,
} from "@/features/governance/utils/submitGovernanceAction";
import { Modal } from "@/shared/components/design-system/modal/Modal";
import { SpinIcon } from "@/shared/components/icons/SpinIcon";
import { DividerDefault } from "@/shared/components/design-system/divider/DividerDefault";
import { cn } from "@/shared/utils/cn";
import daoConfigByDaoId from "@/shared/dao-config";
import type { DaoIdEnum } from "@/shared/types/daos";

type ActionStep = "waiting-signature" | "pending-tx" | "success" | "error";

interface GovernanceActionModalProps {
isOpen: boolean;
onClose: () => void;
action: GovernanceAction;
proposal: ProposalViewData;
daoId: DaoIdEnum;
}

export const GovernanceActionModal = ({
isOpen,
onClose,
action,
proposal,
daoId,
}: GovernanceActionModalProps) => {
const [step, setStep] = useState<ActionStep>("waiting-signature");
const [error, setError] = useState<string | null>(null);

const { address } = useAccount();
const chain = daoConfigByDaoId[daoId].daoOverview.chain;
const { data: walletClient } = useWalletClient({ chainId: chain.id });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle missing chain-specific wallet client in action modal

The modal requests a wallet client pinned to chain.id, but the action flow only auto-starts when that client exists; otherwise it silently stays on the “confirm in your wallet” step forever. In practice, users connected to the wrong network get no error or switch-network prompt and cannot queue/execute until they infer the issue themselves. Adding explicit chain-mismatch handling (e.g., show an error/CTA) avoids this dead-end.

Useful? React with 👍 / 👎.


const title = action === "queue" ? "Confirm Queue" : "Confirm Execution";
const confirmLabel =
action === "queue"
? "Confirm queuing in your wallet"
: "Confirm execution in your wallet";

const handleAction = useCallback(async () => {
if (!address || !walletClient) return;
if (step === "pending-tx" || step === "success") return;

setError(null);
setStep("waiting-signature");

try {
const handler = action === "queue" ? queueProposal : executeProposal;
await handler(
proposal.targets,
proposal.values,
proposal.calldatas ?? [],
proposal.description,
address,
daoId,
walletClient,
() => setStep("pending-tx"),
proposal.id,
);
setStep("success");
showCustomToast(
action === "queue"
? "Proposal queued successfully!"
: "Proposal executed successfully!",
"success",
);
onClose();
setTimeout(() => window.location.reload(), 2000);
} catch (err) {
const message = err instanceof Error ? err.message : "Action failed.";
console.error("[GovernanceActionModal]", err);
let shortMessage: string;
if (message.includes("rejected") || message.includes("denied")) {
shortMessage = "Transaction rejected by user.";
} else if (message.includes("reverted")) {
shortMessage =
"Contract call reverted. The proposal may not be in the correct state.";
} else {
shortMessage =
message.split("\n")[0]?.slice(0, 100) ?? "Action failed.";
}
setError(shortMessage);
setStep("error");
}
}, [address, walletClient, step, action, proposal, daoId, onClose, chain]);

useEffect(() => {
if (!isOpen || !walletClient || step !== "waiting-signature") return;
handleAction();
}, [isOpen, walletClient, handleAction, step]);

const handleClose = () => {
setStep("waiting-signature");
setError(null);
onClose();
};

return (
<Modal
open={isOpen}
onOpenChange={(open) => !open && handleClose()}
title={title}
>
{/* Proposal info */}
<div className="flex flex-col gap-2 pb-4">
<div className="flex items-start gap-2">
<span className="text-secondary w-32 shrink-0 text-xs font-medium leading-5">
Proposal ID
</span>
<span className="text-primary text-sm leading-5">{proposal.id}</span>
</div>
<div className="flex items-start gap-2">
<span className="text-secondary w-32 shrink-0 text-xs font-medium leading-5">
Proposal name
</span>
<span className="text-primary text-sm leading-5">
{proposal.title}
</span>
</div>
</div>

{/* Stepper */}
<div className="border-border-default flex flex-col gap-1.5 border p-3">
<StepRow
done={step === "success" || step === "pending-tx"}
active={step === "waiting-signature"}
icon={<PenLine className="text-primary size-3.5" />}
label={confirmLabel}
error={step === "error" ? error : undefined}
/>

<DividerDefault isVertical className="ml-3.5 h-6 w-0.5" />

<StepRow
done={step === "success"}
active={step === "pending-tx"}
icon={<Hourglass className="text-primary size-3.5" />}
label="Wait for transaction to complete"
/>
</div>
</Modal>
);
};

interface StepRowProps {
done: boolean;
active: boolean;
icon: React.ReactNode;
label: string;
error?: string | null;
}

const StepRow = ({ done, active, icon, label, error }: StepRowProps) => {
const getBackgroundColor = () => {
if (done) return "bg-surface-opacity-success";
if (active) return "bg-primary";
return "bg-border-default";
};

return (
<div className="flex w-full flex-col gap-1">
<div className="flex w-full items-center gap-2">
<div className="relative flex size-8 shrink-0 items-center justify-center">
{active && (
<SpinIcon className="text-warning absolute inset-0 size-8 animate-spin" />
)}
<div
className={cn(
"flex size-6 shrink-0 items-center justify-center rounded-full",
getBackgroundColor(),
)}
>
<div className="border-border-default flex items-center justify-center rounded-full border p-1">
{done ? <Check className="text-success size-3.5" /> : icon}
</div>
</div>
</div>
<p className="text-primary text-sm leading-5">{label}</p>
</div>

{error && (
<p className="text-error ml-11 break-words text-xs leading-4">
{error}
</p>
)}
</div>
);
};
Loading
Loading