style: cut the comments down
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
66789d49c1
commit
30d96a4049
11 files changed
+59
-132
No files matched your search
@@ -5,9 +5,7 @@ import 'package:revoltvpn/logic/app_colors.dart';
|
||||
import 'package:revoltvpn/logic/session_timer.dart';
|
||||
import 'package:revoltvpn/logic/vpn_connection.dart';
|
||||
|
||||
// Nothing rendered the connection's own messages, so they are collected here
|
||||
// in one place: whatever the user most needs to know right now, and whether it
|
||||
// is a problem (red) or just progress (dim).
|
||||
// The one line worth showing right now, and whether it is a problem.
|
||||
class _StatusDetail {
|
||||
const _StatusDetail(this.text, {this.isProblem = false});
|
||||
|
||||
@@ -26,8 +24,7 @@ _StatusDetail? _detail(VpnConnection vpn, SessionTimer timer) {
|
||||
return _StatusDetail(vpn.statusMessage);
|
||||
}
|
||||
|
||||
// The tunnel still carries traffic; only the control plane is missing, so
|
||||
// this is a warning, not a disconnect (review #2).
|
||||
// Control plane only - the tunnel still carries traffic.
|
||||
if (vpn.status == VpnStatus.connected && timer.serverUnreachable) {
|
||||
return const _StatusDetail(
|
||||
'Can\'t reach the server — still connected',
|
||||
@@ -136,9 +133,7 @@ class StatusBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Until this existed, a failed connect, an expired session and a
|
||||
// cap kill all just turned the button back to idle - the user
|
||||
// could not tell protected from unprotected (review #3).
|
||||
// Errors and session-end reasons used to render nowhere.
|
||||
if (detail != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
|
||||
@@ -96,18 +96,14 @@ class AdManager extends ChangeNotifier {
|
||||
|
||||
// ── Show ad (or debug bypass) ─────────────────────────────────────
|
||||
|
||||
// Returns the nonce carried in the SSV custom_data when the reward was
|
||||
// earned, null otherwise. The main flow passes it to vpn.connect(adNonce:)
|
||||
// so the client waits for the session this ad created — minting a second
|
||||
// nonce there is what broke every connect once ads were on.
|
||||
// Returns the nonce put in the SSV custom_data, null if unrewarded. connect()
|
||||
// must reuse it: minting a second nonce there rejects the ad's own session.
|
||||
Future<String?> showAd(String adType) async {
|
||||
if (!adsEnabled) {
|
||||
// Main connect: no ad to show — HivemindService.fetchConfigDirectly()
|
||||
// fires the server-gated bypass itself (rule 87).
|
||||
// Main connect fires its own bypass in fetchConfigDirectly (rule 87).
|
||||
if (adType != 'support' || !kDebugMode) return null;
|
||||
|
||||
// Debug bypass: fire fake AdMob callback so support ads work in dev.
|
||||
// The server's ADMOB_BYPASS must be True for this to succeed.
|
||||
// Needs ADMOB_BYPASS=True server-side.
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final nonce = HivemindService.newNonce();
|
||||
try {
|
||||
|
||||
@@ -81,15 +81,10 @@ class HivemindService {
|
||||
_expectedNonce = null;
|
||||
}
|
||||
|
||||
// Fetches this device's VLESS URL. Three ways in:
|
||||
// adNonce != null — a real rewarded ad was watched; its SSV custom_data
|
||||
// carried this nonce, so wait for a session holding it.
|
||||
// Google's SSV callback can lag, so poll longer.
|
||||
// resume == true — re-attach to the session already granted (mode-switch
|
||||
// reconnect in connection_mode.dart). No ad, no bypass,
|
||||
// any nonce accepted — device_id already owns it.
|
||||
// neither — ads disabled: mint a nonce and fire the server-gated
|
||||
// ADMOB_BYPASS callback (rule 87).
|
||||
// Fetches this device's VLESS URL.
|
||||
// adNonce — wait for the session that ad created (SSV can lag, so poll longer)
|
||||
// resume — re-attach to the session already held; any nonce accepted
|
||||
// neither — ads off: mint a nonce and fire the bypass (rule 87)
|
||||
static Future<String> fetchConfigDirectly({
|
||||
void Function(int attempt, int total)? onAttempt,
|
||||
String? adNonce,
|
||||
|
||||
@@ -32,15 +32,12 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Posts the "disconnected - not protected" alert for a session end the user
|
||||
// did not ask for. The native service posts the same alert (same id) when
|
||||
// its own deadline fires, because by then this process may be gone; a double
|
||||
// post replaces rather than stacks.
|
||||
// Same id as the service's own alert, so a double post replaces.
|
||||
static Future<void> showDisconnectedAlert() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('showDisconnectedAlert');
|
||||
} catch (_) {
|
||||
// Informational only - never worth failing a disconnect over.
|
||||
// Informational only.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
int _lastUsedBytes = 0;
|
||||
Stopwatch? _sinceLastSuccessfulSync;
|
||||
// Wall-clock deadline, kept alongside the Stopwatch because they answer
|
||||
// different questions. The Stopwatch is CLOCK_MONOTONIC: it does not advance
|
||||
// while the device is suspended, which is exactly when an RTC_WAKEUP expiry
|
||||
// alarm fires, so it cannot be used to recognise an expiry after sleep.
|
||||
// Wall clock, because the Stopwatch is CLOCK_MONOTONIC and stops across
|
||||
// device suspend - exactly when the RTC_WAKEUP expiry alarm fires.
|
||||
DateTime? _deadlineAt;
|
||||
double _currentSpeedKBps = 0.0;
|
||||
|
||||
@@ -70,14 +68,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
int get usedBytes => _usedBytes;
|
||||
double get currentSpeedKBps => _currentSpeedKBps;
|
||||
|
||||
// True once the control plane has missed enough polls in a row to be worth
|
||||
// telling the user about. It is a display state only: a tunnel that still
|
||||
// carries traffic is never torn down because the API is unreachable (the
|
||||
// native deadline and the server both still enforce expiry).
|
||||
// Display state only - an unreachable API never tears down a live tunnel.
|
||||
bool get serverUnreachable => _consecutiveFailures >= _maxConsecutiveFailures;
|
||||
|
||||
// Why the last session ended, when the user did not end it themselves.
|
||||
// Cleared on the next connect. Rendered by status_bar.dart.
|
||||
// Why the last session ended, if the user did not end it. Cleared on connect.
|
||||
String? _lastEndMessage;
|
||||
String? get lastEndMessage => _lastEndMessage;
|
||||
|
||||
@@ -146,19 +140,15 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
if (vpnConnection.status == VpnStatus.disconnected) {
|
||||
// The native deadline fires on the exact second while this 1 s tick is
|
||||
// in arbitrary phase, so expiry almost always arrives here rather than
|
||||
// at the countdown check in _tick - without this, the commonest session
|
||||
// end left the UI with nothing to say.
|
||||
// The native deadline beats the 1 s tick, so expiry lands here, not in
|
||||
// _tick. postAlert is false: the service already posted, and a
|
||||
// notification-Disconnect tap is the user asking.
|
||||
final deadline = _deadlineAt;
|
||||
final expired = _hasSyncedOnce &&
|
||||
deadline != null &&
|
||||
DateTime.now().isAfter(
|
||||
deadline.subtract(const Duration(seconds: 5)),
|
||||
);
|
||||
// postAlert is false either way: the native side already posted for its
|
||||
// own deadline, and the notification's Disconnect action is the user
|
||||
// asking, which must never raise a "not protected" alert.
|
||||
unawaited(_doDisconnect(
|
||||
'VPN tunnel dropped',
|
||||
endedMessage:
|
||||
@@ -168,9 +158,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
// resetSupportClaim is false when re-attaching to a session the device
|
||||
// already holds: the support top-up is once per server session, and the
|
||||
// claim flag is the only thing enforcing that.
|
||||
// resetSupportClaim is false when re-attaching to a session already held:
|
||||
// the top-up is once per server session and this flag is what enforces it.
|
||||
Future<void> start({bool resetSupportClaim = true}) async {
|
||||
_sessionEpoch++;
|
||||
_remainingSeconds = 0;
|
||||
@@ -255,11 +244,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
await _doDisconnect(reason);
|
||||
}
|
||||
|
||||
// endedMessage is the one sentence the UI shows after an end the user did
|
||||
// not ask for; postAlert independently decides whether to raise the "not
|
||||
// protected" notification, which the native service already posts for every
|
||||
// stop it sees. Both exist so nobody is left on their real IP without being
|
||||
// told. A user-requested disconnect passes neither.
|
||||
// endedMessage is what the UI shows; postAlert raises the notification.
|
||||
// A user-requested disconnect passes neither.
|
||||
Future<void> _doDisconnect(
|
||||
String reason, {
|
||||
String? endedMessage,
|
||||
@@ -428,12 +414,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_remainingSeconds =
|
||||
(_remainingAtLastSync - elapsed).clamp(0, 1 << 31).toInt();
|
||||
|
||||
// The Stopwatch is CLOCK_MONOTONIC and stops while the device is
|
||||
// suspended, so after a long sleep it over-reports - and with the server
|
||||
// unreachable no sync arrives to correct it, which is exactly the state
|
||||
// the app now stays in rather than disconnecting. Take whichever is
|
||||
// smaller, never the wall clock alone: a clock set backwards must not be
|
||||
// able to extend a session.
|
||||
// The Stopwatch over-reports after device suspend. Take the smaller of
|
||||
// the two, never the wall clock alone - a backward clock must not extend.
|
||||
final deadline = _deadlineAt;
|
||||
if (deadline != null) {
|
||||
final byWallClock = deadline.difference(DateTime.now()).inSeconds;
|
||||
|
||||
@@ -224,9 +224,8 @@ class VpnConnection extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SOCKS5 mode skips requestPermission below (it would raise the VPN
|
||||
// consent dialog for an interface it never creates), so ask for the
|
||||
// notification permission on its own - the session alerts need it.
|
||||
// Proxy mode skips requestPermission below (VPN consent it never needs),
|
||||
// so it has to ask for POST_NOTIFICATIONS on its own.
|
||||
if (!kIsWeb && _activeMode == ConnectionMode.proxy) {
|
||||
try {
|
||||
await _nativeControl.invokeMethod<void>('requestNotificationPermission');
|
||||
|
||||
@@ -67,10 +67,8 @@ class _ConnectionModeTileState extends State<ConnectionModeTile> {
|
||||
widget.onChanged?.call(next);
|
||||
}
|
||||
|
||||
// Recovers from an orphaned proxy: tear the runtime down and re-attach to
|
||||
// the session this device already holds (connect(resume: true)), so a
|
||||
// readable set of credentials exists again. No rewarded ad and no new
|
||||
// session — the user is repairing a connection they already paid for.
|
||||
// Recovers an orphaned proxy: tear the runtime down and re-attach to the
|
||||
// session already held. No ad, no new session.
|
||||
Future<void> _reconnect() async {
|
||||
if (_reconnecting) return;
|
||||
final vpn = context.read<VpnConnection>();
|
||||
|
||||
Reference in new issue
Block a user