From b0e78dbe6d03798af1e404eb42c0555664660749 Mon Sep 17 00:00:00 2001 From: arandomogg <139588083+arandomogg@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:36:29 +0100 Subject: [PATCH] feat: auto-refresh contract state every 30s and wire landlord release action Resolves merge conflicts in page.tsx and queries.ts, introduces 30-second contract state polling that pauses when the browser tab is hidden, and wires the Release Funds button so only the connected landlord can trigger a Freighter- signed release transaction with a TransactionReview security modal. closes #501 closes #502 --- .../[contractId]/EscrowDashboardClient.tsx | 194 +++++++---- app/escrow/[contractId]/page.tsx | 294 +--------------- hooks/useContractPolling.ts | 73 ++++ lib/stellar/actions/release.ts | 92 ++--- lib/stellar/queries.ts | 315 +++++++++++++++--- 5 files changed, 525 insertions(+), 443 deletions(-) create mode 100644 hooks/useContractPolling.ts diff --git a/app/escrow/[contractId]/EscrowDashboardClient.tsx b/app/escrow/[contractId]/EscrowDashboardClient.tsx index 64131bbe..72cf29d5 100644 --- a/app/escrow/[contractId]/EscrowDashboardClient.tsx +++ b/app/escrow/[contractId]/EscrowDashboardClient.tsx @@ -1,78 +1,81 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import EscrowStatus from "@/components/escrow/EscrowStatus"; import FundingProgress from "@/components/escrow/FundingProgress"; import MultiSigApproval from "@/components/escrow/MultiSigApproval"; -import RoommateList, { type Roommate } from "@/components/escrow/RoommateList"; +import RoommateList from "@/components/escrow/RoommateList"; import EscrowDashboardSkeleton from "@/components/escrow/EscrowDashboardSkeleton"; -import { ChevronLeft, ExternalLink, ShieldCheck, Activity, Globe } from "lucide-react"; +import TransactionReview from "@/components/wallet/TransactionReview"; +import { ChevronLeft, ExternalLink, ShieldCheck, Activity, Globe, AlertCircle, Loader2, ArrowUpRight } from "lucide-react"; import Link from "next/link"; import { getExplorerLink } from "@/lib/stellar/explorer"; import { createLandlordMajorityConfig } from "@/lib/stellar/multisig"; import RefreshIndicator from "@/components/escrow/RefreshIndicator"; +import useContractPolling from "@/hooks/useContractPolling"; +import { useStellar } from "@/context/StellarContext"; +import { buildReleaseXdr, signAndSubmitRelease } from "@/lib/stellar/actions/release"; +import { useToast } from "@/hooks/useToast"; interface Props { contractId: string; } -interface ContractState { - id: string; - landlord: string; - totalRent: string; - deadline: string; - status: "active" | "funded" | "released" | "expired"; - totalFunded: number; - lastUpdate: string; - roommates: Roommate[]; -} +type ReleasePhase = "idle" | "building" | "review" | "submitting"; export default function EscrowDashboardClient({ contractId }: Props) { - const [contractState, setContractState] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // Artificial 3s delay to verify skeleton renders before content - fetchData(); - }, [contractId]); - - const fetchData = async () => { - return new Promise((resolve) => { - setTimeout(() => { - setContractState({ - id: contractId, - landlord: "GD7K4X5L7P2Q9F6N1M3R8S4T0U1V2W3X4Y5Z6A7B8C9D0E1F2G", - totalRent: "1250", - deadline: "April 05, 2026", - status: "active", - totalFunded: 775, - lastUpdate: new Date().toISOString(), - roommates: [ - { - address: "GA3X2Y1Z0W9V8U7T6S5R4Q3P2O1N0M9L8K7J6I5H4G3F2E1D0C", - expectedShare: "450", - paidAmount: "450", - isPaid: true, - }, - { - address: "GB5X4Y3Z2W1V0U9T8S7R6Q5P4O3N2M1L0K9J8I7H6G5F4E3D2C", - expectedShare: "450", - paidAmount: "325", - isPaid: false, - }, - { - address: "GC7X6Y5Z4W3V2U1T0S9R8Q7P6O5N4M3L2K1J0I9H8G7F6E5D4C", - expectedShare: "350", - paidAmount: "0", - isPaid: false, - }, - ], - }); - setIsLoading(false); - resolve(); - }, 1000); - }); - }; + const { contractState, isLoading, error, refresh } = useContractPolling(contractId); + const { isConnected, publicKey } = useStellar(); + const toast = useToast(); + + const [releasePhase, setReleasePhase] = useState("idle"); + const [preparedXdr, setPreparedXdr] = useState(null); + const [releaseError, setReleaseError] = useState(null); + + const isLandlord = + isConnected && + publicKey !== null && + contractState !== null && + publicKey === contractState.landlord; + + async function handleReleaseFunds() { + if (!contractState) return; + setReleasePhase("building"); + setReleaseError(null); + try { + const xdr = await buildReleaseXdr({ + contractId, + landlordAddress: contractState.landlord, + }); + setPreparedXdr(xdr); + setReleasePhase("review"); + } catch (err) { + setReleaseError(err instanceof Error ? err.message : "Failed to prepare transaction."); + setReleasePhase("idle"); + } + } + + async function handleConfirmRelease() { + if (!preparedXdr || !contractState) return; + setReleasePhase("submitting"); + try { + await signAndSubmitRelease(preparedXdr, contractState.landlord); + toast.success("Funds released to landlord."); + setReleasePhase("idle"); + setPreparedXdr(null); + setReleaseError(null); + await refresh(); + } catch (err) { + setReleaseError(err instanceof Error ? err.message : "Transaction failed."); + setReleasePhase("idle"); + } + } + + function handleCancelRelease() { + setReleasePhase("idle"); + setPreparedXdr(null); + setReleaseError(null); + } const multiSigConfig = contractState ? createLandlordMajorityConfig({ @@ -87,6 +90,20 @@ export default function EscrowDashboardClient({ contractId }: Props) {
+ {/* TransactionReview modal overlay */} + {(releasePhase === "review" || releasePhase === "submitting") && preparedXdr && ( +
+ +
+ )} +
{/* Navigation Breadcrumb */}