@@ -79,6 +79,8 @@ export interface VpnModel {
7979 readonly killSwitch : boolean ;
8080 /** Auto-connect on app launch once discovery completes (persisted). */
8181 readonly autoConnect : boolean ;
82+ /** Unix-ms when the active session connected, or null when not connected. */
83+ readonly connectedSince : number | null ;
8284}
8385
8486/** Chain-payment identity derived from the device key + last enrollment. */
@@ -131,6 +133,13 @@ export function useVpn(): VpnModel & VpnActions {
131133 const [ killSwitch , setKillSwitchState ] = useState ( false ) ;
132134 const [ autoConnect , setAutoConnectState ] = useState ( false ) ;
133135 const autoConnectedRef = useRef ( false ) ;
136+ // Unix-ms when the current session connected, for the session timer.
137+ const [ connectedSince , setConnectedSince ] = useState < number | null > ( null ) ;
138+ // Whether the user currently wants a tunnel (true after connect, false after
139+ // an explicit disconnect) + whether we reached 'connected' — together these
140+ // distinguish an unexpected drop from a user disconnect, for auto-reconnect.
141+ const wantConnectedRef = useRef ( false ) ;
142+ const wasConnectedRef = useRef ( false ) ;
134143
135144 // Latest enrolled gateway IP, kept in a ref for the status poller.
136145 const gatewayIpRef = useRef < string | null > ( null ) ;
@@ -222,6 +231,11 @@ export function useVpn(): VpnModel & VpnActions {
222231 const sub = onTunnelStatus ( ( s ) => {
223232 setStatus ( s ) ;
224233 setState ( s . state ) ;
234+ if ( s . state === 'connected' ) {
235+ setConnectedSince ( ( prev ) => prev ?? Date . now ( ) ) ;
236+ } else if ( s . state === 'disconnected' || s . state === 'error' ) {
237+ setConnectedSince ( null ) ;
238+ }
225239 } ) ;
226240 return ( ) => sub . remove ( ) ;
227241 } , [ ] ) ;
@@ -276,6 +290,7 @@ export function useVpn(): VpnModel & VpnActions {
276290 if ( ! keypair ) {
277291 return ;
278292 }
293+ wantConnectedRef . current = true ;
279294 setError ( null ) ;
280295 setState ( 'connecting' ) ;
281296 try {
@@ -333,6 +348,8 @@ export function useVpn(): VpnModel & VpnActions {
333348 } , [ keypair , countries , selectedCode , routeStyle , entryCode , exitCode , killSwitch ] ) ;
334349
335350 const disconnect = useCallback ( async ( ) : Promise < void > => {
351+ wantConnectedRef . current = false ;
352+ wasConnectedRef . current = false ;
336353 setState ( 'disconnecting' ) ;
337354 try {
338355 await CumulusTunnel . stopTunnel ( ) ;
@@ -341,6 +358,24 @@ export function useVpn(): VpnModel & VpnActions {
341358 }
342359 } , [ ] ) ;
343360
361+ // ---- auto-reconnect on an unexpected drop ------------------------------
362+ // If the tunnel drops while the user still wants it (they didn't tap
363+ // Disconnect) and we had reached 'connected', bring it back up shortly. Only
364+ // reconnects a genuine drop — initial connect failures are handled by the
365+ // watchdog + the error message, not a retry loop.
366+ useEffect ( ( ) => {
367+ if ( state === 'connected' ) {
368+ wasConnectedRef . current = true ;
369+ return ;
370+ }
371+ if ( state === 'disconnected' && wantConnectedRef . current && wasConnectedRef . current ) {
372+ wasConnectedRef . current = false ;
373+ const id = setTimeout ( ( ) => void connect ( ) , 3000 ) ;
374+ return ( ) => clearTimeout ( id ) ;
375+ }
376+ return undefined ;
377+ } , [ state , connect ] ) ;
378+
344379 // ---- auto-connect on launch (opt-in) -----------------------------------
345380 // Once, after the first successful discovery, if the user enabled auto-connect
346381 // and nothing is up yet, bring the tunnel up automatically.
@@ -408,6 +443,7 @@ export function useVpn(): VpnModel & VpnActions {
408443 exit,
409444 killSwitch,
410445 autoConnect,
446+ connectedSince,
411447 connect,
412448 disconnect,
413449 selectCountry,
0 commit comments