Skip to content
3 changes: 3 additions & 0 deletions assets/locales/ar.po
Original file line number Diff line number Diff line change
Expand Up @@ -1596,3 +1596,6 @@ msgstr "تم حذف حسابك بشكل دائم."

msgid "verify_with"
msgstr "التحقق عبر %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/bn.po
Original file line number Diff line number Diff line change
Expand Up @@ -1632,3 +1632,6 @@ msgstr "আপনার অ্যাকাউন্ট স্থায়ীভ

msgid "verify_with"
msgstr "%s দিয়ে যাচাই করুন"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1583,3 +1583,6 @@ msgstr "Connection failed. Please try again."

msgid "err_ruleset_failed"
msgstr "Unable to load routing configuration. Retrying..."

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/es-cu.po
Original file line number Diff line number Diff line change
Expand Up @@ -1657,3 +1657,6 @@ msgstr "Tu cuenta ha sido eliminada permanentemente. "

msgid "verify_with"
msgstr "Verificar con %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -1653,3 +1653,6 @@ msgstr "Tu cuenta ha sido eliminada permanentemente. "

msgid "verify_with"
msgstr "Verificar con %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/fa.po
Original file line number Diff line number Diff line change
Expand Up @@ -1630,3 +1630,6 @@ msgstr "حساب شما به طور دائمی حذف شده است."

msgid "verify_with"
msgstr "تأیید با %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/fr-ca.po
Original file line number Diff line number Diff line change
Expand Up @@ -2047,3 +2047,6 @@ msgstr ""

msgid "domain_fronting_error"
msgstr ""

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/fr.po
Original file line number Diff line number Diff line change
Expand Up @@ -1516,3 +1516,6 @@ msgstr ""

msgid "expired"
msgstr "Expiré"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/hi.po
Original file line number Diff line number Diff line change
Expand Up @@ -1632,3 +1632,6 @@ msgstr "आपका खाता स्थायी रूप से हटा

msgid "verify_with"
msgstr "%s से सत्यापित करें"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/ms.po
Original file line number Diff line number Diff line change
Expand Up @@ -1642,3 +1642,6 @@ msgstr "Akaun anda telah dipadamkan secara kekal. "

msgid "verify_with"
msgstr "Sahkan dengan %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/my.po
Original file line number Diff line number Diff line change
Expand Up @@ -1672,3 +1672,6 @@ msgstr "သင့်အကောင့်ကို အပြီးတိုင

msgid "verify_with"
msgstr "%s ဖြင့် အတည်ပြုပါ"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/ps.po
Original file line number Diff line number Diff line change
Expand Up @@ -1659,3 +1659,6 @@ msgstr "ستاسو حساب د تل لپاره حذف شو."

msgid "verify_with"
msgstr "د %s سره تایید وکړئ"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/ru.po
Original file line number Diff line number Diff line change
Expand Up @@ -1649,3 +1649,6 @@ msgstr "Вы не сможете его восстановить."

msgid "verify_with"
msgstr "Подтвердить через %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/th.po
Original file line number Diff line number Diff line change
Expand Up @@ -1604,3 +1604,6 @@ msgstr "บัญชีของคุณถูกลบอย่างถาว

msgid "verify_with"
msgstr "ยืนยันด้วย %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/tk.po
Original file line number Diff line number Diff line change
Expand Up @@ -1617,3 +1617,6 @@ msgstr "Hasabyňyz hemişelik pozuldy."

msgid "verify_with"
msgstr "%s bilen tassyklaň"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/tr.po
Original file line number Diff line number Diff line change
Expand Up @@ -1630,3 +1630,6 @@ msgstr "Hesabınız kalıcı olarak silindi."

msgid "verify_with"
msgstr "%s ile doğrula"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/ur.po
Original file line number Diff line number Diff line change
Expand Up @@ -1997,3 +1997,6 @@ msgstr ""

msgid "domain_fronting_error"
msgstr ""

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/vi.po
Original file line number Diff line number Diff line change
Expand Up @@ -1635,3 +1635,6 @@ msgstr "Tài khoản của bạn đã bị xóa vĩnh viễn. "

msgid "verify_with"
msgstr "Xác minh với %s"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/zh-Hans.po
Original file line number Diff line number Diff line change
Expand Up @@ -1496,3 +1496,6 @@ msgstr "您的账户已永久删除。"

