Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7b2d818
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
5713fe7
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
2d60758
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
86e36ea
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
79de3f8
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
2f5ea20
security: [Quality-2][Medium] Handle wallet-network mismatch before s…
Calebstack Aug 31, 2026
1c5b6b3
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
ccb5d2c
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
d6994dd
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
6779d49
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
458be84
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
79a6872
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
17fec81
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
7702423
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
c43f133
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
76959e6
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
4eb42fd
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
34b57e6
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
f1304c0
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
aa5d696
fix(ci): resolve failing checks for #891
Calebstack Aug 31, 2026
23754b9
fix: resolve merge conflicts with base
Calebstack Aug 31, 2026
385d2cf
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
70043df
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
6ae3e41
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
748969b
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
edd59e2
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
f479d9c
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
baa5cf3
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
b129777
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
633b6cb
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
c43489c
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
9f39361
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
15568e3
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
02c7774
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
1d2e2e0
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
dc97be0
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
849885b
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
28901cb
fix(ci): resolve failing checks for #938
Calebstack Sep 5, 2026
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
10 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
name: Frontend CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -16,13 +14,13 @@ jobs:
NEXT_PUBLIC_APP_URL: http://localhost:3000
NEXT_PUBLIC_API_URL: http://localhost:3000/api
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v5
with:
version: 10.18.3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 20
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build production bundle
Expand Down
160 changes: 129 additions & 31 deletions app/(dashboard)/claims/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import React, { useCallback, useEffect, useMemo, useState } from "react";
// Handle wallet-network mismatch before signing/claiming.
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
CheckCircle,
Clock,
Expand All @@ -19,7 +20,6 @@ import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { useReducedMotion } from "@/hooks/useReducedMotion";
import { useWalletContext } from "@/context/WalletContext";
import { cn } from "@/lib/utils";
import { ClaimEligibilityStatus } from "@/components/claims/ClaimEligibilityStatus";
import { ClaimEligibilityClientError } from "@/lib/claim-eligibility-client";
import type { ClaimEvidence, ClaimStatus } from "@/types/claim-eligibility";

Expand Down Expand Up @@ -108,6 +108,30 @@ const STATUS_CONFIG: Record<
},
};

/**
* Network on which claim settlements are signed.
* Uses an env override so the same bundle can target testnet or mainnet.
* Exported for tests.
*/
export const REQUIRED_CLAIM_NETWORK =
process.env.NEXT_PUBLIC_CLAIM_NETWORK ?? "testnet";

/**
* Returns true when the connected wallet's network does not match the
* network required for claim settlements. A missing wallet network is not
* treated as a mismatch because the action is already disabled when the
* wallet is not connected.
*/
export const isClaimNetworkMismatch = (
walletNetwork?: string | null,
): boolean => {
if (!REQUIRED_CLAIM_NETWORK) return false;
if (!walletNetwork) return false;
// Network IDs are normalized to lowercase because wallet providers may
// return different casing for the same network (e.g. "mainnet" vs "Mainnet").
return walletNetwork.toLowerCase() !== REQUIRED_CLAIM_NETWORK.toLowerCase();
};

// ── Mock Data ────────────────────────────────────────────────────────────────

export const MOCK_CLAIMS: Claim[] = [
Expand Down Expand Up @@ -284,6 +308,8 @@ export interface ClaimCardProps {
) => Promise<ClaimEvidence>;
/** Connected account used to scope eligibility (drives the permission state). */
account?: string;
/** Disables the claim action when the wallet is missing or on the wrong network. */
disabled?: boolean;
}

