Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.getlantern.lantern.BuildConfig
import org.getlantern.lantern.MainActivity
import org.getlantern.lantern.constant.VPNStatus
import org.getlantern.lantern.notification.NotificationHelper
import org.getlantern.lantern.service.LanternVpnService.Companion.ACTION_STOP_VPN
import org.getlantern.lantern.utils.AppLogger
import org.getlantern.lantern.utils.DeviceUtil
import org.getlantern.lantern.utils.FlutterEventListener
Expand Down Expand Up @@ -146,8 +147,20 @@ class LanternVpnService :
closeTunInterface()
// Clean up synchronously — cannot use serviceScope here because
// it is cancelled in the finally block below.
runCatching { Mobile.stopVPN() }
.onFailure { e -> AppLogger.e(TAG, "Mobile.stopVPN() failed during destroy", e) }
// Only call stopVPN if Radiance IPC is actually running; calling it before
// setup completes results in a misleading "IPC not running" error.
if (Mobile.isRadianceConnected()) {
runCatching { Mobile.stopVPN() }
.onFailure { e ->
AppLogger.e(
TAG,
"Mobile.stopVPN() failed during destroy",
e
)
}
} else {
AppLogger.d(TAG, "Skipping stopVPN — Radiance IPC not running")
}
runCatching {
runBlocking(Dispatchers.IO) { DefaultNetworkMonitor.stop() }
}.onFailure { e ->
Expand Down Expand Up @@ -263,6 +276,14 @@ class LanternVpnService :
// VPN service starts, replaced by connected notification on success.
notificationHelper.showStartingVPNConnectedNotification(this@LanternVpnService)
runCatching {
// Radiance is pre-warmed via ACTION_START_RADIANCE, but as a background
// service it may have been killed by the OS before setup completed.
// Re-run setup here under the foreground notification so it is guaranteed
// to finish before we attempt to start the VPN tunnel.
if (!Mobile.isRadianceConnected()) {
AppLogger.d(TAG, "Radiance not ready, setting up before VPN start")
Mobile.setupRadiance(opts(), flutterEventListener)
}
DefaultNetworkMonitor.start()
connect()
VpnStatusManager.postVPNStatus(VPNStatus.Connected)
Expand Down Expand Up @@ -296,7 +317,13 @@ class LanternVpnService :
private suspend fun stopVPNTunnel() {
try {
closeTunInterface()
runCatching { Mobile.stopVPN() }
runCatching {
if (!Mobile.isVPNConnected()) {
AppLogger.d(TAG, "VPN is not connected, skipping stopVPN")
return@runCatching
}
Mobile.stopVPN()
}
.onFailure { e -> AppLogger.e(TAG, "Mobile.stopVPN() failed", e) }

runCatching { DefaultNetworkMonitor.stop() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,16 @@ object AppLogger {
writeAsync("INFO", tag, message)
}

fun w(tag: String, message: String, tr: Throwable? = null) {
Log.w(tag, message, tr)
writeAsync("WARN", tag, message)
fun w(tag: String, message: String, throwable: Throwable? = null) {
Log.w(tag, message, throwable)
val errorMessage = buildString {
append(message)
if (throwable != null) {
append("\n")
append(throwable.stackTraceToString())
}
}
writeAsync("WARN", tag, errorMessage)
}

fun e(tag: String, message: String, throwable: Throwable? = null) {
Expand Down
11 changes: 0 additions & 11 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ PODS:
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- Sentry/HybridSDK (8.56.2)
- sentry_flutter (9.14.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.56.2)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
Expand Down Expand Up @@ -112,7 +107,6 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- store_checker (from `.symlinks/plugins/store_checker/ios`)
Expand All @@ -122,7 +116,6 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- OrderedSet
- Sentry
- Stripe
- StripeApplePay
- StripeCore
Expand Down Expand Up @@ -154,8 +147,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
Expand All @@ -179,8 +170,6 @@ SPEC CHECKSUMS:
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: a9f60d84bfddcae9a0ebf834547702f8854766a1
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
store_checker: a7be624ac44a4ba88e7917e71afe8433f197161e
Expand Down
11 changes: 0 additions & 11 deletions lib/core/common/app_secrets.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:io';

import 'package:flutter_dotenv/flutter_dotenv.dart';

class AppSecrets {
Expand All @@ -18,13 +16,4 @@ class AppSecrets {

static String get lanternPackageName => "org.getlantern.lantern";

static String dnsConfig() {
if (Platform.isAndroid) {
return "https://4753d78f885f4b79a497435907ce4210@o75725.ingest.sentry.io/5850353";
}
if (Platform.isIOS) {
return "https://c14296fdf5a6be272e1ecbdb7cb23f76@o75725.ingest.sentry.io/4506081382694912";
}
return "https://7397d9db6836eb599f41f2c496dee648@o75725.ingest.us.sentry.io/4507734480912384";
}
}
5 changes: 2 additions & 3 deletions lib/core/models/feature_flags.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
enum FeatureFlag {
sentry('sentry'),
privateGcp('private.gcp'),
autoUpdateEnabled('autoUpdateEnabled'),
metrics('otel.metrics'),
traces('otel.traces');
traces('otel.traces'),
autoUpdateEnabled('autoUpdateEnabled');

final String key;

Expand Down
104 changes: 59 additions & 45 deletions lib/core/services/injection_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,76 @@ import 'logger_service.dart';
final GetIt sl = GetIt.instance;

Future<void> injectServices() async {
sl.registerLazySingleton<Updater>(() => Updater());

appLogger.debug('Initializing storage services...');
final storage = LocalStorageService();
final storeUtils = StoreUtils();
try {
appLogger.info("Initializing LocalStorageService");
final storage = LocalStorageService();
await storage.init();
sl.registerSingleton<LocalStorageService>(storage);
await Future.wait([storage.init(), storeUtils.init()]);
appLogger.debug('Storage services initialized');

appLogger.info("Initializing StoreUtils");
final storeUtils = StoreUtils();
await storeUtils.init();
sl.registerSingleton<LocalStorageService>(storage);
sl.registerSingleton<StoreUtils>(storeUtils);
} catch (e, st) {
appLogger.error('Storage init failed', e, st);
rethrow;
}

sl.registerLazySingleton(() => AppRouter());
sl.registerLazySingleton(() => AppPurchase());
sl<AppPurchase>().init();
sl.registerLazySingleton<DeepLinkCallbackManager>(
() => DeepLinkCallbackManager());
// We want to make sure the platform service and FFI service are
// initialized as early as possible so we can communicate with
// native code on different platforms.
final ps = LanternPlatformService();
await ps.init();
sl.registerSingleton<LanternPlatformService>(ps);
sl.registerLazySingleton<Updater>(() => Updater());
sl.registerLazySingleton<AppRouter>(() => AppRouter());
sl.registerLazySingleton<DeepLinkCallbackManager>(
() => DeepLinkCallbackManager(),
);

if (PlatformUtils.isFFISupported) {
sl.registerLazySingleton(() => LanternFFIService());
await sl<LanternFFIService>().init();
} else {
sl.registerLazySingleton<LanternFFIService>(
() => MockLanternFFIService());
}
sl.registerLazySingleton<LanternService>(
() => LanternService(
appLogger.debug('Initializing AppPurchase...');
final appPurchase = AppPurchase();
appPurchase.init();
sl.registerSingleton<AppPurchase>(appPurchase);
appLogger.debug('AppPurchase initialized');

sl.registerSingleton<LanternPlatformService>(LanternPlatformService());
sl.registerSingleton<LanternFFIService>(
PlatformUtils.isFFISupported
? LanternFFIService()
: MockLanternFFIService(),
);

sl.registerSingletonAsync<LanternService>(
() async {
final service = LanternService(
ffiService: sl<LanternFFIService>(),
platformService: sl<LanternPlatformService>(),
appPurchase: sl<AppPurchase>(),
),
);
);
try {
await service.init();
appLogger.debug('LanternService initialized');
} catch (e, st) {
appLogger.error('LanternService init failed', e, st);
}
return service;
},
);

appLogger.debug('Initializing notification/Stripe services...');
final notificationService = NotificationService();
try {
if (PlatformUtils.isAndroid) {
sl.registerSingletonAsync<StripeService>(() async {
appLogger.info("Initializing StripeService");
final stripeService = StripeService();
await stripeService.initialize();
return stripeService;
});
}
sl.registerSingletonAsync<NotificationService>(() async {
appLogger.info("Initializing NotificationService");
final notificationService = NotificationService();
final stripeService = StripeService();
await Future.wait([
notificationService.init(),
stripeService.initialize(),
]);
sl.registerSingleton<StripeService>(stripeService);
appLogger.debug('StripeService initialized');
} else {
await notificationService.init();
return notificationService;
});
appLogger.info("All services injected ✅");
}
} catch (e, st) {
appLogger.error("Error during service injection", e, st);
appLogger.error('Notification/Stripe init failed', e, st);
}
sl.registerSingleton<NotificationService>(notificationService);
appLogger.debug('NotificationService initialized');

await sl.allReady();
appLogger.info('All services injected ✅');
}
51 changes: 34 additions & 17 deletions lib/core/updater/updater.dart
Original file line number Diff line number Diff line change
@@ -1,50 +1,67 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:auto_updater/auto_updater.dart';
import 'package:flutter/foundation.dart';
import 'package:lantern/core/common/app_build_info.dart';
import 'package:lantern/core/common/common.dart';
import 'package:lantern/core/models/feature_flags.dart';
import 'package:lantern/core/services/injection_container.dart';
import 'package:lantern/lantern/lantern_service.dart';

class Updater {
bool _initialized = false;

bool get _isSupportedPlatform =>
!kIsWeb && (Platform.isMacOS || Platform.isWindows);

Future<void> init({required Map<String, dynamic> flags}) async {
Future<void> init() async {
if (_initialized) return;
_initialized = true;

if (kDebugMode) return;
if (!_isSupportedPlatform) return;

final enabled = flags.getBool(FeatureFlag.autoUpdateEnabled);
if (!enabled) return;
final flagResult = await sl<LanternService>().featureFlag();
final flags = flagResult.fold((_) => <String, dynamic>{}, (jsonStr) {
try {
return json.decode(jsonStr) as Map<String, dynamic>;
} catch (_) {
return <String, dynamic>{};
}
});

if (!flags.getBool(FeatureFlag.autoUpdateEnabled, defaultValue: true)) {
appLogger.info('autoUpdater disabled by feature flag');
return;
}

final buildType = AppBuildInfo.buildType;
final feedUrl = AppUrls.appcastFor(buildType);

try {
final _updater = AutoUpdater.instance;
await _updater.setFeedURL(feedUrl);
await _updater.setScheduledCheckInterval(3600);
final updater = AutoUpdater.instance;
await updater.setFeedURL(feedUrl);
await updater.setScheduledCheckInterval(3600);

// Background check after startup (avoid modal immediately on launch)
const firstPromptDelay = Duration(seconds: 45);
unawaited(Future<void>.delayed(firstPromptDelay, () async {
try {
await _updater.checkForUpdates(inBackground: true);
} catch (e, st) {
appLogger.error('Failed to check for auto-updates: $e', st);
}
}));

appLogger
.info('autoUpdater configured. buildType=$buildType url=$feedUrl');
unawaited(
Future<void>.delayed(firstPromptDelay, () async {
try {
await updater.checkForUpdates(inBackground: true);
} catch (e, st) {
appLogger.error('Failed to check for auto-updates', e, st);
}
}),
);

appLogger.info(
'autoUpdater configured. buildType=$buildType url=$feedUrl',
);
} catch (e, st) {
appLogger.error('Failed to configure autoUpdater: $e', st);
appLogger.error('Failed to configure autoUpdater:', e, st);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/lantern/lantern_core_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import '../core/services/app_purchase.dart';

/// LanternCoreService has all method that interact with lantern-core services
abstract class LanternCoreService {
Future<void> init();

///App Methods
Future<Either<Failure, Unit>> updateLocal(String locale);

Expand Down
6 changes: 5 additions & 1 deletion lib/lantern/lantern_ffi_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class LanternFFIService implements LanternCoreService {
return candidates.first;
}

@override
Future<void> init() async {
// Set safe defaults up front so callers always have something to listen to.
_status = _defaultStatusStream();
Expand Down Expand Up @@ -1694,4 +1695,7 @@ class FfiPlatformPaths {
}
}

class MockLanternFFIService extends LanternFFIService {}
class MockLanternFFIService extends LanternFFIService {
@override
Future<void> init() async {}
}
Loading
Loading