11import React , { useState , useEffect , useRef } from "react" ;
2- import { setAllowed } from "@stellar/freighter-api" ;
3- import React , { useState , useEffect } from "react" ;
42import { setAllowed , isAllowed , getAddress } from "@stellar/freighter-api" ;
5- import { Loader2 , LogOut , Wallet } from './icons' ;
3+ import { Loader2 , LogOut , Wallet , AlertCircle , Check } from './icons' ;
64import { hasCustomRpcConfig , networkConfig } from '../config/network' ;
75import { useToast } from '../context/ToastContext' ;
86import { useTranslation } from '../i18n' ;
@@ -15,8 +13,13 @@ interface WalletConnectProps {
1513 onDisconnect : ( ) => void ;
1614}
1715
16+ type ConnectionErrorType = 'not-installed' | 'not-allowed' | 'no-address' | 'generic' | null ;
17+
1818const WalletConnect : React . FC < WalletConnectProps > = ( { walletAddress, onConnect, onDisconnect } ) => {
1919 const [ isConnecting , setIsConnecting ] = useState ( false ) ;
20+ const [ connectionError , setConnectionError ] = useState < ConnectionErrorType > ( null ) ;
21+ const [ showTooltip , setShowTooltip ] = useState ( false ) ;
22+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
2023 const announcedAddressRef = useRef < string | null > ( null ) ;
2124 const toast = useToast ( ) ;
2225 const { t } = useTranslation ( ) ;
@@ -64,38 +67,51 @@ const WalletConnect: React.FC<WalletConnectProps> = ({ walletAddress, onConnect,
6467
6568 const handleConnect = async ( ) => {
6669 setIsConnecting ( true ) ;
70+ setConnectionError ( null ) ;
6771 try {
6872 await setAllowed ( ) ;
6973 const allowed = await isAllowed ( ) ;
7074 if ( allowed . isAllowed ) {
7175 const userInfo = await getAddress ( ) ;
7276 if ( userInfo . address ) {
7377 onConnect ( userInfo . address ) ;
78+ setConnectionError ( null ) ;
7479 toast . success ( {
7580 title : t ( 'toast.walletConnected.title' ) ,
7681 description : t ( 'toast.walletConnected.description' ) ,
7782 } ) ;
83+ return ;
84+ } else {
85+ setConnectionError ( 'no-address' ) ;
86+ toast . error ( {
87+ title : t ( 'toast.walletConnectionFailed.title' ) ,
88+ description : t ( 'wallet.error.noAddress' ) ,
89+ } ) ;
90+ return ;
7891 }
79- }
80- const discoveredAddress = await discoverConnectedAddress ( ) ;
81- if ( discoveredAddress ) {
82- onConnect ( discoveredAddress ) ;
83- toast . success ( {
84- title : "Wallet connected" ,
85- description : "Freighter is now connected to your YieldVault session." ,
86- } ) ;
8792 } else {
93+ setConnectionError ( 'not-allowed' ) ;
8894 toast . warning ( {
8995 title : t ( 'toast.walletPermissionRequired.title' ) ,
90- description : t ( 'toast.walletPermissionRequired.description ' ) ,
96+ description : t ( 'wallet.error.notAllowed ' ) ,
9197 } ) ;
98+ return ;
9299 }
93100 } catch ( e : unknown ) {
94101 console . error ( e ) ;
102+ const error = e as Error ;
103+
104+ // Detect specific error types
105+ if ( error . message ?. includes ( 'Freighter' ) ) {
106+ setConnectionError ( 'not-installed' ) ;
107+ } else {
108+ setConnectionError ( 'generic' ) ;
109+ }
110+
95111 toast . error ( {
96112 title : t ( 'toast.walletConnectionFailed.title' ) ,
97- description : t ( 'toast.walletConnectionFailed.description ' ) ,
98- } ) ;
113+ description : t ( 'wallet.error.generic ' ) ,
114+ } ) ;
99115 } finally {
100116 setIsConnecting ( false ) ;
101117 }
@@ -105,6 +121,33 @@ const WalletConnect: React.FC<WalletConnectProps> = ({ walletAddress, onConnect,
105121 return `${ addr . substring ( 0 , 5 ) } ...${ addr . substring ( addr . length - 4 ) } ` ;
106122 } ;
107123
124+ const getErrorDescription = ( ) : string => {
125+ switch ( connectionError ) {
126+ case 'not-installed' :
127+ return t ( 'wallet.error.notInstalled' ) ;
128+ case 'not-allowed' :
129+ return t ( 'wallet.error.notAllowed' ) ;
130+ case 'no-address' :
131+ return t ( 'wallet.error.noAddress' ) ;
132+ case 'generic' :
133+ return t ( 'wallet.error.generic' ) ;
134+ default :
135+ return '' ;
136+ }
137+ } ;
138+
139+ const getStatusTooltip = ( ) : string => {
140+ if ( walletAddress ) {
141+ return t ( 'wallet.tooltip.connectedStatus' ) ;
142+ } else if ( isConnecting ) {
143+ return t ( 'wallet.tooltip.connectingStatus' ) ;
144+ } else if ( connectionError ) {
145+ return getErrorDescription ( ) ;
146+ } else {
147+ return t ( 'wallet.tooltip.disconnectedStatus' ) ;
148+ }
149+ } ;
150+
108151 if ( walletAddress ) {
109152 return (
110153 < div className = "wallet-status flex items-center gap-md" >
@@ -150,6 +193,7 @@ const WalletConnect: React.FC<WalletConnectProps> = ({ walletAddress, onConnect,
150193 className = "btn btn-outline"
151194 style = { { padding : '8px' , borderRadius : '50%' } }
152195 onClick = { ( ) => {
196+ setConnectionError ( null ) ;
153197 onDisconnect ( ) ;
154198 toast . info ( {
155199 title : t ( 'toast.walletDisconnected.title' ) ,
@@ -167,16 +211,87 @@ const WalletConnect: React.FC<WalletConnectProps> = ({ walletAddress, onConnect,
167211 return (
168212 < div style = { { position : 'relative' } } >
169213 < button
170- className = "btn btn-primary animate-glow"
214+ ref = { buttonRef }
215+ className = { `btn ${ connectionError ? 'btn-error' : 'btn-primary' } ${ isConnecting ? 'animate-glow' : '' } ` }
171216 onClick = { handleConnect }
172217 disabled = { isConnecting }
173218 aria-busy = { isConnecting }
219+ onMouseEnter = { ( ) => setShowTooltip ( true ) }
220+ onMouseLeave = { ( ) => setShowTooltip ( false ) }
221+ onFocus = { ( ) => setShowTooltip ( true ) }
222+ onBlur = { ( ) => setShowTooltip ( false ) }
223+ title = { getStatusTooltip ( ) }
224+ style = { {
225+ display : 'flex' ,
226+ alignItems : 'center' ,
227+ gap : '8px' ,
228+ position : 'relative' ,
229+ } }
174230 >
175- { isConnecting ? < Loader2 size = { 18 } className = "spin" style = { { animation : 'spin 1s linear infinite' } } /> : < Wallet size = { 18 } /> }
176- { isConnecting ? t ( 'wallet.connecting' ) : t ( 'wallet.connectFreighter' ) }
231+ { isConnecting ? (
232+ < Loader2 size = { 18 } className = "spin" style = { { animation : 'spin 1s linear infinite' } } />
233+ ) : connectionError ? (
234+ < AlertCircle size = { 18 } />
235+ ) : (
236+ < Wallet size = { 18 } />
237+ ) }
238+ < span > { isConnecting ? t ( 'wallet.connecting' ) : t ( 'wallet.connectFreighter' ) } </ span >
177239 </ button >
240+
241+ { /* Tooltip */ }
242+ { showTooltip && (
243+ < div
244+ style = { {
245+ position : 'absolute' ,
246+ bottom : '100%' ,
247+ left : '50%' ,
248+ transform : 'translateX(-50%)' ,
249+ marginBottom : '8px' ,
250+ padding : '8px 12px' ,
251+ backgroundColor : 'var(--surface-secondary)' ,
252+ border : connectionError ? '1px solid var(--accent-red-dim)' : '1px solid var(--accent-cyan-dim)' ,
253+ borderRadius : '4px' ,
254+ fontSize : '0.75rem' ,
255+ color : connectionError ? 'var(--accent-red)' : 'var(--text-secondary)' ,
256+ whiteSpace : 'nowrap' ,
257+ zIndex : 1000 ,
258+ boxShadow : connectionError
259+ ? '0 0 12px rgba(255, 80, 100, 0.15)'
260+ : '0 0 12px rgba(0, 240, 255, 0.15)' ,
261+ pointerEvents : 'none' ,
262+ } }
263+ >
264+ { getStatusTooltip ( ) }
265+ < div
266+ style = { {
267+ position : 'absolute' ,
268+ top : '100%' ,
269+ left : '50%' ,
270+ transform : 'translateX(-50%)' ,
271+ width : '0' ,
272+ height : '0' ,
273+ borderLeft : '4px solid transparent' ,
274+ borderRight : '4px solid transparent' ,
275+ borderTop : connectionError
276+ ? '4px solid var(--accent-red-dim)'
277+ : '4px solid var(--accent-cyan-dim)' ,
278+ } }
279+ />
280+ </ div >
281+ ) }
282+
178283 < style > { `
179284 @keyframes spin { 100% { transform: rotate(360deg); } }
285+ .btn-error {
286+ background-color: rgba(255, 80, 100, 0.1);
287+ border-color: var(--accent-red-dim);
288+ color: var(--accent-red);
289+ }
290+ .btn-error:hover:not(:disabled) {
291+ background-color: rgba(255, 80, 100, 0.2);
292+ border-color: var(--accent-red);
293+ box-shadow: 0 0 12px rgba(255, 80, 100, 0.3);
294+ }
180295 ` } </ style >
181296 </ div >
182297 ) ;
0 commit comments