diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d990c..d9fd75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH --- +## [3.3.3] — 2026-09-01 + +### Fixed +- **The session clock stayed at 00:00:00 on a live tunnel, and the notification + countdown froze with it.** `SessionTimer` is built lazily, so when the app reattaches + to a tunnel that outlived its UI process, `VpnConnection` had already reported + "connected" before the timer existed — and a change listener does not replay events + it was not present for. The timer now evaluates the current connection state once at + construction instead of waiting for a transition, and is created eagerly alongside + `VpnConnection`. Because the timer is what pushes text to the foreground + notification, a stopped clock also meant a frozen countdown; both come from this. +- Resuming the clock no longer requires `adoptedRunningRuntime` or a previous + successful sync, both of which are false in a freshly started process. A connected + tunnel with a stopped clock is now always treated as something to resume. + ## [3.3.2] — 2026-09-01 Background reliability. 3.3.1 could show "connected" against a tunnel that was no diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 8ef89b7..f783201 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -47,6 +47,11 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); vpnConnection.addListener(_onVpnConnectionChanged); unawaited(_loadSupportRewardState()); + // This provider is built lazily, so the tunnel may already be up and + // VpnConnection may already have fired its change before we attached. + // A listener does not replay history -- evaluate the current state once. + // Deferred so we never notify during the build that created us. + unawaited(Future.microtask(_onVpnConnectionChanged)); } int get remaining => _remainingSeconds; @@ -102,18 +107,15 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } void _onVpnConnectionChanged() { + // A live tunnel with a stopped clock is always wrong, however we got here: + // adopting a tunnel that outlived the UI process, recovering from a blip, or + // attaching late to one that was already up. Previously this required either + // adoptedRunningRuntime or _hasSyncedOnce, both false on a fresh process, so + // the clock stayed at 00:00:00 and the notification countdown froze with it. if (vpnConnection.status == VpnStatus.connected && !isRunning && - vpnConnection.adoptedRunningRuntime) { - _resumeTicking(); - return; - } - - if (vpnConnection.status == VpnStatus.connected && - !isRunning && - !_isDisconnecting && - _hasSyncedOnce) { - debugPrint('[Timer] VPN reconnected after blip - resuming.'); + !_isDisconnecting) { + debugPrint('[Timer] Connected with a stopped clock - resuming.'); _resumeTicking(); return; } diff --git a/lib/main.dart b/lib/main.dart index b11a401..b7d21a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,6 +47,9 @@ class ReVoltApp extends StatelessWidget { providers: [ ChangeNotifierProvider(create: (_) => VpnConnection(), lazy: false), ChangeNotifierProxyProvider( + // Eager, like VpnConnection: the timer has to be listening before the + // engine reports an already-running tunnel, not after the intro. + lazy: false, create: (ctx) => SessionTimer(vpnConnection: ctx.read()), update: (_, vpn, prev) => diff --git a/pubspec.yaml b/pubspec.yaml index 4dc3973..3afba06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.3.2+28 +version: 3.3.3+29 environment: sdk: ">=3.0.0 <4.0.0"