Skip to content
Merged

Dev #27

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
29e6a8e
fix: upgrade mainnet registry for identify support, preserve agents o…
Tranquil-Flow Mar 12, 2026
b127413
fix: prevent false-positive registration success for pre-existing age…
Tranquil-Flow Mar 12, 2026
797dd7e
fix: move intervalRef declaration before async IIFE to fix block-scop…
Tranquil-Flow Mar 12, 2026
9b43cca
fix: clear agents only when switching to a different lookup tab
Tranquil-Flow Mar 12, 2026
b0b9a06
fix: passport identify fallback for pre-upgrade agents, wallet persis…
Tranquil-Flow Mar 12, 2026
e5d9a50
add hosted scan page and server-side QR rendering for agent flows
Tranquil-Flow Mar 17, 2026
073b679
Simplify registration page with two-column layout and agent bootstrap…
Tranquil-Flow Mar 18, 2026
5c699ae
Add Celo Agent Visa integration — API route, VisaCard component, and …
Tranquil-Flow Mar 18, 2026
54136f1
Add agent detail page with visa card and tier badges on agent list
Tranquil-Flow Mar 18, 2026
6c503de
Add batch visa tier endpoint, action buttons, and fix N+1 badge loading
Tranquil-Flow Mar 18, 2026
dc57baa
Fix eligibility key mismatch, normalize USD values, add scoring servi…
Tranquil-Flow Mar 18, 2026
b998e1a
Fix agent detail page params for Next.js 14 (use direct params instea…
Tranquil-Flow Mar 18, 2026
27de50a
fix: QR code not rendering on register page, increase size, hot-reloa…
Tranquil-Flow Mar 18, 2026
2fd5dfb
Merge branch 'feat/celo-agent-visa-ui' into dev
Tranquil-Flow Mar 18, 2026
45a8a08
fix: CLI register page stuck on "Preparing QR code" due to SDK load r…
Tranquil-Flow Mar 18, 2026
cb78efc
Wire up visa claim flow, redeploy contract on Sepolia with real registry
Tranquil-Flow Mar 19, 2026
c34f1f7
Clickable explorer link for visa claim tx, simplify refresh button
Tranquil-Flow Mar 19, 2026
4853673
Auto-refresh agents when returning to tab
Tranquil-Flow Mar 19, 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
4 changes: 4 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ NEXT_PUBLIC_PRIVY_APP_ID=
# NEXT_PUBLIC_RPC_CELO=https://celo-mainnet.infura.io/v3/YOUR_KEY
# NEXT_PUBLIC_RPC_CELO_SEPOLIA=https://celo-sepolia.infura.io/v3/YOUR_KEY

# Celo Agent Visa scoring service URL (for Check Eligibility / Claim Upgrade buttons)
# Leave empty to disable scoring service integration (visa card still shows on-chain data)
# NEXT_PUBLIC_SCORING_SERVICE_URL=http://localhost:3001

# NOTE: Contract addresses are hardcoded in lib/network.ts.
# Update that file directly on contract redeploy — no env vars needed.

Expand Down
239 changes: 239 additions & 0 deletions app/app/agents/[agentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

"use client";

import React, { useState, useEffect } from "react";
import Link from "next/link";
import { Loader2, ArrowLeft, Copy, Check } from "lucide-react";
import { useNetwork } from "@/lib/NetworkContext";
import { Card } from "@/components/Card";
import { Badge } from "@/components/Badge";
import { VisaCard } from "@/components/VisaCard";

interface AgentInfo {
agentId: number;
chainId: number;
agentKey: string;
agentAddress: string;
isVerified: boolean;
proofProvider: string;
verificationStrength: number;
strengthLabel: string;
credentials: {
nationality: string;
olderThan: number;
ofac: boolean[];
};
registeredAt: number;
network: string;
}

function truncateAddress(address: string): string {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}

function formatDate(timestamp: number): string {
if (!timestamp) return "Unknown";
const date = new Date(timestamp * 1000);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}

function CopyableAddress({ address }: { address: string }) {
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
await navigator.clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<button
onClick={handleCopy}
className="inline-flex items-center gap-1.5 font-mono text-sm hover:text-accent transition-colors"
>
{truncateAddress(address)}
{copied ? (
<Check className="w-3.5 h-3.5 text-accent-success" />
) : (
<Copy className="w-3.5 h-3.5 text-muted" />
)}
</button>
);
}

export default function AgentDetailPage({
params,
}: {
params: { agentId: string };
}) {
const { agentId } = params;
const { network } = useNetwork();
const chainId = network.chainId;

const [agent, setAgent] = useState<AgentInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;

async function load() {
setLoading(true);
setError(null);

try {
const res = await fetch(`/api/agent/info/${chainId}/${agentId}`);

if (!res.ok) {
if (res.status === 404) {
setError("not_found");
} else {
setError("network");
}
return;
}

const data = (await res.json()) as AgentInfo;
if (!cancelled) setAgent(data);
} catch {
if (!cancelled) setError("network");
} finally {
if (!cancelled) setLoading(false);
}
}

void load();
return () => {
cancelled = true;
};
}, [chainId, agentId]);