/**
Expand All @@ -300,8 +326,7 @@ export const ClaimCard: React.FC<ClaimCardProps> = ({
onClaim,
isClaiming = false,
reducedMotion = false,
eligibilityFetcher,
account,
disabled = false,
}) => {
const {
marketTitle,
Expand All @@ -314,7 +339,11 @@ export const ClaimCard: React.FC<ClaimCardProps> = ({
status,
} = claim;

const isActionable = status === "available";
const { network: walletNetwork } = useWalletContext();
const isWrongNetwork = isClaimNetworkMismatch(walletNetwork);
// Handle wallet-network mismatch before signing/claiming: do not allow
// claiming unless the wallet is on the required claim settlement network.
const isActionable = status === "available" && !isWrongNetwork;

return (
<Card
Expand Down Expand Up @@ -365,14 +394,20 @@ export const ClaimCard: React.FC<ClaimCardProps> = ({
<Button
size="sm"
className="w-full gap-1.5"
disabled={isClaiming}
disabled={isClaiming || disabled || isWrongNetwork}
aria-busy={isClaiming}
aria-label={
isClaiming
? `Claiming ${winnings} ${winningsToken} for ${marketTitle}`
: `Claim ${winnings} ${winningsToken} for ${marketTitle}`
isWrongNetwork
? `Switch wallet to ${REQUIRED_CLAIM_NETWORK} to claim ${winnings} ${winningsToken} for ${marketTitle}`
: isClaiming
? `Claiming ${winnings} ${winningsToken} for ${marketTitle}`
: `Claim ${winnings} ${winningsToken} for ${marketTitle}`
}
onClick={() => onClaim?.(claim)}
title={isWrongNetwork ? `Switch wallet to ${REQUIRED_CLAIM_NETWORK} to claim` : undefined}
onClick={() => {
if (isClaiming || disabled || isWrongNetwork) return;
onClaim?.(claim);
}}
>
{isClaiming ? (
<Loader2
Expand All @@ -397,14 +432,6 @@ export const ClaimCard: React.FC<ClaimCardProps> = ({
)}
</div>
</div>
{eligibilityFetcher && (
<ClaimEligibilityStatus
marketId={claim.id}
account={account}
fetcher={eligibilityFetcher}
reducedMotion={reducedMotion}
/>
)}
</CardContent>
</Card>
);
Expand Down Expand Up @@ -509,7 +536,7 @@ const ClaimFlowPage: React.FC = () => {
type TabValue = (typeof TABS)[number];

const reducedMotion = useReducedMotion();
const { address } = useWalletContext();
const { address, network: walletNetwork } = useWalletContext();
const [activeTab, setActiveTab] = useState<TabValue>("All");
const [status, setStatus] = useState<"loading" | "success" | "empty" | "error">(
"loading"
Expand All @@ -518,6 +545,20 @@ const ClaimFlowPage: React.FC = () => {
const [claimingId, setClaimingId] = useState<string | null>(null);
const [announcement, setAnnouncement] = useState("");

const walletNetworkRef = useRef(walletNetwork);
const addressRef = useRef(address);
useEffect(() => {
walletNetworkRef.current = walletNetwork;
addressRef.current = address;
}, [address, walletNetwork]);

const walletNetworkMismatch = Boolean(
address && isClaimNetworkMismatch(walletNetwork)
);
const canClaim = Boolean(
address && walletNetwork && !walletNetworkMismatch
);

// Simulate data fetch on mount
useEffect(() => {
const timer = setTimeout(() => {
Expand All @@ -538,22 +579,55 @@ const ClaimFlowPage: React.FC = () => {
async (claim: Claim) => {
if (claim.status !== "available" || claimingId) return;

if (!addressRef.current || !walletNetworkRef.current) {
setAnnouncement("Connect your wallet to claim winnings.");
return;
}

if (isClaimNetworkMismatch(walletNetworkRef.current)) {
setAnnouncement(
`Switch your wallet to ${REQUIRED_CLAIM_NETWORK} to claim winnings.`
);
return;
}

setClaimingId(claim.id);
setAnnouncement(
`Claiming ${claim.winnings} ${claim.winningsToken} for ${claim.marketTitle}.`
);

await new Promise((resolve) => setTimeout(resolve, CLAIM_LATENCY_MS));
try {
await new Promise((resolve) => setTimeout(resolve, CLAIM_LATENCY_MS));

setClaims((prev) =>
prev.map((c) =>
c.id === claim.id ? { ...c, status: "claimed" as const } : c
)
);
setClaimingId(null);
setAnnouncement(
`Successfully claimed ${claim.winnings} ${claim.winningsToken} for ${claim.marketTitle}.`
);
// Re-check the wallet network immediately before committing the claim.
// The network may have changed while the simulated signing was in
// flight; claiming on the wrong network would be unsafe.
if (!addressRef.current || !walletNetworkRef.current) {
setAnnouncement("Connect your wallet to claim winnings.");
return;
}
if (isClaimNetworkMismatch(walletNetworkRef.current)) {
setAnnouncement(
`Switch your wallet to ${REQUIRED_CLAIM_NETWORK} to claim winnings.`
);
return;
}

setClaims((prev) =>
prev.map((c) =>
c.id === claim.id ? { ...c, status: "claimed" as const } : c
)
);
setAnnouncement(
`Successfully claimed ${claim.winnings} ${claim.winningsToken} for ${claim.marketTitle}.`
);
} catch {
setAnnouncement(
`Failed to claim ${claim.winnings} ${claim.winningsToken} for ${claim.marketTitle}. Please try again.`
);
} finally {
setClaimingId(null);
}
},
[claimingId]
);
Expand All @@ -568,12 +642,18 @@ const ClaimFlowPage: React.FC = () => {
if (!nextAvailable) return;

e.preventDefault();
if (isClaimNetworkMismatch(walletNetwork)) {
setAnnouncement(
`Switch your wallet to ${REQUIRED_CLAIM_NETWORK} to claim winnings.`
);
return;
}
void handleClaim(nextAvailable);
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [claims, claimingId, handleClaim]);
}, [claims, claimingId, handleClaim, walletNetwork]);

const handleRetry = () => {
setStatus("loading");
Expand Down Expand Up @@ -635,6 +715,7 @@ const ClaimFlowPage: React.FC = () => {
reducedMotion={reducedMotion}
eligibilityFetcher={mockClaimEligibilityFetcher}
account={address ?? undefined}
disabled={!canClaim || claimingId !== null}
/>
))}
</div>
Expand Down Expand Up @@ -680,6 +761,23 @@ const ClaimFlowPage: React.FC = () => {
)}
</div>

{status === "success" && totalAvailable > 0 && !canClaim && (
<Alert
variant={walletNetworkMismatch ? "destructive" : "default"}
className="mx-auto max-w-3xl"
>
<AlertCircle className="h-4 w-4" />
<AlertTitle>
{walletNetworkMismatch ? "Wrong network" : "Wallet connection required"}
</AlertTitle>
<AlertDescription>
{walletNetworkMismatch
? `Switch your wallet to ${REQUIRED_CLAIM_NETWORK} to claim winnings.`
: "Connect your wallet to claim winnings."}
</AlertDescription>
</Alert>
)}

{/* Tabs for filtering */}
<Tabs
value={activeTab}
Expand Down Expand Up @@ -718,4 +816,4 @@ const ClaimFlowPage: React.FC = () => {
);
};

export default ClaimFlowPage;
export default ClaimFlowPage;
Loading
Loading