From 3b69fe71063269526a333c1ad8f23f79ba47800d Mon Sep 17 00:00:00 2001 From: bamiebot Date: Thu, 26 Mar 2026 02:50:20 +0100 Subject: [PATCH] feat: add retry UI for failed transactions --- src/components/common/CreatorCard.tsx | 49 +++++++++++++--- .../common/TransactionRetryNotice.tsx | 57 +++++++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 src/components/common/TransactionRetryNotice.tsx diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index c812e0af..d36a0c2a 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -1,3 +1,4 @@ +import { useRef, useState } from 'react'; import { useAccount } from 'wagmi'; import { Button } from '@/components/ui/button'; import type { Course } from '@/services/course.service'; @@ -5,6 +6,7 @@ import { cn } from '@/lib/utils'; import { ShoppingCart, Wallet, Link as LinkIcon } from 'lucide-react'; import toast from 'react-hot-toast'; import showToast from '@/utils/toast.util'; +import TransactionRetryNotice from '@/components/common/TransactionRetryNotice'; interface CreatorCardProps { creator: Course; @@ -13,6 +15,32 @@ interface CreatorCardProps { const CreatorCard: React.FC = ({ creator, className }) => { const { isConnected } = useAccount(); + const [transactionState, setTransactionState] = useState< + 'idle' | 'submitting' | 'failed' + >('idle'); + const hasFailedOnceRef = useRef(false); + + const runPurchaseAttempt = () => { + setTransactionState('submitting'); + showToast.loading(`Purchasing keys for ${creator.title}...`); + + window.setTimeout(() => { + toast.remove(); + + if (!hasFailedOnceRef.current) { + hasFailedOnceRef.current = true; + setTransactionState('failed'); + return; + } + + hasFailedOnceRef.current = false; + setTransactionState('idle'); + showToast.transactionSuccess( + 'Purchase Successful!', + `You successfully bought a key for ${creator.title}` + ); + }, 1500); + }; const handleBuy = () => { if (!isConnected) { @@ -23,14 +51,7 @@ const CreatorCard: React.FC = ({ creator, className }) => { return; } - showToast.loading(`Purchasing keys for ${creator.title}...`); - // Implementation for contract interaction would go here - setTimeout(() => { - showToast.transactionSuccess( - 'Purchase Successful!', - `You successfully bought a key for ${creator.title}` - ); - }, 1500); + runPurchaseAttempt(); }; return ( @@ -82,6 +103,7 @@ const CreatorCard: React.FC = ({ creator, className }) => { onClick={handleBuy} variant={isConnected ? 'default' : 'outline'} size="sm" + disabled={transactionState === 'submitting'} className={cn( 'rounded-xl font-bold', !isConnected && @@ -89,7 +111,7 @@ const CreatorCard: React.FC = ({ creator, className }) => { )} > - Buy Key + {transactionState === 'submitting' ? 'Processing...' : 'Buy Key'} @@ -99,6 +121,15 @@ const CreatorCard: React.FC = ({ creator, className }) => { Wallet Required )} + + {transactionState === 'failed' && ( + + )} ); }; diff --git a/src/components/common/TransactionRetryNotice.tsx b/src/components/common/TransactionRetryNotice.tsx new file mode 100644 index 00000000..1f5547da --- /dev/null +++ b/src/components/common/TransactionRetryNotice.tsx @@ -0,0 +1,57 @@ +import { AlertTriangle, RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface TransactionRetryNoticeProps { + message: string; + onRetry: () => void; + title?: string; + retryLabel?: string; + className?: string; + disabled?: boolean; +} + +const TransactionRetryNotice: React.FC = ({ + title = 'Transaction failed', + message, + onRetry, + retryLabel = 'Retry', + className, + disabled = false, +}) => { + return ( +
+
+
+ +
+
+

{title}

+

+ {message} +

+ +
+
+
+ ); +}; + +export default TransactionRetryNotice;