if (loading) {
return (
<div className="flex items-center justify-center py-24">
<Loader2 className="w-6 h-6 animate-spin text-muted" />
</div>
);
}

if (error === "not_found") {
return (
<div className="max-w-2xl mx-auto space-y-6">
<Link
href="/agents"
className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Agents
</Link>
<Card>
<p className="text-sm text-muted">
Agent not found. The agent ID may be invalid or does not exist on
this network.
</p>
</Card>
</div>
);
}

if (error === "network" || !agent) {
return (
<div className="max-w-2xl mx-auto space-y-6">
<Link
href="/agents"
className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Agents
</Link>
<Card>
<p className="text-sm text-muted">
Failed to load agent. Please check your connection and try again.
</p>
</Card>
</div>
);
}

const nationality = agent.credentials?.nationality
?.replace(/[\x00-\x1f]/g, "")
.trim();

return (
<div className="max-w-2xl mx-auto space-y-6">
{/* Back link */}
<Link
href="/agents"
className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to Agents
</Link>

{/* Header */}
<div className="space-y-3">
<h1 className="text-2xl font-bold">Agent #{agent.agentId}</h1>
<div className="flex items-center gap-2 flex-wrap">
<Badge variant={agent.isVerified ? "success" : "error"}>
{agent.isVerified ? "Verified" : "Unverified"}
</Badge>
<Badge variant="muted">
{agent.network === "mainnet" ? "Mainnet" : "Testnet"}
</Badge>
</div>
</div>

{/* Agent Details */}
<Card>
<div className="space-y-4">
<h3 className="text-sm font-semibold">Agent Details</h3>

<div className="grid gap-3">
<div className="flex items-center justify-between">
<span className="text-xs text-muted">Address</span>
<CopyableAddress address={agent.agentAddress} />
</div>

<div className="flex items-center justify-between">
<span className="text-xs text-muted">Registered</span>
<span className="text-sm">{formatDate(agent.registeredAt)}</span>
</div>

{nationality && (
<div className="flex items-center justify-between">
<span className="text-xs text-muted">Nationality</span>
<span className="text-sm">{nationality}</span>
</div>
)}

<div className="flex items-center justify-between">
<span className="text-xs text-muted">Verification</span>
<span className="text-sm">
{agent.verificationStrength > 0
? `Level ${agent.verificationStrength}`
: "None"}
</span>
</div>
</div>
</div>
</Card>

{/* Celo Agent Visa */}
<div className="space-y-2">
<h3 className="text-sm font-semibold">Celo Agent Visa</h3>
<VisaCard
agentId={agent.agentId}
chainId={chainId}
blockExplorer={network.blockExplorer}
/>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion app/app/agents/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function AgentsLayout({
children: React.ReactNode;
}) {
return (
<main className="min-h-screen max-w-2xl mx-auto px-6 pt-24 pb-12">
<main className="min-h-screen mx-auto px-6 pt-24 pb-12">
<AgentsTabs />
{children}
</main>
Expand Down
Loading
Loading