From a32252c56d404ee88968c35d746d1aa1cd851769 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sun, 26 Jul 2026 10:41:30 +0300 Subject: [PATCH] deleting old safety checks and connect polish --- lib/components/connect_button.dart | 30 ++++++++--- lib/logic/hivemind_service.dart | 82 ++++++++++-------------------- lib/logic/vpn_connection.dart | 7 +-- 3 files changed, 55 insertions(+), 64 deletions(-) diff --git a/lib/components/connect_button.dart b/lib/components/connect_button.dart index 59229af..24eecd0 100644 --- a/lib/components/connect_button.dart +++ b/lib/components/connect_button.dart @@ -7,6 +7,13 @@ import 'package:revoltvpn/logic/session_timer.dart'; import 'package:revoltvpn/logic/ad_manager.dart'; /// ConnectButton — circle with brand.png + pulsing glow when connected. +/// +/// Rules: +/// disconnected → connect (ad → config → tunnel) +/// connecting → disconnect (cancel) +/// connected → disconnect +/// error → connect (retry) +/// disconnecting → ignore class ConnectButton extends StatefulWidget { const ConnectButton({super.key}); @@ -17,6 +24,7 @@ class ConnectButton extends StatefulWidget { class _ConnectButtonState extends State with SingleTickerProviderStateMixin { bool _busy = false; + DateTime _lastTap = DateTime.now(); AnimationController? _pulse; Animation? _pulseAnim; @@ -51,21 +59,30 @@ class _ConnectButtonState extends State } Future _handleTap() async { + // 1-second cooldown to prevent spam + final now = DateTime.now(); + if (now.difference(_lastTap).inMilliseconds < 1000) return; + _lastTap = now; + HapticFeedback.lightImpact(); final vpn = context.read(); final timer = context.read(); final ad = context.read(); - // Disconnect always works — even mid-connection + // ── Disconnect if connected or connecting ─────────────────────────── if (vpn.status == VpnStatus.connected || vpn.status == VpnStatus.connecting) { - _busy = false; // cancel any in-flight connect + _busy = false; + timer.stop(); // MUST stop timer BEFORE disconnect — prevents auto-reconnect await vpn.disconnect(); - timer.stop(); return; } - if (_busy) return; // already connecting, prevent double-tap + // ── Ignore if disconnecting ───────────────────────────────────────── + if (vpn.status == VpnStatus.disconnecting) return; + + // ── Connect (or retry) ───────────────────────────────────────────── + if (_busy) return; setState(() => _busy = true); try { @@ -87,8 +104,7 @@ class _ConnectButtonState extends State builder: (context, vpn, _) { final isConnected = vpn.status == VpnStatus.connected; final spinning = vpn.status == VpnStatus.connecting || - vpn.status == VpnStatus.disconnecting || - _busy; + vpn.status == VpnStatus.disconnecting; return GestureDetector( onTap: _handleTap, @@ -130,7 +146,7 @@ class _ConnectButtonState extends State ), ) : Padding( - padding: const EdgeInsets.all(0), // <-- CHANGE ME: brand.png padding from edge + padding: const EdgeInsets.all(14), // <-- CHANGE ME: brand.png padding from edge child: Image.asset('assets/brand.png', fit: BoxFit.contain), ), ); diff --git a/lib/logic/hivemind_service.dart b/lib/logic/hivemind_service.dart index 08c4e80..a22472d 100644 --- a/lib/logic/hivemind_service.dart +++ b/lib/logic/hivemind_service.dart @@ -7,104 +7,82 @@ import 'package:revoltvpn/logic/app_config.dart'; import 'package:revoltvpn/logic/crypto_service.dart'; class HivemindService { - /// Nonce generated at the start of a connect cycle. - /// Sent to the server during session creation and verified in every poll - /// response so we never accidentally resume a stale session. static String? _expectedNonce; + static int _currentCallId = 0; + + /// Call this from disconnect to abort any in-progress polling. + static void cancel() { + _currentCallId++; // bump — any running loop sees a stale ID and exits + } - /// Stores a nonce so that fetchConfigWithPolling can verify it. - /// Called by AdManager.showAd() before showing a rewarded ad so the - /// subsequent poll accepts only the session created by *this* ad. static void setExpectedNonce(String nonce) { _expectedNonce = nonce; } - /// Polls the Hivemind server until the session is ready or timeout occurs. - /// Returns a VLESS connection URL string. - /// - /// [onAttempt] is called after each poll with (current, total) so the UI - /// can show progress (e.g. "Contacting server (3/15)…"). - + /// Tries to fetch the VLESS config from the server. + /// Only polls a few times — by the time the ad finishes, the SSV callback + /// has usually already fired. static Future fetchConfigWithPolling({ void Function(int attempt, int total)? onAttempt, }) async { final deviceId = await CryptoService.getDeviceId(); - // ── Generate a connect nonce ────────────────────────────────────── + final callId = ++_currentCallId; final nonce = '${Random().nextInt(0x7FFFFFFF)}-${DateTime.now().millisecondsSinceEpoch}'; _expectedNonce = nonce; - debugPrint('[HivemindService] Connect nonce: $nonce'); + debugPrint('[HivemindService] Call #$callId — nonce: $nonce'); final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId'); // ── AD BYPASS (debug only) ──────────────────────────────────────── if (kDebugMode) { try { - final customData = jsonEncode({ - 'device_id': deviceId, - 'nonce': nonce, - }); - final fakeAdmobPingUrl = Uri.parse( + final customData = jsonEncode({'device_id': deviceId, 'nonce': nonce}); + final fakeUrl = Uri.parse( '${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}'); - await http.get(fakeAdmobPingUrl).timeout(const Duration(seconds: 8)); + await http.get(fakeUrl).timeout(const Duration(seconds: 8)); } catch (_) {} } - // ─────────────────────────────────────────────────────────────────── - int attempts = 0; - while (attempts < 15) { - debugPrint('[HivemindService] Polling attempt ${attempts + 1}/15…'); - onAttempt?.call(attempts + 1, 15); + const maxAttempts = 5; + for (int i = 1; i <= maxAttempts; i++) { + if (_currentCallId != callId) throw Exception('Cancelled'); + + onAttempt?.call(i, maxAttempts); try { - final response = await http.get(url).timeout(const Duration(seconds: 10)); + final response = await http.get(url).timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final data = jsonDecode(response.body); - debugPrint('[HivemindService] Session found, checking nonce…'); - - // ── Nonce gate ────────────────────────────────────────────── final serverNonce = data['nonce'] as String?; if (_expectedNonce != null && serverNonce != null && serverNonce != _expectedNonce) { - debugPrint('[HivemindService] Nonce mismatch (expected $_expectedNonce, got $serverNonce) — stale session, retrying…'); + debugPrint('[HivemindService] Nonce mismatch — retrying…'); } else if (data['active'] == true && data['vless_uuid'] != null) { final vlessUuid = data['vless_uuid']; - - // Build VLESS connection URL final vlessUrl = 'vless://$vlessUuid@${AppConfig.serverDomain}:${AppConfig.serverPort}' '?path=${Uri.encodeComponent(AppConfig.vlessPath)}' '&security=${AppConfig.vlessSecurity}' '&type=${AppConfig.vlessType}' '#ReVoltVPN'; - - _expectedNonce = null; // consumed successfully + _expectedNonce = null; return vlessUrl; - } else if (data['active'] == true && data['client_ip'] != null) { - // Server is still running AWG — VLESS client cannot use this. - debugPrint('[HivemindService] Server returned AWG format (client_ip) — server migration needed.'); - throw Exception('Server is running the old AWG protocol. ' - 'Please update the server to VLESS.'); } - } else { - debugPrint('[HivemindService] Non-200 status: ${response.statusCode}'); } } catch (e) { - debugPrint('[HivemindService] Polling exception: $e'); - // Ignore network errors and keep polling + if (e.toString().contains('Cancelled')) rethrow; + debugPrint('[HivemindService] Attempt $i failed: $e'); } - attempts++; - await Future.delayed(const Duration(seconds: 2)); + if (i < maxAttempts) await Future.delayed(const Duration(seconds: 1)); } - throw Exception("Session not activated. S2S callback failed or timed out."); + throw Exception('Session not activated. Server callback may have timed out.'); } - /// Quick one-shot check: does the server have a live session for this device? static Future verifySession() async { try { final deviceId = await CryptoService.getDeviceId(); - final url = Uri.parse( - '${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId'); + final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId'); final response = await http.get(url).timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final data = jsonDecode(response.body); @@ -114,7 +92,6 @@ class HivemindService { return false; } - /// Disconnects the session (cleans up on server) static Future disconnectAndCleanup() async { final deviceId = await CryptoService.getDeviceId(); final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop'); @@ -124,9 +101,6 @@ class HivemindService { headers: {'Content-Type': 'application/json'}, body: jsonEncode({'device_id': deviceId}), ).timeout(const Duration(seconds: 3)); - } catch (_) { - // Best effort cleanup - } + } catch (_) {} } - } diff --git a/lib/logic/vpn_connection.dart b/lib/logic/vpn_connection.dart index 22ba4e9..1676931 100644 --- a/lib/logic/vpn_connection.dart +++ b/lib/logic/vpn_connection.dart @@ -132,9 +132,9 @@ class VpnConnection extends ChangeNotifier { } catch (e) { debugPrint('[VPN] Hivemind config error: $e'); final raw = e.toString().replaceAll('Exception: ', ''); - if (raw.contains('AWG protocol') || raw.contains('old AWG')) { - _errorMessage = 'Server update required.\nThe VPN server needs to be upgraded to VLESS.'; - _setStatus(VpnStatus.error, 'Server outdated'); + if (raw.contains('Cancelled')) { + // User tapped disconnect — silently abort + return false; } else if (raw.contains('timed out') || raw.contains('Session not activated')) { _errorMessage = 'The server did not respond in time.\nCheck your connection and try again.'; _setStatus(VpnStatus.error, 'Server unreachable'); @@ -202,6 +202,7 @@ class VpnConnection extends ChangeNotifier { // ── Disconnect ──────────────────────────────────────────────────────────── Future disconnect() async { _cancelled = true; + HivemindService.cancel(); // abort any in-progress polling if (_status == VpnStatus.disconnected || _status == VpnStatus.disconnecting) { return; }