msgid "verify_with"
msgstr "通过 %s 验证"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
3 changes: 3 additions & 0 deletions assets/locales/zh-Hant.po
Original file line number Diff line number Diff line change
Expand Up @@ -1498,3 +1498,6 @@ msgstr "您的帳戶已永久刪除。"

msgid "verify_with"
msgstr "透過 %s 驗證"

msgid "feature_disabled_for_build"
msgstr "This feature is not available in this build"
51 changes: 51 additions & 0 deletions docs/stealth-feature-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Stealth feature gates

Stealth artifacts disable high-identification product surfaces at compile time
with Dart environment defines. Normal builds remain unchanged.

## Build flags

Use either profile-style mode names or an explicit boolean flag:

```sh
flutter build apk --dart-define=STEALTH_MODE=stealth-vpn
flutter build apk --dart-define=STEALTH_MODE=stealth-novpn
flutter build apk --dart-define=STEALTH_MODE=true
flutter build apk --dart-define=STEALTH_BUILD=true
Comment thread
reflog marked this conversation as resolved.
flutter build apk --dart-define=STEALTH_NO_VPN=true
```
Comment thread
reflog marked this conversation as resolved.

`STEALTH_MODE=true` is kept as a compatibility alias for generic stealth
artifacts; profile-specific builds should prefer `stealth-vpn` or
`stealth-novpn`.

The app derives these gates from the stealth flag:

- `enableOAuth`
- `enablePayments`
- `enableStorePayments`
- `enableAppLinks`
- `enableSocialLinks`
- `enableAutoUpdate`

`STEALTH_NO_VPN=true` is the explicit no-VPN compatibility flag. It enables the
same feature gates as `STEALTH_BUILD=true` and is treated as stealth mode even
when `STEALTH_MODE` is not supplied.

## Disabled surfaces

Stealth builds hide or short-circuit:

- OAuth login buttons, callbacks, and SSO account deletion verification.
- Store purchase initialization, restore purchase, Google Play subscription
management, Stripe/payment redirect entry points, and upgrade CTAs.
- Runtime app-link handling in Flutter.
- Follow-us, forum, alternate download, referral/social, and project-link
surfaces.
- Desktop auto-update initialization, manual update checks, and appcast URL
resolution.

Native manifest and entitlement removal is handled by the stealth manifest
filtering build step. Artifact leakage checks should still scan final APK/IPA
and desktop bundles for OAuth provider names, app-link hosts/schemes, billing
entry points, social URLs, and appcast/update URLs.
30 changes: 30 additions & 0 deletions lib/core/common/app_build_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,36 @@ class AppBuildInfo {
defaultValue: false,
);

static const bool stealthBuild = bool.fromEnvironment(
'STEALTH_BUILD',
defaultValue: false,
);

static const String stealthModeName = String.fromEnvironment(
'STEALTH_MODE',
defaultValue: '',
);

static const bool stealthNoVpn = bool.fromEnvironment(
'STEALTH_NO_VPN',
defaultValue: false,
);

/// Removes high-identification UI and runtime flows from stealth artifacts.
static const bool suppressIdentifyingFeatures =
stealthBuild ||
Comment thread
reflog marked this conversation as resolved.
stealthNoVpn ||
stealthModeName == 'stealth-vpn' ||
stealthModeName == 'stealth-novpn' ||
stealthModeName == 'true';

static const bool enableOAuth = !suppressIdentifyingFeatures;
static const bool enablePayments = !suppressIdentifyingFeatures;
static const bool enableStorePayments = !suppressIdentifyingFeatures;
static const bool enableAppLinks = !suppressIdentifyingFeatures;
static const bool enableSocialLinks = !suppressIdentifyingFeatures;
static const bool enableAutoUpdate = !suppressIdentifyingFeatures;

/// Developer mode is exposed in debug and nightly builds only.
static bool get isDevModeEnabled => kDebugMode || buildType == 'nightly';
}
Expand Down
30 changes: 19 additions & 11 deletions lib/core/common/app_urls.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'package:lantern/core/common/app_build_info.dart';

