diff --git a/app/kyc/renew/page.tsx b/app/kyc/renew/page.tsx new file mode 100644 index 0000000..e28778e --- /dev/null +++ b/app/kyc/renew/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { KycDocumentStatusDashboard } from "@/components/kyc/KycDocumentStatusDashboard"; + +export default function KycRenewPage() { + return

Renew investor accreditation

Replace rejected or expired evidence. Each document returns to Under Review after submission.

; +} diff --git a/app/kyc/status/page.tsx b/app/kyc/status/page.tsx index 5b805e7..fe5f299 100644 --- a/app/kyc/status/page.tsx +++ b/app/kyc/status/page.tsx @@ -10,5 +10,5 @@ export default function KycStatusPage() { const { data, isLoading } = useQuery<{ status: KS; submittedAt: string; reviewedAt?: string; rejectionReason?: string }>({ queryKey: ["kyc-status"], queryFn: async () => { const r = await fetch("/api/kyc/status"); if (!r.ok) throw new Error("Failed"); return r.json(); }, refetchInterval: 60_000 }); if (isLoading) return
Loading…
; const s = data?.status ?? "pending"; const cfg = C[s]; - return (
KYC Verification Status
{cfg.l}

Submitted: {data?.submittedAt ? new Date(data.submittedAt).toLocaleDateString() : "—"}

{s === "rejected" && data?.rejectionReason &&

Rejection reason:

{data.rejectionReason}

}{s === "rejected" && }{s === "pending" &&

Refreshes every 60s.

}
); + return (
KYC Verification Status
{cfg.l}

Submitted: {data?.submittedAt ? new Date(data.submittedAt).toLocaleDateString() : "—"}

{s === "rejected" && data?.rejectionReason &&

Rejection reason:

{data.rejectionReason}

}{s === "rejected" && }{s === "pending" &&

Refreshes every 60s.

}
); } diff --git a/components/dashboard/InvestorPortfolioPage.tsx b/components/dashboard/InvestorPortfolioPage.tsx index 3570f5d..da3779f 100644 --- a/components/dashboard/InvestorPortfolioPage.tsx +++ b/components/dashboard/InvestorPortfolioPage.tsx @@ -15,6 +15,7 @@ import { FractionTransferModal } from "@/components/dashboard/FractionTransferMo import { SettlementCountdown } from "@/components/marketplace/SettlementCountdown"; import { Download, ExternalLink } from "lucide-react"; import { PortfolioRiskIndicator } from "@/components/invoices/InvoiceRiskAlert"; +import { ListPositionButton } from "@/components/dashboard/ListPositionButton"; // ─── Types ───────────────────────────────────────────────────────────────── @@ -153,7 +154,7 @@ function HoldingsTab() {
- +
diff --git a/components/dashboard/ListPositionButton.tsx b/components/dashboard/ListPositionButton.tsx new file mode 100644 index 0000000..5c705a2 --- /dev/null +++ b/components/dashboard/ListPositionButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { createListing } from "@/lib/listingFee"; +import { fetchListingFeeConfig, computeListingFee } from "@/lib/listingFee"; +import type { InvestmentPosition } from "@/lib/portfolio"; + +export function ListPositionButton({ position }: { position: InvestmentPosition }) { + const [open, setOpen] = useState(false); + const [quantity, setQuantity] = useState(String(Math.max(0, (position.quantity ?? 0) - (position.listed_quantity ?? 0)))); + const [price, setPrice] = useState(""); + const fee = useQuery({ queryKey: ["listing-fee"], queryFn: () => fetchListingFeeConfig(), enabled: open }); + const qty = Number(quantity); const ask = Number(price); + const preview = computeListingFee(qty, ask, fee.data?.fee_bps ?? 0); + const mutation = useMutation({ mutationFn: () => createListing({ invoiceId: position.invoice_id, quantity: qty, askPrice: ask }), onSuccess: () => setOpen(false) }); + const locked = Boolean(position.lockup_expires_at && new Date(position.lockup_expires_at).getTime() > Date.now()); + const available = Math.max(0, (position.quantity ?? 0) - (position.listed_quantity ?? 0)); + return <>List {position.invoice_title} for resale{locked ?

This position is locked until {new Date(position.lockup_expires_at!).toLocaleString()}.

:

Available fractions: {available}

setQuantity(e.target.value)} />
setPrice(e.target.value)} />
Gross
{preview.gross.toFixed(2)} XLM
Platform fee
{preview.fee.toFixed(2)} XLM
Net proceeds
{preview.netProceeds.toFixed(2)} XLM
}
; +} diff --git a/components/invoices/DynamicYieldCard.tsx b/components/invoices/DynamicYieldCard.tsx new file mode 100644 index 0000000..13b7a8c --- /dev/null +++ b/components/invoices/DynamicYieldCard.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { fetchDynamicYield } from "@/lib/api"; + +export function DynamicYieldCard({ wallet, invoiceRate }: { wallet: string; invoiceRate?: number | null }) { + const query = useQuery({ queryKey: ["dynamic-yield", wallet], queryFn: () => fetchDynamicYield(wallet), enabled: Boolean(wallet) }); + const data = query.data; + if (query.isLoading || query.isError || !data) return null; + const aboveBase = data.current_rate >= data.base_rate; + return Dynamic yield
{data.current_rate.toFixed(2)}% current issuer rate

Base rate {data.base_rate.toFixed(2)}% · {data.settlement_count} on-time settlements

{invoiceRate != null &&

This invoice was priced at {invoiceRate.toFixed(2)}%.

}{data.adjustment_history.length > 0 && }
; +} diff --git a/components/invoices/InvoiceDetailView.tsx b/components/invoices/InvoiceDetailView.tsx index 9e340e7..4d636f0 100644 --- a/components/invoices/InvoiceDetailView.tsx +++ b/components/invoices/InvoiceDetailView.tsx @@ -56,6 +56,7 @@ import { truncateAddress } from "@/lib/stellar"; import { DEFAULT_MIN_INVESTMENT } from "@/lib/invoiceDefaults"; import { Money } from "@/components/currency"; import { CurrencyToggle } from "@/components/layout/CurrencyToggle"; +import { DynamicYieldCard } from "@/components/invoices/DynamicYieldCard"; function InvoiceDetailSkeleton() { return ( @@ -424,6 +425,8 @@ export function InvoiceDetailView({ invoiceId }: InvoiceDetailViewProps) { + + {/* Invest CTA — KYC gated. */} {isOpenForInvestment ? ( { + const res = await fetch(`${API_BASE}/issuers/${encodeURIComponent(wallet)}/dynamic-yield`); + if (res.status === 404) return null; + if (!res.ok) throw new Error("Failed to fetch issuer yield history"); + return res.json(); +} + export interface InvoicesResponse { invoices: Invoice[]; has_more: boolean;