Skip to content
Open
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
13 changes: 13 additions & 0 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ on:
required: false
type: boolean
default: false
skip_signing:
description: "Skip code signing for this run"
required: false
type: boolean
default: false

jobs:
build-windows:
Expand Down Expand Up @@ -126,6 +131,7 @@ jobs:
}

- name: Sign embedded binaries
if: ${{ !inputs.skip_signing }}
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
Expand Down Expand Up @@ -168,6 +174,12 @@ jobs:

Write-Host "All binaries signed successfully"

- name: Signing disabled for this run
if: ${{ inputs.skip_signing }}
shell: pwsh
run: |
Write-Host "Skipping embedded and installer signing for this run."

- name: Package installer
shell: pwsh
env:
Expand Down Expand Up @@ -202,6 +214,7 @@ jobs:
}

- name: Sign installer
if: ${{ !inputs.skip_signing }}
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ jobs:
build_type: ${{ needs.set-metadata.outputs.build_type }}
installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }}
enable_ip_check: ${{ needs.set-metadata.outputs.build_type == 'nightly' }}
skip_signing: ${{ needs.set-metadata.outputs.build_type == 'nightly' && needs.set-metadata.outputs.is_test_run == 'true' }}

build-linux:
needs: [set-metadata, release-create, release-approval]
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/common v1.2.1-0.20260325181816-33f69c725899
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce
github.com/getlantern/radiance v0.0.0-20260326182827-e35d6c1be462
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.41.0
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,10 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e
github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0=
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac=
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce h1:ARU8+/fVmcz2AJY2SqsrqWV0BHMyY6nmoHGmvWD4PzQ=
github.com/getlantern/radiance v0.0.0-20260326160312-80e8b51cccce/go.mod h1:jn8+Y7vnyULW+8DX1iDp3qKVBa81EUWFVDvMvkaZrQE=
github.com/getlantern/radiance v0.0.0-20260326151852-020898b9b028 h1:LXR1YBRvbrhhc7/G8yBSzB4oNJ59vbf8ZIUHbiednEk=
github.com/getlantern/radiance v0.0.0-20260326151852-020898b9b028/go.mod h1:jn8+Y7vnyULW+8DX1iDp3qKVBa81EUWFVDvMvkaZrQE=
github.com/getlantern/radiance v0.0.0-20260326182827-e35d6c1be462 h1:80p2Lm7Q7opsCKkQZBc4jbciVBCeYbqBUNgquVokVUc=
github.com/getlantern/radiance v0.0.0-20260326182827-e35d6c1be462/go.mod h1:jn8+Y7vnyULW+8DX1iDp3qKVBa81EUWFVDvMvkaZrQE=
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60 h1:m9eXjDK9vllbVH467+QXbrxUFFM9Yp7YJ90wZLw4dwU=
github.com/getlantern/samizdat v0.0.3-0.20260310125445-325cf1bd1b60/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo=
Expand Down
41 changes: 35 additions & 6 deletions lib/core/models/lantern_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,35 @@

import '../common/common.dart';

enum VPNStatusOrigin {
userAction('user_action'),
settingsMutation('settings_mutation'),
system('system'),
unknown('unknown');

const VPNStatusOrigin(this.wireValue);

final String wireValue;

static VPNStatusOrigin fromWire(dynamic rawOrigin) {
if (rawOrigin is! String) {
return VPNStatusOrigin.unknown;
}

final normalized = rawOrigin.toLowerCase();
for (final origin in VPNStatusOrigin.values) {
if (origin.wireValue == normalized) {
return origin;
}
}
return VPNStatusOrigin.unknown;
}
}

class LanternStatus {
final VPNStatus status;
final String? error;
final VPNStatusOrigin origin;

factory LanternStatus.fromJson(Map<String, dynamic> json) {
appLogger.info('LanternStatus.fromJson $json');
Expand All @@ -33,14 +59,17 @@ class LanternStatus {
appLogger.error('Unknown status: $statusStr');
status = VPNStatus.disconnected;
}
return LanternStatus(
status: status,
error: json['error'],
);
final origin = VPNStatusOrigin.fromWire(json['origin']);
return LanternStatus(status: status, error: json['error'], origin: origin);
}

LanternStatus({required this.status, this.error});
LanternStatus({
required this.status,
this.error,
this.origin = VPNStatusOrigin.unknown,
});

@override
String toString() => 'LanternStatus(status: $status, error: $error)';
String toString() =>
'LanternStatus(status: $status, error: $error, origin: $origin)';
}
63 changes: 63 additions & 0 deletions lib/core/utils/latest_async_queue.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:async';

