Skip to content

Commit fca05a8

Browse files
authored
Merge pull request #26 from bamiebot-maker/feat/issue-24-transaction-retry-ui
feat: add retry UI for failed transactions
2 parents e935330 + 3b69fe7 commit fca05a8

2 files changed

Lines changed: 97 additions & 9 deletions

File tree

‎src/components/common/CreatorCard.tsx‎

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { useRef, useState } from 'react';
12
import { useAccount } from 'wagmi';
23
import { Button } from '@/components/ui/button';
34
import type { Course } from '@/services/course.service';
45
import { cn } from '@/lib/utils';
56
import { ShoppingCart, Wallet, Link as LinkIcon } from 'lucide-react';
67
import toast from 'react-hot-toast';
78
import showToast from '@/utils/toast.util';
9+
import TransactionRetryNotice from '@/components/common/TransactionRetryNotice';
810

911
interface CreatorCardProps {
1012
creator: Course;
@@ -13,6 +15,32 @@ interface CreatorCardProps {
1315

1416
const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
1517
const { isConnected } = useAccount();
18+
const [transactionState, setTransactionState] = useState<
19+
'idle' | 'submitting' | 'failed'
20+
>('idle');
21+
const hasFailedOnceRef = useRef(false);
22+
23+
const runPurchaseAttempt = () => {
24+
setTransactionState('submitting');
25+
showToast.loading(`Purchasing keys for ${creator.title}...`);
26+
27+
window.setTimeout(() => {
28+
toast.remove();
29+
30+
if (!hasFailedOnceRef.current) {
31+
hasFailedOnceRef.current = true;
32+
setTransactionState('failed');
33+
return;
34+
}
35+
36+
hasFailedOnceRef.current = false;
37+
setTransactionState('idle');
38+
showToast.transactionSuccess(
39+
'Purchase Successful!',
40+
`You successfully bought a key for ${creator.title}`
41+
);
42+
}, 1500);
43+
};
1644

1745
const handleBuy = () => {
1846
if (!isConnected) {
@@ -23,14 +51,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
2351
return;
2452
}
2553

26-
showToast.loading(`Purchasing keys for ${creator.title}...`);
27-
// Implementation for contract interaction would go here
28-
setTimeout(() => {
29-
showToast.transactionSuccess(
30-
'Purchase Successful!',
31-
`You successfully bought a key for ${creator.title}`
32-
);
33-
}, 1500);
54+
runPurchaseAttempt();
3455
};
3556

3657
return (
@@ -82,14 +103,15 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
82103
onClick={handleBuy}
83104
variant={isConnected ? 'default' : 'outline'}
84105
size="sm"
106+
disabled={transactionState === 'submitting'}
85107
className={cn(
86108
'rounded-xl font-bold',
87109
!isConnected &&
88110
'border-white/10 text-white/60 hover:bg-white/5'
89111
)}
90112
>
91113
<ShoppingCart className="mr-2 size-4" />
92-
Buy Key
114+
{transactionState === 'submitting' ? 'Processing...' : 'Buy Key'}
93115
</Button>
94116
</div>
95117

@@ -99,6 +121,15 @@ const CreatorCard: React.FC<CreatorCardProps> = ({ creator, className }) => {
99121
Wallet Required
100122
</div>
101123
)}
124+
125+
{transactionState === 'failed' && (
126+
<TransactionRetryNotice
127+
className="mt-4"
128+
message="The previous purchase attempt failed before confirmation. Retry the Stellar action to try again."
129+
retryLabel="Retry Purchase"
130+
onRetry={runPurchaseAttempt}
131+
/>
132+
)}
102133
</div>
103134
);
104135
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { AlertTriangle, RotateCcw } from 'lucide-react';
2+
import { Button } from '@/components/ui/button';
3+
import { cn } from '@/lib/utils';
4+
5+
interface TransactionRetryNoticeProps {
6+
message: string;
7+
onRetry: () => void;
8+
title?: string;
9+
retryLabel?: string;
10+
className?: string;
11+
disabled?: boolean;
12+
}
13+
14+
const TransactionRetryNotice: React.FC<TransactionRetryNoticeProps> = ({
15+
title = 'Transaction failed',
16+
message,
17+
onRetry,
18+
retryLabel = 'Retry',
19+
className,
20+
disabled = false,
21+
}) => {
22+
return (
23+
<div
24+
role="status"
25+
aria-live="polite"
26+
className={cn(
27+
'rounded-2xl border border-red-500/20 bg-red-500/10 p-4 text-left',
28+
className
29+
)}
30+
>
31+
<div className="flex items-start gap-3">
32+
<div className="mt-0.5 rounded-full bg-red-500/15 p-2 text-red-300">
33+
<AlertTriangle className="size-4" />
34+
</div>
35+
<div className="min-w-0 flex-1">
36+
<p className="font-jakarta text-sm font-bold text-white">{title}</p>
37+
<p className="mt-1 font-jakarta text-sm leading-relaxed text-white/70">
38+
{message}
39+
</p>
40+
<Button
41+
type="button"
42+
size="sm"
43+
variant="outline"
44+
onClick={onRetry}
45+
disabled={disabled}
46+
className="mt-3 border-red-400/30 bg-transparent text-white hover:bg-red-500/10"
47+
>
48+
<RotateCcw className="size-4" />
49+
{retryLabel}
50+
</Button>
51+
</div>
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
export default TransactionRetryNotice;

0 commit comments

Comments
 (0)