diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 44e54e7..0b99cc1 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -240,11 +240,17 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } } - Future disconnect({String reason = 'User requested'}) async { + // release is false when the caller intends to re-attach to the same session, + // as the SOCKS5 Reconnect does: handing it back here would destroy the very + // session the follow-up connect(resume: true) is about to ask for. + Future disconnect({ + String reason = 'User requested', + bool release = true, + }) async { // Only here, not in _doDisconnect: this is the one path the user actually // asked for. Unawaited so a slow or unreachable API never holds up the // teardown. - unawaited(HivemindService.releaseSession()); + if (release) unawaited(HivemindService.releaseSession()); await _doDisconnect(reason); } diff --git a/lib/screens/settings/in_settings/connection_mode.dart b/lib/screens/settings/in_settings/connection_mode.dart index 0f0fe32..e91f3ad 100644 --- a/lib/screens/settings/in_settings/connection_mode.dart +++ b/lib/screens/settings/in_settings/connection_mode.dart @@ -76,7 +76,7 @@ class _ConnectionModeTileState extends State { setState(() => _reconnecting = true); try { - await timer.disconnect(); + await timer.disconnect(release: false); if (!mounted) return; if (await vpn.connect(resume: true)) { if (!mounted) return; diff --git a/test/session_lifecycle_regression_test.dart b/test/session_lifecycle_regression_test.dart index e282042..3819ced 100644 --- a/test/session_lifecycle_regression_test.dart +++ b/test/session_lifecycle_regression_test.dart @@ -131,4 +131,22 @@ void main() { reason: 'exactly one call site', ); }); + + test('SOCKS5 Reconnect re-attaches instead of handing the session back', () { + final timer = File('lib/logic/session_timer.dart').readAsStringSync(); + final mode = + File('lib/screens/settings/in_settings/connection_mode.dart') + .readAsStringSync(); + + // Reconnect repairs an orphaned proxy by re-attaching to the session the + // device already holds. Releasing it on the way out would destroy the very + // session the follow-up connect(resume: true) then asks for. + expect(timer, contains('bool release = true')); + expect(timer, contains('if (release) unawaited(HivemindService.releaseSession())')); + expect(mode, contains('timer.disconnect(release: false)')); + expect(mode, contains('vpn.connect(resume: true)')); + + // And it must not hand back a second support top-up for the same session. + expect(mode, contains('timer.start(resetSupportClaim: false)')); + }); }