typedef LatestAsyncQueueWorker<T, R> = Future<R> Function(T value);

/// Serializes async updates and coalesces queued values so only the latest
/// pending value is processed after each await point.
class LatestAsyncQueue<T, R> {
LatestAsyncQueue({
required LatestAsyncQueueWorker<T, R> worker,
required this.defaultResult,
}) : _worker = worker;

final LatestAsyncQueueWorker<T, R> _worker;
final R defaultResult;

bool _running = false;
bool _hasPending = false;
late T _pendingValue;
Completer<R>? _cycleCompleter;

bool get isRunning => _running;

Future<R> enqueue(T value) {
_pendingValue = value;
_hasPending = true;
_cycleCompleter ??= Completer<R>();

if (_running) {
return _cycleCompleter!.future;
}

return _drainCurrentCycle();
}

Future<R> _drainCurrentCycle() async {
_running = true;
final completer = _cycleCompleter!;
var cycleResult = defaultResult;

try {
while (_hasPending) {
final value = _pendingValue;
_hasPending = false;
cycleResult = await _worker(value);
}

if (!completer.isCompleted) {
completer.complete(cycleResult);
}
return cycleResult;
} catch (e, st) {
if (!completer.isCompleted) {
completer.completeError(e, st);
}
rethrow;
} finally {
_running = false;
if (identical(_cycleCompleter, completer)) {
_cycleCompleter = null;
}
}
}
}
138 changes: 100 additions & 38 deletions lib/features/home/provider/app_setting_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:lantern/core/common/common.dart';
import 'package:lantern/core/models/app_setting.dart';
import 'package:lantern/core/services/injection_container.dart' show sl;
import 'package:lantern/core/services/local_storage_service.dart';
import 'package:lantern/core/utils/latest_async_queue.dart';
import 'package:lantern/core/utils/storage_utils.dart';
import 'package:lantern/lantern/lantern_service.dart';
import 'package:lantern/lantern/lantern_service_notifier.dart';
Expand All @@ -19,6 +20,15 @@ part 'app_setting_notifier.g.dart';
@Riverpod(keepAlive: true)
class AppSettingNotifier extends _$AppSettingNotifier {
LocalStorageService get _storage => sl<LocalStorageService>();
late final LatestAsyncQueue<RoutingMode, Either<Failure, Unit>>
_routingModeQueue = LatestAsyncQueue(
worker: _applyRoutingMode,
defaultResult: right(unit),
);
late final LatestAsyncQueue<bool, Unit> _blockAdsQueue = LatestAsyncQueue(
worker: _applyBlockAds,
defaultResult: unit,
);

@override
AppSetting build() {
Expand All @@ -36,13 +46,15 @@ class AppSettingNotifier extends _$AppSettingNotifier {

if (settings == null) {
appLogger.info(
'No stored settings found, saving defaults: ${_settingsLogFields(fallback)}');
'No stored settings found, saving defaults: ${_settingsLogFields(fallback)}',
);
unawaited(_storage.saveAppSettings(fallback));
return fallback;
}

appLogger
.info('Loaded stored app settings: ${_settingsLogFields(settings)}');
appLogger.info(
'Loaded stored app settings: ${_settingsLogFields(settings)}',
);
return settings;
}

Expand All @@ -62,19 +74,42 @@ class AppSettingNotifier extends _$AppSettingNotifier {
update(state.copyWith(newIsSpiltTunnelingOn: value));

Future<Either<Failure, Unit>> setRoutingMode(RoutingMode mode) async {
final prev = state.routingModeRaw;
if (_routingModeQueue.isRunning) {
appLogger.info(
'Routing mode update in progress. Queued latest request: ${mode.key}',
);
}

try {
return await _routingModeQueue.enqueue(mode);
} catch (e, st) {
appLogger.error('Unexpected routing mode update failure', e, st);
return left(e.toFailure());
}
}

Future<Either<Failure, Unit>> _applyRoutingMode(RoutingMode mode) async {
if (state.routingMode == mode) {
return right(unit);
}

final prev = state.routingModeRaw;
appLogger.info('Setting routing mode to: ${mode.key}');
update(state.copyWith(routingModeRaw: mode.key));
await update(state.copyWith(routingModeRaw: mode.key));

final lantern = ref.read(lanternServiceProvider);
final res = await lantern.setRoutingMode(mode == RoutingMode.smart);

res.fold((f) {
appLogger.error('Failed to set routing mode', f);
update(state.copyWith(routingModeRaw: prev));
}, (_) {});
return res;
try {
final res = await lantern.setRoutingMode(mode == RoutingMode.smart);
return await res.match((f) async {
appLogger.error('Failed to set routing mode', f);
await update(state.copyWith(routingModeRaw: prev));
return left(f);
}, (_) async => right(unit));
} catch (e, st) {
appLogger.error('Unexpected setRoutingMode error', e, st);
await update(state.copyWith(routingModeRaw: prev));
return left(e.toFailure());
}
}

void setUserLoggedIn(bool value) =>
Expand All @@ -90,16 +125,42 @@ class AppSettingNotifier extends _$AppSettingNotifier {
update(state.copyWith(successfulConnection: value));

void setBlockAds(bool value) {
final prev = state.blockAds;
update(state.copyWith(blockAds: value));
if (_blockAdsQueue.isRunning) {
appLogger.info(
'Block ads update in progress. Queued latest request: $value',
);
}
unawaited(_enqueueBlockAds(value));
}

Future<void> _enqueueBlockAds(bool value) async {
try {
await _blockAdsQueue.enqueue(value);
} catch (e, st) {
appLogger.error('Unexpected setBlockAdsEnabled error', e, st);
}
}

Future<Unit> _applyBlockAds(bool value) async {
if (state.blockAds == value) {
return unit;
}

final svc = ref.read(lanternServiceProvider);
svc.setBlockAdsEnabled(value).then((res) {
res.match((err) {
final prev = state.blockAds;
await update(state.copyWith(blockAds: value));

try {
final res = await svc.setBlockAdsEnabled(value);
await res.match((err) async {
appLogger.error('setBlockAdsEnabled failed: ${err.error}');
update(state.copyWith(blockAds: prev));
}, (_) {});
});
await update(state.copyWith(blockAds: prev));
}, (_) async {});
} catch (e, st) {
appLogger.error('Unexpected setBlockAdsEnabled failure', e, st);
await update(state.copyWith(blockAds: prev));
}
return unit;
}

void updateAnonymousDataConsent(bool value) {
Expand Down Expand Up @@ -192,8 +253,9 @@ class AppSettingNotifier extends _$AppSettingNotifier {
}

Future<void> updateTelemetryConsent(bool consent) async {
final result =
await ref.read(lanternServiceProvider).updateTelemetryEvents(consent);
final result = await ref
.read(lanternServiceProvider)
.updateTelemetryEvents(consent);

result.fold(
(err) {
Expand All @@ -208,21 +270,21 @@ class AppSettingNotifier extends _$AppSettingNotifier {
}

Map<String, Object> _settingsLogFields(AppSetting setting) => {
'isPro': setting.isPro,
'isSplitTunnelingOn': setting.isSplitTunnelingOn,
'themeMode': setting.themeMode,
'environment': setting.environment,
'locale': setting.locale,
'userLoggedIn': setting.userLoggedIn,
'blockAds': setting.blockAds,
'showSplashScreen': setting.showSplashScreen,
'telemetryDialogDismissed': setting.telemetryDialogDismissed,
'telemetryConsent': setting.telemetryConsent,
'successfulConnection': setting.successfulConnection,
'routingModeRaw': setting.routingModeRaw,
'dataCapThreshold': setting.dataCapThreshold,
'onboardingCompleted': setting.onboardingCompleted,
'hasOAuthToken': setting.oAuthToken.isNotEmpty,
'hasEmail': setting.email.isNotEmpty,
};
'isPro': setting.isPro,
'isSplitTunnelingOn': setting.isSplitTunnelingOn,
'themeMode': setting.themeMode,
'environment': setting.environment,
'locale': setting.locale,
'userLoggedIn': setting.userLoggedIn,
'blockAds': setting.blockAds,
'showSplashScreen': setting.showSplashScreen,
'telemetryDialogDismissed': setting.telemetryDialogDismissed,
'telemetryConsent': setting.telemetryConsent,
'successfulConnection': setting.successfulConnection,
'routingModeRaw': setting.routingModeRaw,
'dataCapThreshold': setting.dataCapThreshold,
'onboardingCompleted': setting.onboardingCompleted,
'hasOAuthToken': setting.oAuthToken.isNotEmpty,
'hasEmail': setting.email.isNotEmpty,
};
}
Loading
Loading