diff --git a/assets/locales/en.po b/assets/locales/en.po index edacdb9175..3d73b863e6 100644 --- a/assets/locales/en.po +++ b/assets/locales/en.po @@ -488,6 +488,12 @@ msgstr "Block Ads" msgid "only_active" msgstr "Only active when VPN is connected" +msgid "share_my_connection" +msgstr "Share My Connection" + +msgid "share_my_connection_subtitle" +msgstr "Let other Lantern users route through your connection to bypass censorship." + msgid "vpn_connected" msgstr "Lantern is now connected." diff --git a/lib/core/models/radiance_settings_state.dart b/lib/core/models/radiance_settings_state.dart index 70323e0a13..28026ff95a 100644 --- a/lib/core/models/radiance_settings_state.dart +++ b/lib/core/models/radiance_settings_state.dart @@ -12,12 +12,14 @@ class RadianceSettingsState { final RoutingMode routingMode; final bool splitTunneling; final bool telemetry; + final bool peerProxy; const RadianceSettingsState({ this.blockAds = false, this.routingMode = RoutingMode.full, this.splitTunneling = false, this.telemetry = false, + this.peerProxy = false, }); RadianceSettingsState copyWith({ @@ -25,12 +27,14 @@ class RadianceSettingsState { RoutingMode? routingMode, bool? splitTunneling, bool? telemetry, + bool? peerProxy, }) { return RadianceSettingsState( blockAds: blockAds ?? this.blockAds, routingMode: routingMode ?? this.routingMode, splitTunneling: splitTunneling ?? this.splitTunneling, telemetry: telemetry ?? this.telemetry, + peerProxy: peerProxy ?? this.peerProxy, ); } @@ -41,9 +45,10 @@ class RadianceSettingsState { blockAds == other.blockAds && routingMode == other.routingMode && splitTunneling == other.splitTunneling && - telemetry == other.telemetry; + telemetry == other.telemetry && + peerProxy == other.peerProxy; @override int get hashCode => - Object.hash(blockAds, routingMode, splitTunneling, telemetry); + Object.hash(blockAds, routingMode, splitTunneling, telemetry, peerProxy); } diff --git a/lib/features/home/provider/radiance_settings_providers.dart b/lib/features/home/provider/radiance_settings_providers.dart index ae9cee0f6d..be8900aede 100644 --- a/lib/features/home/provider/radiance_settings_providers.dart +++ b/lib/features/home/provider/radiance_settings_providers.dart @@ -29,16 +29,23 @@ class RadianceSettings extends _$RadianceSettings { final routingF = svc.isSmartRoutingEnabled(); final telemetryF = svc.isTelemetryEnabled(); final splitF = PlatformUtils.isIOS ? null : svc.isSplitTunnelingEnabled(); + // Peer-proxy probe runs only on platforms with native handlers + // (Windows + Linux via FFI, macOS via MethodChannel — i.e. all desktop). + // On iOS / Android the call would fail with MissingPluginException on + // every settings init. + final peerF = PlatformUtils.isDesktop ? svc.isPeerProxyEnabled() : null; final results = await Future.wait([ blockAdsF, routingF, telemetryF, ?splitF, + ?peerF, ]); if (!ref.mounted) return; const defaults = RadianceSettingsState(); + final peerIdx = 3 + (splitF == null ? 0 : 1); state = RadianceSettingsState( blockAds: results[0].fold((_) => defaults.blockAds, (v) => v), routingMode: results[1].fold( @@ -49,6 +56,9 @@ class RadianceSettings extends _$RadianceSettings { splitTunneling: splitF == null ? defaults.splitTunneling : results[3].fold((_) => defaults.splitTunneling, (v) => v), + peerProxy: peerF == null + ? defaults.peerProxy + : results[peerIdx].fold((_) => defaults.peerProxy, (v) => v), ); } @@ -97,6 +107,16 @@ class RadianceSettings extends _$RadianceSettings { (_) => state = state.copyWith(telemetry: consent), ); } + + Future setPeerProxy(bool value) async { + final svc = ref.read(lanternServiceProvider); + final result = await svc.setPeerProxyEnabled(value); + if (!ref.mounted) return; + result.fold( + (err) => appLogger.error('setPeerProxyEnabled failed: ${err.error}'), + (_) => state = state.copyWith(peerProxy: value), + ); + } } /// Fetches whether user logged in via OAuth from radiance. diff --git a/lib/features/setting/vpn_setting.dart b/lib/features/setting/vpn_setting.dart index 8c108473a9..ec4d3920a5 100644 --- a/lib/features/setting/vpn_setting.dart +++ b/lib/features/setting/vpn_setting.dart @@ -35,6 +35,9 @@ class VPNSetting extends HookConsumerWidget { final telemetryConsent = ref.watch( radianceSettingsProvider.select((s) => s.telemetry), ); + final peerProxy = ref.watch( + radianceSettingsProvider.select((s) => s.peerProxy), + ); return ListView( padding: const EdgeInsets.all(0), @@ -117,6 +120,36 @@ class VPNSetting extends HookConsumerWidget { }, ), ), + if (PlatformUtils.isDesktop) ...{ + SizedBox(height: 16), + AppCard( + padding: EdgeInsets.zero, + child: AppTile( + label: 'share_my_connection'.i18n, + subtitle: Text( + 'share_my_connection_subtitle'.i18n, + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + letterSpacing: 0.0, + ), + ), + icon: AppImagePaths.share, + trailing: SwitchButton( + value: peerProxy, + onChanged: (bool? value) { + ref + .read(radianceSettingsProvider.notifier) + .setPeerProxy(value ?? false); + }, + ), + onPressed: () { + ref + .read(radianceSettingsProvider.notifier) + .setPeerProxy(!peerProxy); + }, + ), + ), + }, SizedBox(height: 16), AppCard( padding: EdgeInsets.zero, diff --git a/lib/lantern/lantern_core_service.dart b/lib/lantern/lantern_core_service.dart index 62f1ad2f8b..d5c4e8c48d 100644 --- a/lib/lantern/lantern_core_service.dart +++ b/lib/lantern/lantern_core_service.dart @@ -76,6 +76,10 @@ abstract class LanternCoreService { Future> isBlockAdsEnabled(); + Future> setPeerProxyEnabled(bool enabled); + + Future> isPeerProxyEnabled(); + Future> isSmartRoutingEnabled(); Future> isTelemetryEnabled(); diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index cc114b4522..c4d16a9ace 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -1508,6 +1508,34 @@ class LanternFFIService implements LanternCoreService { } } + @override + Future> setPeerProxyEnabled(bool enabled) async { + try { + final result = await runInBackground(() async { + return _ffiService + .setPeerProxyEnabled(enabled ? 1 : 0) + .cast() + .toDartString(); + }); + checkAPIError(result); + return right(unit); + } catch (e, st) { + appLogger.error('setPeerProxyEnabled error: $e', e, st); + return Left(e.toFailure()); + } + } + + @override + Future> isPeerProxyEnabled() async { + try { + final res = _ffiService.isPeerProxyEnabled(); + return right(res != 0); + } catch (e, st) { + appLogger.error('isPeerProxyEnabled error: $e', e, st); + return Left(e.toFailure()); + } + } + @override Future> isSmartRoutingEnabled() async { try { diff --git a/lib/lantern/lantern_generated_bindings.dart b/lib/lantern/lantern_generated_bindings.dart index f80e9d21d8..94ea75775b 100644 --- a/lib/lantern/lantern_generated_bindings.dart +++ b/lib/lantern/lantern_generated_bindings.dart @@ -6253,6 +6253,26 @@ class LanternBindings { late final _isBlockAdsEnabled = _isBlockAdsEnabledPtr .asFunction(); + ffi.Pointer setPeerProxyEnabled(int enabled) { + return _setPeerProxyEnabled(enabled); + } + + late final _setPeerProxyEnabledPtr = + _lookup Function(ffi.Int)>>( + 'setPeerProxyEnabled', + ); + late final _setPeerProxyEnabled = _setPeerProxyEnabledPtr + .asFunction Function(int)>(); + + int isPeerProxyEnabled() { + return _isPeerProxyEnabled(); + } + + late final _isPeerProxyEnabledPtr = + _lookup>('isPeerProxyEnabled'); + late final _isPeerProxyEnabled = _isPeerProxyEnabledPtr + .asFunction(); + ffi.Pointer setSmartRoutingEnabled(int enabled) { return _setSmartRoutingEnabled(enabled); } diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index 891362404d..d11a4de314 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -288,6 +288,30 @@ class LanternPlatformService implements LanternCoreService { } } + @override + Future> setPeerProxyEnabled(bool enabled) async { + try { + await _methodChannel.invokeMethod('setPeerProxyEnabled', { + 'enabled': enabled, + }); + return right(unit); + } catch (e, st) { + appLogger.error('setPeerProxyEnabled failed', e, st); + return Left(e.toFailure()); + } + } + + @override + Future> isPeerProxyEnabled() async { + try { + final res = await _methodChannel.invokeMethod('isPeerProxyEnabled'); + return right(res ?? false); + } catch (e, st) { + appLogger.error('isPeerProxyEnabled failed', e, st); + return Left(e.toFailure()); + } + } + @override Future> isSmartRoutingEnabled() async { try { diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index c8fb82210d..bfa2ade45a 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -773,6 +773,22 @@ class LanternService implements LanternCoreService { return _platformService.setBlockAdsEnabled(enabled); } + @override + Future> isPeerProxyEnabled() { + if (PlatformUtils.isFFISupported) { + return _ffiService.isPeerProxyEnabled(); + } + return _platformService.isPeerProxyEnabled(); + } + + @override + Future> setPeerProxyEnabled(bool enabled) { + if (PlatformUtils.isFFISupported) { + return _ffiService.setPeerProxyEnabled(enabled); + } + return _platformService.setPeerProxyEnabled(enabled); + } + @override Future> isSmartRoutingEnabled() { if (PlatformUtils.isFFISupported) { diff --git a/macos/Runner/Handlers/MethodHandler.swift b/macos/Runner/Handlers/MethodHandler.swift index a18dc691b8..57c4f5c12d 100644 --- a/macos/Runner/Handlers/MethodHandler.swift +++ b/macos/Runner/Handlers/MethodHandler.swift @@ -245,6 +245,16 @@ class MethodHandler { let enabled = data?["enabled"] as? Bool ?? false self.setBlockAdsEnabled(result: result, enabled: enabled) + case "isPeerProxyEnabled": + Task { + await MainActor.run { result(MobileIsPeerShareEnabled()) } + } + + case "setPeerProxyEnabled": + let data = call.arguments as? [String: Any] + let enabled = data?["enabled"] as? Bool ?? false + self.setPeerProxyEnabled(result: result, enabled: enabled) + case "updateTelemetryEvents": guard let consent: Bool = self.decodeValue(from: call.arguments, result: result) else { return @@ -1131,6 +1141,20 @@ class MethodHandler { } } + func setPeerProxyEnabled(result: @escaping FlutterResult, enabled: Bool) { + Task { + var error: NSError? + MobileSetPeerShareEnabled(enabled, &error) + if let error { + await self.handleFlutterError(error, result: result, code: "SET_PEER_PROXY_ERROR") + return + } + await MainActor.run { + result("ok") + } + } + } + func updateTelemetryEvents(consent: Bool, result: @escaping FlutterResult) { Task { var error: NSError?