From 3b64a4867557e71f949eb877a04f643d86ce46c9 Mon Sep 17 00:00:00 2001 From: neumattock <152253273+newmattock@users.noreply.github.com> Date: Tue, 26 May 2026 10:01:04 -0700 Subject: [PATCH] feat: improve LP funding confirmation flow --- app/governance/page.tsx | 8 ++- src/components/FundConfirmModal.tsx | 40 ++++++++--- src/components/LPDashboard.tsx | 1 + .../__tests__/FundConfirmModal.test.tsx | 72 +++++++++++++------ vitest.config.ts | 2 +- 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/app/governance/page.tsx b/app/governance/page.tsx index 87a40dd..013ee7e 100644 --- a/app/governance/page.tsx +++ b/app/governance/page.tsx @@ -21,6 +21,7 @@ function StatusBadge({ status }: { status: ProposalStatus }) { Active: { color: "bg-emerald-500/15 text-emerald-500 border-emerald-500/30", icon: "fiber_manual_record" }, Passed: { color: "bg-primary/15 text-primary border-primary/30", icon: "check_circle" }, Failed: { color: "bg-red-500/15 text-red-500 border-red-500/30", icon: "cancel" }, + Vetoed: { color: "bg-red-500/15 text-red-500 border-red-500/30", icon: "block" }, Executed: { color: "bg-purple-500/15 text-purple-500 border-purple-500/30", icon: "rocket_launch" }, Pending: { color: "bg-amber-500/15 text-amber-500 border-amber-500/30", icon: "schedule" }, }; @@ -172,10 +173,13 @@ export default function GovernancePage() { }, []); useEffect(() => { - load(); + const timeout = window.setTimeout(load, 0); // Refresh every 30 s for real-time vote counts const interval = setInterval(load, 30_000); - return () => clearInterval(interval); + return () => { + window.clearTimeout(timeout); + clearInterval(interval); + }; }, [load]); const filtered = diff --git a/src/components/FundConfirmModal.tsx b/src/components/FundConfirmModal.tsx index 26e5255..2944981 100644 --- a/src/components/FundConfirmModal.tsx +++ b/src/components/FundConfirmModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { useWallet } from "@/context/WalletContext"; import { useToast } from "@/context/ToastContext"; -import TokenSelector, { TokenAmount } from "./TokenSelector"; +import { TokenAmount } from "./TokenSelector"; import { useApprovedTokens } from "@/hooks/useApprovedTokens"; import { buildApproveTokenTransaction, @@ -9,21 +9,22 @@ import { Invoice, submitSignedTransaction, } from "@/utils/soroban"; -import { formatTokenAmount, formatDate, calculateYield } from "@/utils/format"; +import { formatTokenAmount, calculateYield } from "@/utils/format"; import { useFundInvoice } from "@/hooks/useInvoices"; type FundingStep = "approve" | "fund"; interface FundConfirmModalProps { invoice: Invoice | null; + payerScore?: number | null; onClose: () => void; onSuccess: () => void; } -export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundConfirmModalProps) { +export default function FundConfirmModal({ invoice, payerScore = null, onClose, onSuccess }: FundConfirmModalProps) { const { address, signTx } = useWallet(); const { addToast, updateToast } = useToast(); - const { tokens, tokenMap, defaultToken } = useApprovedTokens(); + const { tokenMap, defaultToken } = useApprovedTokens(); const { mutate: fund, isPending: isFunding } = useFundInvoice(); const [isApproving, setIsApproving] = useState(false); @@ -31,6 +32,7 @@ export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundCo const [allowance, setAllowance] = useState(null); const [fundingError, setFundingError] = useState(null); const [faqExpanded, setFaqExpanded] = useState(false); + const [referenceTimeMs] = useState(Date.now); const selectedInvoiceToken = invoice ? tokenMap.get(invoice.token ?? defaultToken?.contractId ?? "") ?? defaultToken ?? null @@ -56,7 +58,8 @@ export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundCo useEffect(() => { if (!invoice || !address) return; - void refreshAllowance(invoice, address); + const timeout = window.setTimeout(() => void refreshAllowance(invoice, address), 0); + return () => window.clearTimeout(timeout); }, [address, refreshAllowance, invoice]); if (!invoice) return null; @@ -113,6 +116,11 @@ export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundCo }; const tokenSymbol = selectedInvoiceToken?.symbol ?? "USDC"; + const yieldAmount = calculateYield(invoice.amount, invoice.discount_rate); + const daysToDue = Math.max( + 0, + Math.ceil((Number(invoice.due_date) * 1000 - referenceTimeMs) / (24 * 60 * 60 * 1000)), + ); return (
@@ -255,16 +263,30 @@ export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundCo
- Your yield (discount): + Gross yield: {selectedInvoiceToken ? (
- {formatTokenAmount(calculateYield(invoice.amount, invoice.discount_rate), selectedInvoiceToken)} {selectedInvoiceToken.symbol} - {(invoice.discount_rate / 100).toFixed(2)}% + {formatTokenAmount(yieldAmount, selectedInvoiceToken)} + + {invoice.discount_rate} bps / {(invoice.discount_rate / 100).toFixed(2)}% +
) : null}
+ +
+ Days to due date: + {daysToDue} days +
+ +
+ Payer reputation score: + + {payerScore === null || payerScore === undefined ? "Unknown" : `${payerScore}/100`} + +
@@ -279,7 +301,7 @@ export default function FundConfirmModal({ invoice, onClose, onSuccess }: FundCo Funding invoice... ) : ( - "Fund Invoice" + "Fund Now" )}