Review findings #1-#3 in internal/APP_REVIEW_2026-10-16.md: every session end dropped the user onto their real IP with no warning, and the app showed no errors at all. - RevoltAlerts (vendored runtime) owns both alerts: a high-importance channel, ids 2 and 3, posted from whichever process is alive - XrayVPNService schedules a 5-minute warning off the deadline it already holds, best effort (a warning that cannot be armed never stops a tunnel), with its own request code so it cannot clobber the expiry alarm - the warning intent is handled above the command dispatch: without that it would fall through to the final else and stop the tunnel 5 minutes early - SessionTimer._doDisconnect takes endedMessage: set for ends the user did not ask for, which both posts the alert and gives the UI a sentence - the API being unreachable no longer disconnects a working tunnel; it is a display state now, with expiry still enforced natively and server-side - status_bar.dart renders connect errors, connecting progress and the session-end reason flutter analyze clean; debug APK builds (Kotlin compiles). Co-Authored-By: Claude Opus 5 <[email protected]>
51 lines
1.8 KiB
Dart
51 lines
1.8 KiB
Dart
import 'package:flutter/services.dart';
|
|
|
|
// ── lib/logic/notification_service.dart ─────────────────────────────────────
|
|
// Bridge to the native VPN notification, which flutter_vless owns. This only
|
|
// refreshes its content text with the session countdown, so a failure here is
|
|
// never worth propagating to the session timer.
|
|
|
|
class NotificationService {
|
|
NotificationService._();
|
|
|
|
static const MethodChannel _channel =
|
|
MethodChannel('com.revoltvpn.app/notification');
|
|
|
|
static String? _lastText;
|
|
|
|
/// Sets the notification's content text to [text].
|
|
///
|
|
/// The native side rebuilds the whole notification, so skip the round-trip
|
|
/// when the text has not changed. A failed native update is intentionally
|
|
/// not cached so the next tick can retry it.
|
|
static Future<void> updateTimer(String text) async {
|
|
if (text == _lastText) return;
|
|
try {
|
|
await _channel.invokeMethod<void>('updateNotificationText', {
|
|
'title': 'Revolt VPN',
|
|
'text': text,
|
|
'disconnectLabel': 'Disconnect',
|
|
});
|
|
_lastText = text;
|
|
} catch (_) {
|
|
// Cosmetic.
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
static Future<void> showDisconnectedAlert() async {
|
|
try {
|
|
await _channel.invokeMethod<void>('showDisconnectedAlert');
|
|
} catch (_) {
|
|
// Informational only - never worth failing a disconnect over.
|
|
}
|
|
}
|
|
|
|
/// Forces the next connect to re-post even if the countdown resumes on the
|
|
/// same value.
|
|
static void reset() => _lastText = null;
|
|
}
|