1+ 'use client' ;
2+
3+ import React from 'react' ;
4+ import { useWalletStore } from '@/store/walletStore' ;
5+ import { useWalletBalance } from '@/hooks/useWalletBalance' ;
6+
7+ // ─── Types ────────────────────────────────────────────────────────────────────
8+
9+ export interface BalanceCheckProps {
10+ /** Total XLM required for this transaction (escrow amount + estimated fee). */
11+ requiredAmount : number ;
12+ /** Called when the user confirms and submits the transaction. */
13+ onSubmit : ( ) => void ;
14+ /** Optional label for the submit button. Defaults to "Confirm & Lock Escrow". */
15+ submitLabel ?: string ;
16+ /** Whether the parent form has additional validation errors that block submit. */
17+ isFormInvalid ?: boolean ;
18+ }
19+
20+ // ─── Component ───────────────────────────────────────────────────────────────
21+
22+ /**
23+ * BalanceCheck — queries wallet balance before an escrow lock and renders a
24+ * warning when the balance is insufficient. The submit button is disabled
25+ * only when the balance is *definitively* too low.
26+ *
27+ * Follows the Component → Hook → Service layered architecture:
28+ * BalanceCheck → useWalletBalance → walletService
29+ */
30+ export function BalanceCheck ( {
31+ requiredAmount,
32+ onSubmit,
33+ submitLabel = 'Confirm & Lock Escrow' ,
34+ isFormInvalid = false ,
35+ } : BalanceCheckProps ) {
36+ const address = useWalletStore ( ( state ) => state . address ) ;
37+ const { balance, isLoading, error, isInsufficient, refresh } =
38+ useWalletBalance ( address ) ;
39+
40+ const insufficient = isInsufficient ( requiredAmount ) ;
41+
42+ // The button is disabled only when the balance is definitively insufficient
43+ // OR when the parent form has other blocking validation errors.
44+ const isSubmitDisabled = insufficient || isFormInvalid ;
45+
46+ return (
47+ < div className = "space-y-3" >
48+ { /* ── Balance display ───────────────────────────────────────────────── */ }
49+ < div className = "rounded-lg border border-gray-200 bg-gray-50 px-4 py-3" >
50+ < div className = "flex items-center justify-between" >
51+ < span className = "text-sm font-medium text-gray-600" > Wallet Balance</ span >
52+
53+ { isLoading ? (
54+ < span className = "text-sm text-gray-400 animate-pulse" > Fetching…</ span >
55+ ) : error ? (
56+ < button
57+ type = "button"
58+ onClick = { refresh }
59+ className = "text-sm text-blue-600 hover:underline"
60+ >
61+ Retry
62+ </ button >
63+ ) : balance !== null ? (
64+ < span
65+ className = { `text-sm font-semibold ${
66+ insufficient ? 'text-red-600' : 'text-green-600'
67+ } `}
68+ >
69+ { balance . toFixed ( 7 ) } XLM
70+ </ span >
71+ ) : null }
72+ </ div >
73+
74+ < div className = "mt-1 flex items-center justify-between" >
75+ < span className = "text-sm font-medium text-gray-600" > Required</ span >
76+ < span className = "text-sm font-semibold text-gray-800" >
77+ { requiredAmount . toFixed ( 7 ) } XLM
78+ </ span >
79+ </ div >
80+ </ div >
81+
82+ { /* ── Insufficient balance warning ──────────────────────────────────── */ }
83+ { insufficient && (
84+ < div
85+ role = "alert"
86+ aria-live = "polite"
87+ className = "flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-red-700"
88+ >
89+ { /* Warning icon */ }
90+ < svg
91+ xmlns = "http://www.w3.org/2000/svg"
92+ className = "mt-0.5 h-5 w-5 shrink-0 text-red-500"
93+ viewBox = "0 0 20 20"
94+ fill = "currentColor"
95+ aria-hidden = "true"
96+ >
97+ < path
98+ fillRule = "evenodd"
99+ d = "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
100+ clipRule = "evenodd"
101+ />
102+ </ svg >
103+
104+ < div className = "text-sm" >
105+ < p className = "font-semibold" > Insufficient balance</ p >
106+ < p className = "mt-0.5" >
107+ You need at least{ ' ' }
108+ < span className = "font-medium" > { requiredAmount . toFixed ( 7 ) } XLM</ span > to
109+ complete this transaction. Your current balance is{ ' ' }
110+ < span className = "font-medium" > { balance ! . toFixed ( 7 ) } XLM</ span > .
111+ </ p >
112+ < p className = "mt-1 text-xs text-red-600" >
113+ Please top up your wallet before proceeding.
114+ </ p >
115+ </ div >
116+ </ div >
117+ ) }
118+
119+ { /* ── Balance fetch error (non-blocking) ────────────────────────────── */ }
120+ { error && ! isLoading && (
121+ < p className = "text-xs text-amber-600" >
122+ ⚠️ Could not verify balance — you may still proceed, but ensure you have
123+ sufficient XLM.
124+ </ p >
125+ ) }
126+
127+ { /* ── Submit button ─────────────────────────────────────────────────── */ }
128+ < button
129+ type = "button"
130+ onClick = { onSubmit }
131+ disabled = { isSubmitDisabled }
132+ aria-disabled = { isSubmitDisabled }
133+ className = { `w-full rounded-lg px-4 py-3 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${
134+ isSubmitDisabled
135+ ? 'cursor-not-allowed bg-gray-200 text-gray-400'
136+ : 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500'
137+ } `}
138+ >
139+ { isLoading ? 'Checking balance…' : submitLabel }
140+ </ button >
141+ </ div >
142+ ) ;
143+ }
144+
145+ export default BalanceCheck ;
0 commit comments