diff --git a/components/organization/bounties/manage/BountyManagementDashboard.tsx b/components/organization/bounties/manage/BountyManagementDashboard.tsx
index 09f792d3..448ec9f5 100644
--- a/components/organization/bounties/manage/BountyManagementDashboard.tsx
+++ b/components/organization/bounties/manage/BountyManagementDashboard.tsx
@@ -3,7 +3,7 @@
import { useState } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
-import { ArrowLeft, Info, Loader2, Lock } from 'lucide-react';
+import { ArrowLeft, Info, Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -27,6 +27,7 @@ import { ordinal } from '@/lib/utils';
import BountySubmissionsPanel from './BountySubmissionsPanel';
import BountyPayoutPanel from './BountyPayoutPanel';
import BountyApplicationsPanel from './BountyApplicationsPanel';
+import BountySettingsPanel from './BountySettingsPanel';
export default function BountyManagementDashboard() {
const params = useParams<{ id: string; bountyId: string }>();
@@ -183,7 +184,11 @@ export default function BountyManagementDashboard() {
/>
-
+
@@ -354,13 +359,3 @@ function Row({
);
}
-
-function TabPlaceholder({ title, issue }: { title: string; issue: string }) {
- return (
-
-
-
{title}
-
Coming soon ({issue}).
-
- );
-}
diff --git a/components/organization/bounties/manage/BountySettingsPanel.tsx b/components/organization/bounties/manage/BountySettingsPanel.tsx
new file mode 100644
index 00000000..8d1c8c62
--- /dev/null
+++ b/components/organization/bounties/manage/BountySettingsPanel.tsx
@@ -0,0 +1,177 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ AlertTriangle,
+ CheckCircle2,
+ ExternalLink,
+ Loader2,
+} from 'lucide-react';
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { BoundlessButton } from '@/components/buttons';
+import {
+ ESCROW_PHASE_LABEL,
+ type BountyOperateOverview,
+} from '@/features/bounties';
+import { useBountyCancel } from '@/hooks/use-bounty-cancel';
+import { getTransactionExplorerUrl } from '@/lib/wallet-utils';
+
+const PHASE_LABEL = {
+ ...ESCROW_PHASE_LABEL,
+ starting: 'Preparing cancellation…',
+ polling: 'Refunding escrow on-chain…',
+};
+
+/** Lifecycle statuses that mean the bounty can no longer be cancelled. */
+const TERMINAL_STATUSES = new Set(['completed', 'cancelled']);
+
+const CONFIRM_PHRASE = 'CANCEL';
+
+export default function BountySettingsPanel({
+ organizationId,
+ bountyId,
+ overview,
+}: {
+ organizationId: string;
+ bountyId: string;
+ overview: BountyOperateOverview;
+}) {
+ const cancelOp = useBountyCancel({ organizationId, bountyId });
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [confirmText, setConfirmText] = useState('');
+
+ const isCancelled = overview.status === 'cancelled';
+ const isTerminal = TERMINAL_STATUSES.has(overview.status);
+ // A draft has no on-chain escrow to refund; cancel only applies once funded.
+ const isPublished = !!overview.escrowEventId;
+ const running = cancelOp.isRunning;
+
+ return (
+
+
+
+
+
+
+ Cancel this bounty
+
+
+ Cancelling closes the bounty and refunds the locked escrow.
+ Contributors are refunded first, then any residual returns to you
+ as the owner. This runs once on-chain and cannot be undone.
+
+
+ {cancelOp.isCompleted || isCancelled ? (
+
+
+
+ This bounty is cancelled and the escrow was refunded.
+