Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
286 changes: 188 additions & 98 deletions components/dashboard/escrow-funding-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import { Loader2, AlertCircle, Wallet, ExternalLink, CheckCircle2 } from "lucide-react";
import { Loader2, AlertCircle, Wallet, ExternalLink, CheckCircle2, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
Expand Down Expand Up @@ -61,6 +61,7 @@
const [showConfirmation, setShowConfirmation] = useState(false);
const [transactionHash, setTransactionHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [transactionStatus, setTransactionStatus] = useState<'idle' | 'pending' | 'success' | 'failed'>('idle');

// Reset state when dialog opens/closes
useEffect(() => {
Expand All @@ -70,6 +71,7 @@
setError(null);
setTransactionHash(null);
setShowConfirmation(false);
setTransactionStatus('idle');
}
}, [open, requiredAmount]);

Expand All @@ -86,7 +88,7 @@
}

validateAmount();
}, [amount, isConnected, address, isWrongNetwork, requiredAmount]);

Check warning on line 91 in components/dashboard/escrow-funding-dialog.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'validateAmount'. Either include it or remove the dependency array

const validateAmount = async () => {
if (!amount || !isConnected || !address) return;
Expand Down Expand Up @@ -146,10 +148,14 @@
}

setShowConfirmation(true);
setTransactionStatus('idle');
setError(null);
setTransactionHash(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to prepare transaction");
const message = err instanceof Error ? err.message : "Failed to prepare transaction";
setError(message);
toast.error("Preparation failed", {
description: error || "Unknown error",
description: message,
});
} finally {
setIsConfirming(false);
Expand All @@ -159,6 +165,7 @@
const handleExecuteTransaction = async () => {
setIsSubmitting(true);
setError(null);
setTransactionStatus('pending');

try {
if (!address || !contractAddress) {
Expand Down Expand Up @@ -193,39 +200,41 @@
const txHash = "mock-tx-hash-" + Date.now();

setTransactionHash(txHash);
setTransactionStatus('success');

// Call the backend API to record the funding
const response = await fetch("/api/escrow/fund", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("tc_dev_access_token")}`,
},
body: JSON.stringify({
contractId,
fundingTxHash: txHash,
amount,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to record funding");
}

const result = await response.json();
try {
const response = await fetch("/api/escrow/fund", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("tc_dev_access_token")}`,
},
body: JSON.stringify({
contractId,
fundingTxHash: txHash,
amount,
}),
});

toast.success("Escrow funded successfully!", {
description: `Transaction hash: ${txHash}`,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to record funding");
}

// Close dialogs and trigger success callback
setShowConfirmation(false);
onOpenChange(false);
onFundingSuccess?.();
toast.success("Escrow funded successfully!", {
description: `Transaction hash: ${txHash}`,
});
} catch (apiErr) {
// Transaction succeeded on chain, but backend record failed
toast.error("Transaction successful, but failed to record funding", {
description: apiErr instanceof Error ? apiErr.message : "Unknown error",
});
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Transaction failed";
setError(errorMessage);
setTransactionStatus('failed');
toast.error("Funding failed", {
description: errorMessage,
});
Expand All @@ -240,6 +249,34 @@
: `https://stellar.expert/explorer/public/tx/${txHash}`;
};

const getEstimatedFee = () => {
// In production, fetch from Horizon or use transaction builder fee
return network === "TESTNET" ? "0.00001 XLM" : "0.0001 XLM";
};

const handleCopyHash = async () => {
if (!transactionHash) return;
try {
await navigator.clipboard.writeText(transactionHash);
toast.success("Transaction hash copied");
} catch {
toast.error("Failed to copy");
}
};

const handleCloseConfirmation = () => {
setShowConfirmation(false);
setTransactionStatus('idle');
setError(null);
setTransactionHash(null);
};

const handleDone = () => {
handleCloseConfirmation();
onOpenChange(false);
onFundingSuccess?.();
};

return (
<>
<Dialog open={open && !showConfirmation} onOpenChange={onOpenChange}>
Expand Down Expand Up @@ -345,8 +382,8 @@

{/* Error Display */}
{error && (
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
<div className="flex items-center gap-2 text-destructive text-sm">
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4">
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-4 w-4" />
{error}
</div>
Expand All @@ -355,114 +392,167 @@
</div>

<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleConfirmFunding}
disabled={!validation.isValid || isConfirming || isSubmitting}
className="min-w-[120px]"
>
{isConfirming ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Preparing...
</>
) : (
"Review Transaction"
"Review & Confirm"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

{/* Transaction Confirmation Dialog */}
<AlertDialog open={showConfirmation} onOpenChange={setShowConfirmation}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5 text-accent" />
Confirm Funding Transaction
</AlertDialogTitle>
<AlertDialogDescription>
Please review the transaction details before confirming
</AlertDialogDescription>
</AlertDialogHeader>
{/* Confirmation Modal */}
<Dialog open={showConfirmation} onOpenChange={handleCloseConfirmation}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{transactionStatus === 'success' ? (
<CheckCircle2 className="h-5 w-5 text-green-500" />
) : transactionStatus === 'failed' ? (
<AlertCircle className="h-5 w-5 text-destructive" />
) : (
<Loader2 className="h-5 w-5 animate-spin text-accent" />
)}
Transaction Confirmation
</DialogTitle>
<DialogDescription>
Review your transaction details
</DialogDescription>
</DialogHeader>

<div className="space-y-4 py-4">
<div className="bg-muted/50 rounded-lg p-4 space-y-3">
{/* Transaction Details */}
<div className="bg-muted/50 rounded-lg p-3 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Amount to Fund:</span>
<span className="font-semibold text-lg">{amount} {currency}</span>
<span className="text-muted-foreground">Type:</span>
<span className="font-medium">Fund Escrow</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">To Contract:</span>
<span className="font-mono text-xs break-all">{contractAddress}</span>
<span className="text-muted-foreground">Amount:</span>
<span className="font-semibold">{amount} {currency}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">From Wallet:</span>
<span className="font-mono text-xs">{address}</span>
<span className="text-muted-foreground">From:</span>
<span className="font-mono text-xs truncate max-w-[200px]">{address}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Network:</span>
<span className="font-medium">{network === "TESTNET" ? "Testnet" : "Mainnet"}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Estimated Fee:</span>
<span className="font-medium">{getEstimatedFee()}</span>
</div>
</div>

<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
<p className="text-sm text-blue-600 dark:text-blue-500">
<strong>Important:</strong> This transaction cannot be undone. Make sure you have reviewed all details.
</p>
</div>
</div>
{/* Transaction Status */}
{transactionStatus === 'pending' && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Processing transaction...
</div>
)}

<AlertDialogFooter>
<AlertDialogCancel disabled={isSubmitting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleExecuteTransaction}
disabled={isSubmitting}
className="min-w-[140px]"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
"Confirm & Fund"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

{/* Success Notification with Explorer Link */}
{transactionHash && (
<div className="fixed bottom-4 right-4 z-50">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 shadow-lg max-w-sm">
<div className="flex items-start gap-3">
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-500 shrink-0 mt-0.5" />
<div className="flex-1 space-y-2">
<p className="font-medium text-green-600 dark:text-green-500">
Funding Successful!
</p>
<p className="text-sm text-muted-foreground">
Transaction hash: <span className="font-mono text-xs">{transactionHash}</span>
</p>
{transactionStatus === 'success' && transactionHash && (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-green-600 dark:text-green-500">
<CheckCircle2 className="h-4 w-4" />
<span className="font-medium">Transaction Successful</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Hash:</span>
<span className="font-mono text-xs truncate flex-1">{transactionHash}</span>
<Button variant="ghost" size="sm" onClick={handleCopyHash}>
<Copy className="h-3 w-3" />
<span className="sr-only">Copy hash</span>
</Button>
</div>
<a
href={getExplorerUrl(transactionHash)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-500 hover:underline"
className="inline-flex items-center gap-1 text-sm text-accent hover:underline"
>
View on Explorer
<ExternalLink className="h-3 w-3" />
View on Explorer
</a>
</div>
</div>
)}

{transactionStatus === 'failed' && (
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="font-medium">Transaction Failed</span>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="flex gap-2 pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowConfirmation(false)}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleExecuteTransaction}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Retrying...
</>
) : (
"Retry"
)}
</Button>
</div>
</div>
)}
</div>
</div>
)}

<DialogFooter>
{transactionStatus === 'pending' ? (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</Button>
) : transactionStatus === 'success' ? (
<Button onClick={handleDone}>
Done
</Button>
) : transactionStatus === 'failed' ? (
<Button variant="outline" onClick={() => setShowConfirmation(false)}>
Close
</Button>
) : (
<div className="flex gap-2">
<Button variant="outline" onClick={handleCloseConfirmation}>
Cancel
</Button>
<Button onClick={handleExecuteTransaction} disabled={isSubmitting}>
Confirm & Submit
</Button>
</div>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
Loading
Loading