class AppUrls {
static String lanternOfficial = 'https://lantern.io';
static String support = 'https://support.lantern.io';
static String lanternForums = 'https://lantern.io/forums';
static String get lanternForums =>
AppBuildInfo.enableSocialLinks ? 'https://lantern.io/forums' : '';
static String faq = '$lanternOfficial/faq';
static String privacyPolicy = '$lanternOfficial/privacy';
static String termsOfService = '$lanternOfficial/terms';
Expand All @@ -10,26 +13,31 @@ class AppUrls {
static String downloadIos = '$lanternOfficial/download?os=ios';
static String downloadMac = '$lanternOfficial/download?os=mac';
static String downloadLinux = '$lanternOfficial/download?os=linux';
static String lanternGithub = 'https://github.com/getlantern/lantern';
static String telegramBot = 'https://t.me/lantern_official_bot';
static String get lanternGithub => AppBuildInfo.enableSocialLinks
? 'https://github.com/getlantern/lantern'
: '';
static String get telegramBot =>
AppBuildInfo.enableSocialLinks ? 'https://t.me/lantern_official_bot' : '';
static String unbounded = 'https://unbounded.lantern.io';
static const appcastProd =
'https://s3.amazonaws.com/lantern.io/releases/production/latest/appcast.xml';
static const appcastBeta =
'https://s3.amazonaws.com/lantern.io/releases/beta/latest/appcast.xml';
static String manuallyServerSetupURL =
'https://github.com/getlantern/lantern-server-manager';
static String digitalOceanBillingUrl =
'https://cloud.digitalocean.com/account/billing';

static String appcastFor(String buildType) {
static String? appcastFor(String buildType) {
if (!AppBuildInfo.enableAutoUpdate) {
return null;
}
switch (buildType) {
case 'production':
return appcastProd;
return 'https://s3.amazonaws.com/lantern.io/releases/production/latest/appcast.xml';
case 'beta':
return appcastBeta;
return 'https://s3.amazonaws.com/lantern.io/releases/beta/latest/appcast.xml';
default:
return appcastProd;
return 'https://s3.amazonaws.com/lantern.io/releases/production/latest/appcast.xml';
}
}

static bool isLanternHost(String host) =>
host == 'lantern.io' || host == 'www.lantern.io';
Comment thread
reflog marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions lib/core/common/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ String generatePaymentRedirectIdempotencyKey() {
}

bool isStoreVersion() {
if (!AppBuildInfo.enablePayments || !AppBuildInfo.enableStorePayments) {
return false;
}
if (!PlatformUtils.isMobile) {
return false;
}
Expand Down
41 changes: 22 additions & 19 deletions lib/core/services/injection_container.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:get_it/get_it.dart';
import 'package:lantern/core/common/app_build_info.dart';
import 'package:lantern/core/services/app_purchase.dart';
import 'package:lantern/core/services/local_storage_service.dart';
import 'package:lantern/core/services/notification_service.dart';
Expand Down Expand Up @@ -43,11 +44,15 @@ Future<void> injectServices() async {
() => DeepLinkCallbackManager(),
);

appLogger.debug('Initializing AppPurchase...');
final appPurchase = AppPurchase();
appPurchase.init();
if (AppBuildInfo.enablePayments && AppBuildInfo.enableStorePayments) {
appLogger.debug('Initializing AppPurchase...');
appPurchase.init();
appLogger.debug('AppPurchase initialized');
} else {
appLogger.info('AppPurchase disabled for this build');
}
sl.registerSingleton<AppPurchase>(appPurchase);
appLogger.debug('AppPurchase initialized');

sl.registerSingleton<LanternPlatformService>(LanternPlatformService());
sl.registerSingleton<LanternFFIService>(
Expand All @@ -56,22 +61,20 @@ Future<void> injectServices() async {
: 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;
},
);
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();
Expand Down
10 changes: 9 additions & 1 deletion lib/core/updater/updater.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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';
Expand All @@ -22,6 +21,10 @@ class Updater {

if (kDebugMode) return;
if (!_isSupportedPlatform) return;
if (!AppBuildInfo.enableAutoUpdate) {
appLogger.info('autoUpdater disabled for this build');
return;
}

final flagResult = await sl<LanternService>().featureFlag();
final flags = flagResult.fold((_) => <String, dynamic>{}, (jsonStr) {
Expand All @@ -39,6 +42,10 @@ class Updater {

final buildType = AppBuildInfo.buildType;
final feedUrl = AppUrls.appcastFor(buildType);
if (feedUrl == null || feedUrl.isEmpty) {
appLogger.info('autoUpdater disabled: no appcast URL for this build');
return;
}

try {
final updater = AutoUpdater.instance;
Expand Down Expand Up @@ -66,6 +73,7 @@ class Updater {
}

Future<void> checkNow() async {
if (!AppBuildInfo.enableAutoUpdate) return;
if (!_isSupportedPlatform) return;
await AutoUpdater.instance.checkForUpdates();
}
Expand Down
Loading
Loading