fix(app): resume the session clock when attaching to a live tunnel

The clock sat at 00:00:00 on a connected 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. A change listener does not replay events it was
not present for, so the timer never learned it should be running. The
notification text is pushed by that same timer, which is why the countdown
froze at the same moment -- one cause, both symptoms.

The timer now evaluates the current connection state once at construction
rather than waiting for a transition, and is created eagerly alongside
VpnConnection so it is listening from the start.

Resuming also no longer requires adoptedRunningRuntime or a previous
successful sync. Both are false in a freshly started process, which is
exactly the case that needed to work. A connected tunnel with a stopped
clock is now always resumed.

Version 3.3.3+29.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-09-01 00:39:22 +03:00
1 parent 186dc906da
commit dfeb7f161c
4 files changed
+31 -11

No files matched your search

+15
View File
@@ -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 ## [3.3.2] — 2026-09-01
Background reliability. 3.3.1 could show "connected" against a tunnel that was no Background reliability. 3.3.1 could show "connected" against a tunnel that was no
+12 -10
View File
@@ -47,6 +47,11 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
vpnConnection.addListener(_onVpnConnectionChanged); vpnConnection.addListener(_onVpnConnectionChanged);
unawaited(_loadSupportRewardState()); 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; int get remaining => _remainingSeconds;
@@ -102,18 +107,15 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
} }
void _onVpnConnectionChanged() { 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 && if (vpnConnection.status == VpnStatus.connected &&
!isRunning && !isRunning &&
vpnConnection.adoptedRunningRuntime) { !_isDisconnecting) {
_resumeTicking(); debugPrint('[Timer] Connected with a stopped clock - resuming.');
return;
}
if (vpnConnection.status == VpnStatus.connected &&
!isRunning &&
!_isDisconnecting &&
_hasSyncedOnce) {
debugPrint('[Timer] VPN reconnected after blip - resuming.');
_resumeTicking(); _resumeTicking();
return; return;
} }
+3
View File
@@ -47,6 +47,9 @@ class ReVoltApp extends StatelessWidget {
providers: [ providers: [
ChangeNotifierProvider(create: (_) => VpnConnection(), lazy: false), ChangeNotifierProvider(create: (_) => VpnConnection(), lazy: false),
ChangeNotifierProxyProvider<VpnConnection, SessionTimer>( ChangeNotifierProxyProvider<VpnConnection, SessionTimer>(
// 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) => create: (ctx) =>
SessionTimer(vpnConnection: ctx.read<VpnConnection>()), SessionTimer(vpnConnection: ctx.read<VpnConnection>()),
update: (_, vpn, prev) => update: (_, vpn, prev) =>
+1 -1
View File
@@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 3.3.2+28 version: 3.3.3+29
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"