@@ -14,6 +14,9 @@ import { WalletContext } from '../hooks/useWallet';
1414
1515const LAST_WALLET_STORAGE_KEY = 'payd:last_wallet_name' ;
1616const SUPPORTED_MODAL_WALLETS = [ FREIGHTER_ID , LOBSTR_ID ] as const ;
17+ const WALLET_CONNECTION_TIMEOUT_MS = 15000 ;
18+ const WALLET_CONNECTION_TIMEOUT_MESSAGE =
19+ 'Wallet connection timed out after 15 seconds. Confirm the request in your wallet and try again.' ;
1720
1821type SelectableWallet = {
1922 id : string ;
@@ -22,6 +25,25 @@ type SelectableWallet = {
2225 isAvailable : boolean ;
2326} ;
2427
28+ function withWalletConnectionTimeout < T > ( promise : Promise < T > , timeoutMs : number ) : Promise < T > {
29+ return new Promise < T > ( ( resolve , reject ) => {
30+ const timeoutId = window . setTimeout ( ( ) => {
31+ reject ( new Error ( WALLET_CONNECTION_TIMEOUT_MESSAGE ) ) ;
32+ } , timeoutMs ) ;
33+
34+ promise . then (
35+ ( value ) => {
36+ window . clearTimeout ( timeoutId ) ;
37+ resolve ( value ) ;
38+ } ,
39+ ( error : unknown ) => {
40+ window . clearTimeout ( timeoutId ) ;
41+ reject ( error instanceof Error ? error : new Error ( String ( error ) ) ) ;
42+ }
43+ ) ;
44+ } ) ;
45+ }
46+
2547function hasAnyWalletExtension ( ) : boolean {
2648 if ( typeof window === 'undefined' ) return true ;
2749 const extendedWindow = window as Window &
@@ -34,7 +56,10 @@ function hasAnyWalletExtension(): boolean {
3456 return Boolean ( extendedWindow . freighterApi || extendedWindow . xBullSDK || extendedWindow . lobstr ) ;
3557}
3658
37- export const WalletProvider : React . FC < { children : React . ReactNode } > = ( { children } ) => {
59+ export const WalletProvider : React . FC < {
60+ children : React . ReactNode ;
61+ connectionTimeoutMs ?: number ;
62+ } > = ( { children, connectionTimeoutMs = WALLET_CONNECTION_TIMEOUT_MS } ) => {
3863 const [ address , setAddress ] = useState < string | null > ( null ) ;
3964 const [ walletName , setWalletName ] = useState < string | null > ( null ) ;
4065 const [ isConnecting , setIsConnecting ] = useState ( false ) ;
@@ -43,6 +68,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
4368 const [ network , setNetwork ] = useState < 'TESTNET' | 'PUBLIC' > ( 'TESTNET' ) ;
4469 const [ walletModalOpen , setWalletModalOpen ] = useState ( false ) ;
4570 const [ walletOptions , setWalletOptions ] = useState < SelectableWallet [ ] > ( [ ] ) ;
71+ const [ connectionError , setConnectionError ] = useState < string | null > ( null ) ;
4672 const kitRef = useRef < StellarWalletsKit | null > ( null ) ;
4773 const { t } = useTranslation ( ) ;
4874 const { notifyWalletEvent } = useNotification ( ) ;
@@ -68,7 +94,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
6894
6995 try {
7096 newKit . setWallet ( lastWalletName ) ;
71- const account = await newKit . getAddress ( ) ;
97+ const account = await withWalletConnectionTimeout ( newKit . getAddress ( ) , connectionTimeoutMs ) ;
7298 if ( account ?. address ) {
7399 setAddress ( account . address ) ;
74100 notifyWalletEvent (
@@ -85,7 +111,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
85111 } ;
86112
87113 void attemptSilentReconnect ( ) ;
88- } , [ notifyWalletEvent , network ] ) ;
114+ } , [ connectionTimeoutMs , notifyWalletEvent , network ] ) ;
89115
90116 const loadWalletOptions = async ( ) : Promise < SelectableWallet [ ] > => {
91117 const kit = kitRef . current ;
@@ -110,35 +136,34 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
110136 const kit = kitRef . current ;
111137 if ( ! kit ) return null ;
112138
139+ setConnectionError ( null ) ;
113140 setIsConnecting ( true ) ;
114141 try {
115142 kit . setWallet ( selectedWalletId ) ;
116-
117- const { address } = await Promise . race ( [
118- kit . getAddress ( ) ,
119- new Promise < { address : string } > ( ( _ , reject ) =>
120- setTimeout ( ( ) => reject ( new Error ( 'Connection timed out after 15 seconds.' ) ) , 15000 )
121- ) ,
122- ] ) ;
143+ const { address } = await withWalletConnectionTimeout ( kit . getAddress ( ) , connectionTimeoutMs ) ;
123144
124145 setAddress ( address ) ;
125146 setWalletName ( selectedWalletId ) ;
126147 localStorage . setItem ( LAST_WALLET_STORAGE_KEY , selectedWalletId ) ;
148+ setConnectionError ( null ) ;
149+ setWalletModalOpen ( false ) ;
127150 notifyWalletEvent (
128151 'connected' ,
129152 `${ address . slice ( 0 , 6 ) } ...${ address . slice ( - 4 ) } via ${ selectedWalletId } `
130153 ) ;
131154 return address ;
132155 } catch ( error ) {
133156 console . error ( 'Failed to connect wallet:' , error ) ;
157+ const message =
158+ error instanceof Error ? error . message : 'Unable to connect to the selected wallet.' ;
159+ setConnectionError ( message ) ;
134160 notifyWalletEvent (
135161 'connection_failed' ,
136- error instanceof Error ? error . message : 'Please try again.'
162+ message
137163 ) ;
138164 return null ;
139165 } finally {
140166 setIsConnecting ( false ) ;
141- setWalletModalOpen ( false ) ;
142167 }
143168 } ;
144169
@@ -148,6 +173,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
148173 notifyWalletEvent ( 'connection_failed' , 'No supported wallet providers were found.' ) ;
149174 return null ;
150175 }
176+ setConnectionError ( null ) ;
151177 setWalletModalOpen ( true ) ;
152178 return null ;
153179 } ;
@@ -186,20 +212,43 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
186212
187213 { walletModalOpen && (
188214 < div className = "fixed inset-0 z-[80] grid place-items-center bg-black/70 px-4" >
189- < div className = "w-full max-w-md rounded-2xl border border-hi bg-surface p-6 shadow-2xl" >
215+ < div
216+ className = "w-full max-w-md rounded-2xl border border-hi bg-surface p-6 shadow-2xl"
217+ role = "dialog"
218+ aria-modal = "true"
219+ aria-labelledby = "wallet-modal-title"
220+ aria-describedby = { connectionError ? 'wallet-connection-error' : undefined }
221+ >
190222 < div className = "mb-4 flex items-center justify-between" >
191- < h3 className = "text-lg font-black" > { t ( 'wallet.modalTitle' ) || 'Select a wallet' } </ h3 >
223+ < h3 id = "wallet-modal-title" className = "text-lg font-black" >
224+ { t ( 'wallet.modalTitle' ) || 'Select a wallet' }
225+ </ h3 >
192226 < button
193- onClick = { ( ) => setWalletModalOpen ( false ) }
227+ type = "button"
228+ onClick = { ( ) => {
229+ setWalletModalOpen ( false ) ;
230+ setConnectionError ( null ) ;
231+ } }
194232 className = "rounded-lg border border-hi px-2 py-1 text-xs text-muted hover:text-text"
195233 >
196234 Close
197235 </ button >
198236 </ div >
237+ { connectionError && (
238+ < div
239+ id = "wallet-connection-error"
240+ role = "alert"
241+ aria-live = "assertive"
242+ className = "mb-4 rounded-xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200"
243+ >
244+ { connectionError }
245+ </ div >
246+ ) }
199247 < div className = "space-y-2" >
200248 { walletOptions . map ( ( wallet ) => (
201249 < button
202250 key = { wallet . id }
251+ type = "button"
203252 onClick = { ( ) => void connectWithWallet ( wallet . id ) }
204253 disabled = { ! wallet . isAvailable || isConnecting }
205254 className = "flex w-full items-center justify-between rounded-xl border border-hi bg-black/20 px-4 py-3 text-left transition hover:bg-black/30 disabled:cursor-not-allowed disabled:opacity-50"
0 commit comments