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 <>>;
+}
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 && {data.adjustment_history.slice(-10).map((item) => - {item.rate.toFixed(2)}%
)}
};
+}
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;