diff --git a/assets/locales/ar.po b/assets/locales/ar.po index 538aea8775..75f9e5ea92 100644 --- a/assets/locales/ar.po +++ b/assets/locales/ar.po @@ -1641,3 +1641,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/bn.po b/assets/locales/bn.po index 0c14451cda..21d06595f5 100644 --- a/assets/locales/bn.po +++ b/assets/locales/bn.po @@ -1677,3 +1677,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/en.po b/assets/locales/en.po index e9591d27f7..3ac4df5f89 100644 --- a/assets/locales/en.po +++ b/assets/locales/en.po @@ -1634,3 +1634,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/es-cu.po b/assets/locales/es-cu.po index 1a23f1fe70..13f8eb5569 100644 --- a/assets/locales/es-cu.po +++ b/assets/locales/es-cu.po @@ -1702,3 +1702,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/es.po b/assets/locales/es.po index ce011eeb53..02fe88a6fc 100644 --- a/assets/locales/es.po +++ b/assets/locales/es.po @@ -1698,3 +1698,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/fa.po b/assets/locales/fa.po index 20db8f4377..9f075f9d6f 100644 --- a/assets/locales/fa.po +++ b/assets/locales/fa.po @@ -1675,3 +1675,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/fr-ca.po b/assets/locales/fr-ca.po index 841bc3e552..6a5e485f43 100644 --- a/assets/locales/fr-ca.po +++ b/assets/locales/fr-ca.po @@ -2092,3 +2092,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/fr.po b/assets/locales/fr.po index 0e915991b6..c92edceb47 100644 --- a/assets/locales/fr.po +++ b/assets/locales/fr.po @@ -1561,3 +1561,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/hi.po b/assets/locales/hi.po index 89ede2f6b1..3f0476ef63 100644 --- a/assets/locales/hi.po +++ b/assets/locales/hi.po @@ -1677,3 +1677,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/ms.po b/assets/locales/ms.po index 0d15f705c7..1dbd77d52d 100644 --- a/assets/locales/ms.po +++ b/assets/locales/ms.po @@ -1687,3 +1687,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/my.po b/assets/locales/my.po index 4f5116a248..bf7b025b6e 100644 --- a/assets/locales/my.po +++ b/assets/locales/my.po @@ -1717,3 +1717,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/ps.po b/assets/locales/ps.po index baaeec2dde..fd40846adc 100644 --- a/assets/locales/ps.po +++ b/assets/locales/ps.po @@ -1704,3 +1704,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/ru.po b/assets/locales/ru.po index ededd01f0f..f7320e6799 100644 --- a/assets/locales/ru.po +++ b/assets/locales/ru.po @@ -1694,3 +1694,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/th.po b/assets/locales/th.po index bb75f7f4ea..abd20e2eee 100644 --- a/assets/locales/th.po +++ b/assets/locales/th.po @@ -1649,3 +1649,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/tk.po b/assets/locales/tk.po index f5975eed86..d8190c7cc5 100644 --- a/assets/locales/tk.po +++ b/assets/locales/tk.po @@ -1662,3 +1662,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/tr.po b/assets/locales/tr.po index 9a47b094ff..9e3b2a3fcc 100644 --- a/assets/locales/tr.po +++ b/assets/locales/tr.po @@ -1675,3 +1675,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/ur.po b/assets/locales/ur.po index d58df7d11b..c6baf4dbc3 100644 --- a/assets/locales/ur.po +++ b/assets/locales/ur.po @@ -2042,3 +2042,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/vi.po b/assets/locales/vi.po index bbd35dd377..eed8d99277 100644 --- a/assets/locales/vi.po +++ b/assets/locales/vi.po @@ -1680,3 +1680,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/zh-Hans.po b/assets/locales/zh-Hans.po index 038fe5adad..36f41248e5 100644 --- a/assets/locales/zh-Hans.po +++ b/assets/locales/zh-Hans.po @@ -1541,3 +1541,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/assets/locales/zh-Hant.po b/assets/locales/zh-Hant.po index 06caa05bca..cdf137836f 100644 --- a/assets/locales/zh-Hant.po +++ b/assets/locales/zh-Hant.po @@ -1543,3 +1543,7 @@ msgstr "Couldn't check for updates" msgid "check_connection_and_retry" msgstr "Check your connection and try again" + +msgid "feature_disabled_for_build" +msgstr "This feature is not available in this build" + diff --git a/docs/stealth-feature-gates.md b/docs/stealth-feature-gates.md new file mode 100644 index 0000000000..5d8ca6bb2a --- /dev/null +++ b/docs/stealth-feature-gates.md @@ -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 +flutter build apk --dart-define=STEALTH_NO_VPN=true +``` + +`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. diff --git a/lib/core/common/app_build_info.dart b/lib/core/common/app_build_info.dart index dd800e9920..7bc16e55d2 100644 --- a/lib/core/common/app_build_info.dart +++ b/lib/core/common/app_build_info.dart @@ -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 || + 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'; } diff --git a/lib/core/common/app_urls.dart b/lib/core/common/app_urls.dart index 9dfc1d917c..119f5c06c5 100644 --- a/lib/core/common/app_urls.dart +++ b/lib/core/common/app_urls.dart @@ -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'; @@ -10,13 +13,12 @@ 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 = @@ -24,14 +26,20 @@ class AppUrls { static String androidSideloadUpdateEndpoint = 'https://update.getlantern.org/update/lantern'; - 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'; } diff --git a/lib/core/common/common.dart b/lib/core/common/common.dart index 98ea81a593..8566b005a2 100644 --- a/lib/core/common/common.dart +++ b/lib/core/common/common.dart @@ -93,6 +93,9 @@ String generatePaymentRedirectIdempotencyKey() { } bool isStoreVersion() { + if (!AppBuildInfo.enablePayments || !AppBuildInfo.enableStorePayments) { + return false; + } if (!PlatformUtils.isMobile) { return false; } diff --git a/lib/core/services/injection_container.dart b/lib/core/services/injection_container.dart index b93d720402..0f41a7d03b 100644 --- a/lib/core/services/injection_container.dart +++ b/lib/core/services/injection_container.dart @@ -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'; @@ -47,7 +48,9 @@ Future injectServices() async { appLogger.debug('Registering AppPurchase...'); final appPurchase = AppPurchase(); sl.registerSingleton(appPurchase); - if (PlatformUtils.isIOS) { + if (PlatformUtils.isIOS && + AppBuildInfo.enablePayments && + AppBuildInfo.enableStorePayments) { // StoreKit can deliver pending transaction updates at launch; init() // only subscribes to the stream and does not fetch product details. appPurchase.init(); diff --git a/lib/core/updater/updater.dart b/lib/core/updater/updater.dart index d65d4782a5..61961dce28 100644 --- a/lib/core/updater/updater.dart +++ b/lib/core/updater/updater.dart @@ -29,6 +29,10 @@ class Updater { _initialized = true; if (kDebugMode || !_isSupportedPlatform) return; + if (!AppBuildInfo.enableAutoUpdate) { + appLogger.info('autoUpdater disabled for this build'); + return; + } final flags = await _featureFlags(); if (_isAndroidPlatform) { @@ -61,6 +65,10 @@ class Updater { try { 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; + } final updater = AutoUpdater.instance; await updater.setFeedURL(feedUrl); await updater.setScheduledCheckInterval(3600); @@ -86,6 +94,7 @@ class Updater { } Future checkNow() async { + if (!AppBuildInfo.enableAutoUpdate) return; if (!_isSupportedPlatform) return; final flags = await _featureFlags(); diff --git a/lib/core/utils/failure.dart b/lib/core/utils/failure.dart index 80cd216980..43d1ed139f 100644 --- a/lib/core/utils/failure.dart +++ b/lib/core/utils/failure.dart @@ -6,6 +6,11 @@ class Failure { Failure({required this.error, required this.localizedErrorMessage}); + factory Failure.featureDisabled(String feature) => Failure( + error: '$feature disabled for this build', + localizedErrorMessage: 'feature_disabled_for_build'.i18n, + ); + @override String toString() => 'Failure(error: $error, localizedErrorMessage: $localizedErrorMessage)'; @@ -13,8 +18,8 @@ class Failure { class VpnConflictFailure extends Failure { VpnConflictFailure() - : super( - error: 'vpn_conflict', - localizedErrorMessage: 'vpn_conflict_body'.i18n, - ); + : super( + error: 'vpn_conflict', + localizedErrorMessage: 'vpn_conflict_body'.i18n, + ); } diff --git a/lib/core/widgets/app_webview.dart b/lib/core/widgets/app_webview.dart index 16aeee0018..00fbccdc1c 100644 --- a/lib/core/widgets/app_webview.dart +++ b/lib/core/widgets/app_webview.dart @@ -143,9 +143,6 @@ class _InnerWebViewState extends ConsumerState<_InnerWebView> { ); } - bool isLanternHost(String host) => - host == 'lantern.io' || host == 'www.lantern.io'; - String? _extractPurchaseResult(Uri uri) { var purchaseResult = uri.queryParameters['purchaseResult']; if (purchaseResult != null) { @@ -186,7 +183,8 @@ class _InnerWebViewState extends ConsumerState<_InnerWebView> { } // OAuth callback. - if (uri.scheme == 'lantern' && + if (AppBuildInfo.enableOAuth && + uri.scheme == 'lantern' && uri.host == 'auth' && uri.queryParameters.containsKey('token')) { loading.stop(); @@ -194,25 +192,28 @@ class _InnerWebViewState extends ConsumerState<_InnerWebView> { return true; } - final purchaseResult = _extractPurchaseResult(uri); - if (purchaseResult != null && isLanternHost(uri.host)) { - loading.stop(); - await appRouter.maybePop(purchaseResult.toLowerCase() == 'true'); - return true; - } - - /// Alipay trade_status=TRADE_SUCCESS once the user has finished paying. - final tradeStatus = uri.queryParameters['trade_status']; - if (tradeStatus != null && tradeStatus.toUpperCase() == 'TRADE_SUCCESS') { - appLogger.info( - 'Webview detected Alipay trade_status=TRADE_SUCCESS on ${uri.host}, closing', - ); - loading.stop(); - await appRouter.maybePop(true); - return true; + if (AppBuildInfo.enablePayments) { + final purchaseResult = _extractPurchaseResult(uri); + if (purchaseResult != null && AppUrls.isLanternHost(uri.host)) { + loading.stop(); + await appRouter.maybePop(purchaseResult.toLowerCase() == 'true'); + return true; + } + + /// Alipay trade_status=TRADE_SUCCESS once the user has finished paying. + final tradeStatus = uri.queryParameters['trade_status']; + if (tradeStatus != null && tradeStatus.toUpperCase() == 'TRADE_SUCCESS') { + appLogger.info( + 'Webview detected Alipay trade_status=TRADE_SUCCESS on ${uri.host}, closing', + ); + loading.stop(); + await appRouter.maybePop(true); + return true; + } } - if (isLanternHost(uri.host) && + if (AppBuildInfo.enableOAuth && + AppUrls.isLanternHost(uri.host) && uri.path == '/auth' && uri.queryParameters.containsKey('token')) { loading.stop(); @@ -250,7 +251,7 @@ class _InnerWebViewState extends ConsumerState<_InnerWebView> { return NavigationActionPolicy.CANCEL; } - if (isLanternHost(u.host) && (u.path == '/' || u.path.isEmpty)) { + if (AppUrls.isLanternHost(u.host) && (u.path == '/' || u.path.isEmpty)) { return NavigationActionPolicy.ALLOW; } diff --git a/lib/core/widgets/oauth_login.dart b/lib/core/widgets/oauth_login.dart index 78d55a7e35..77d19c1343 100644 --- a/lib/core/widgets/oauth_login.dart +++ b/lib/core/widgets/oauth_login.dart @@ -32,6 +32,9 @@ class OAuthLogin extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + if (!AppBuildInfo.enableOAuth) { + return const SizedBox.shrink(); + } if (methodType == SignUpMethodType.apple) { return SecondaryButton( label: label ?? 'continue_with_apple'.i18n, @@ -60,6 +63,7 @@ class OAuthLogin extends HookConsumerWidget { WidgetRef ref, BuildContext context, ) async { + if (!AppBuildInfo.enableOAuth) return; final allowed = await _isRegionAllowed(ref, context); if (!allowed || !context.mounted) return; await oAuthLogin(type, ref, context); @@ -93,6 +97,7 @@ class OAuthLogin extends HookConsumerWidget { WidgetRef ref, BuildContext context, ) async { + if (!AppBuildInfo.enableOAuth) return; context.showLoadingDialog(); final result = await ref.read(authProvider.notifier).oAuthLogin(type.name); result.fold( diff --git a/lib/core/widgets/pro_banner.dart b/lib/core/widgets/pro_banner.dart index 24c572f31a..b0bc07cdd3 100644 --- a/lib/core/widgets/pro_banner.dart +++ b/lib/core/widgets/pro_banner.dart @@ -9,23 +9,23 @@ class ProBanner extends HookConsumerWidget { final double topMargin; - const ProBanner({ - super.key, - this.title, - this.topMargin = 16, - }); + const ProBanner({super.key, this.title, this.topMargin = 16}); @override Widget build(BuildContext context, WidgetRef ref) { + if (!AppBuildInfo.enablePayments) { + return const SizedBox.shrink(); + } final isExpired = ref.watch(isUserExpiredProvider); final textTheme = Theme.of(context).textTheme; return Container( margin: EdgeInsets.only(top: topMargin), padding: EdgeInsets.all(defaultSize), decoration: BoxDecoration( - color: context.bgPromo, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: context.borderPromo, width: 1)), + color: context.bgPromo, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: context.borderPromo, width: 1), + ), child: Column( children: [ AutoSizeText( diff --git a/lib/features/account/account.dart b/lib/features/account/account.dart index 845ab53a8f..25926d68f6 100644 --- a/lib/features/account/account.dart +++ b/lib/features/account/account.dart @@ -54,7 +54,7 @@ class Account extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isUserFree) + if (isUserFree && AppBuildInfo.enablePayments) ProButton( onPressed: () { appRouter.push(const Plans()); @@ -71,12 +71,13 @@ class Account extends HookConsumerWidget { text: 'pro_subscription_expired_message'.i18n, ), SizedBox(height: defaultSize), - ProButton( - label: 'renew_pro_subscription'.i18n, - onPressed: () { - appRouter.push(const Plans()); - }, - ), + if (AppBuildInfo.enablePayments) + ProButton( + label: 'renew_pro_subscription'.i18n, + onPressed: () { + appRouter.push(const Plans()); + }, + ), }, SizedBox(height: defaultSize), Padding( @@ -196,6 +197,9 @@ class Account extends HookConsumerWidget { final autoRenew = user.legacyUserData.subscriptionData.autoRenew; final isUserExpired = user.legacyUserData.userLevel == 'expired'; final isUserPro = user.legacyUserData.isPro; + if (!AppBuildInfo.enablePayments) { + return null; + } ///User has an active subscription with auto-renew enabled if (!isUserExpired && autoRenew) { @@ -220,6 +224,9 @@ class Account extends HookConsumerWidget { BuildContext buildContext, UserResponseModel user, ) async { + if (!AppBuildInfo.enablePayments) { + return; + } final provider = user.legacyUserData.subscriptionData.provider; switch (provider) { case 'apple': @@ -255,6 +262,10 @@ class Account extends HookConsumerWidget { } void onRenewTap() { + if (!AppBuildInfo.enablePayments) { + return; + } + /// Most user renewal attempts are one-time purchases. /// Send the user to the plans page. appRouter.push(const Plans()); diff --git a/lib/features/account/delete_account.dart b/lib/features/account/delete_account.dart index f077197748..49062937ad 100644 --- a/lib/features/account/delete_account.dart +++ b/lib/features/account/delete_account.dart @@ -16,7 +16,7 @@ class DeleteAccount extends StatefulHookConsumerWidget { const DeleteAccount({super.key}); @override - _DeleteAccountState createState() => _DeleteAccountState(); + ConsumerState createState() => _DeleteAccountState(); } class _DeleteAccountState extends ConsumerState { @@ -36,9 +36,15 @@ class _DeleteAccountState extends ConsumerState { final textTheme = Theme.of(context).textTheme; final passwordController = useTextEditingController(); final buttonEnabled = useState(false); - final isSSOUser = ref.watch(isSSOUserProvider).value ?? false; - final oAuthProviderName = ref.watch(oAuthProviderProvider).value ?? ''; - final oAuthMethodType = _resolveOAuthMethodType(oAuthProviderName); + final isSSOUser = + AppBuildInfo.enableOAuth && + (ref.watch(isSSOUserProvider).value ?? false); + final oAuthProviderName = AppBuildInfo.enableOAuth + ? (ref.watch(oAuthProviderProvider).value ?? '') + : ''; + final oAuthMethodType = AppBuildInfo.enableOAuth + ? _resolveOAuthMethodType(oAuthProviderName) + : SignUpMethodType.email; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -53,8 +59,11 @@ class _DeleteAccountState extends ConsumerState { ), SizedBox(height: defaultSize), Center( - child: Text('delete_account_?'.i18n, - style: textTheme.headlineSmall)), + child: Text( + 'delete_account_?'.i18n, + style: textTheme.headlineSmall, + ), + ), SizedBox(height: defaultSize), Padding( padding: const EdgeInsets.only(left: 16), @@ -70,9 +79,9 @@ class _DeleteAccountState extends ConsumerState { padding: const EdgeInsets.only(left: 16), child: Text( isSSOUser - ? 'confirm_with_account' - .i18n - .fill([oAuthProviderName.capitalize]) + ? 'confirm_with_account'.i18n.fill([ + oAuthProviderName.capitalize, + ]) : 'delete_account_message_two'.i18n, style: textTheme.bodyLarge!.copyWith( color: context.textSecondary, @@ -95,9 +104,7 @@ class _DeleteAccountState extends ConsumerState { SizedBox(height: size24), if (isSSOUser) OAuthLogin( - label: 'verify_with' - .i18n - .fill([oAuthProviderName.capitalize]), + label: 'verify_with'.i18n.fill([oAuthProviderName.capitalize]), methodType: oAuthMethodType, bgColor: context.actionPrimaryBg, foregroundColor: context.actionPrimaryText, @@ -126,6 +133,8 @@ class _DeleteAccountState extends ConsumerState { } void processOAuthResult(Map payload) { + // Only reachable when isSSOUser==true, which short-circuits to false + // when AppBuildInfo.enableOAuth==false, so no extra guard needed here. final token = payload['token'] as String? ?? ''; if (token.isEmpty) { @@ -148,8 +157,7 @@ class _DeleteAccountState extends ConsumerState { } final currentEmail = ref.read(userEmailProvider); - if (currentEmail.isNotEmpty && - newTokenData['email'] != currentEmail) { + if (currentEmail.isNotEmpty && newTokenData['email'] != currentEmail) { context.showSnackBarError('oauth_different_account'.i18n); return; } @@ -170,8 +178,7 @@ class _DeleteAccountState extends ConsumerState { result.fold( (failure) { - appLogger - .error('Account deletion failed: ${failure.error}'); + appLogger.error('Account deletion failed: ${failure.error}'); context.hideLoadingDialog(); context.showSnackBarError(failure.localizedErrorMessage); }, @@ -179,7 +186,8 @@ class _DeleteAccountState extends ConsumerState { context.hideLoadingDialog(); ref.read(appSettingProvider.notifier).setUserLoggedIn(false); appLogger.info( - 'Account deletion successful, clearing user data and navigating to root'); + 'Account deletion successful, clearing user data and navigating to root', + ); ref.read(homeProvider.notifier).updateUserData(userResponse); showAccountDeletionSuccessDialog(); }, @@ -193,27 +201,28 @@ class _DeleteAccountState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 24), - AppImage( - path: AppImagePaths.greenCheck, - useThemeColor: false, - ), + AppImage(path: AppImagePaths.greenCheck, useThemeColor: false), SizedBox(height: 16), - Text('account_deleted'.i18n, - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: context.textPrimary, - )), + Text( + 'account_deleted'.i18n, + style: Theme.of( + context, + ).textTheme.headlineSmall!.copyWith(color: context.textPrimary), + ), SizedBox(height: 16), - Text('account_deleted_message'.i18n, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: context.textPrimary, - )), + Text( + 'account_deleted_message'.i18n, + style: Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: context.textPrimary), + ), ], ), action: [ AppTextButton( label: 'close'.i18n, onPressed: () => appRouter.popUntilRoot(), - ) + ), ], ); } diff --git a/lib/features/auth/add_email.dart b/lib/features/auth/add_email.dart index 7a230ebb6a..4d914f782a 100644 --- a/lib/features/auth/add_email.dart +++ b/lib/features/auth/add_email.dart @@ -145,24 +145,26 @@ class _AddEmailState extends ConsumerState { ), ), if (!isUserRegistered) ...{ - SizedBox(height: defaultSize), - DividerSpace(), - SizedBox(height: defaultSize), - OAuthLogin( - methodType: SignUpMethodType.google, - onResult: (token) => - onOAuthResult(token, SignUpMethodType.google), - ), - SizedBox(height: defaultSize), - OAuthLogin( - methodType: SignUpMethodType.apple, - foregroundColor: context.textPrimary, - onResult: (token) => - onOAuthResult(token, SignUpMethodType.apple), - ), - SizedBox(height: defaultSize), - DividerSpace(), - SizedBox(height: defaultSize), + if (AppBuildInfo.enableOAuth) ...[ + SizedBox(height: defaultSize), + DividerSpace(), + SizedBox(height: defaultSize), + OAuthLogin( + methodType: SignUpMethodType.google, + onResult: (token) => + onOAuthResult(token, SignUpMethodType.google), + ), + SizedBox(height: defaultSize), + OAuthLogin( + methodType: SignUpMethodType.apple, + foregroundColor: context.textPrimary, + onResult: (token) => + onOAuthResult(token, SignUpMethodType.apple), + ), + SizedBox(height: defaultSize), + DividerSpace(), + SizedBox(height: defaultSize), + ], if (isStoreVersion() && widget.authFlow == AuthFlow.signUp) Center( child: AppTextButton( @@ -347,6 +349,9 @@ class _AddEmailState extends ConsumerState { Map result, SignUpMethodType type, ) async { + if (!AppBuildInfo.enableOAuth) { + return; + } final token = result['token']; if (token != null) { context.showLoadingDialog(); diff --git a/lib/features/auth/choose_payment_method.dart b/lib/features/auth/choose_payment_method.dart index 935f96379f..5215031bf0 100644 --- a/lib/features/auth/choose_payment_method.dart +++ b/lib/features/auth/choose_payment_method.dart @@ -29,6 +29,17 @@ class ChoosePaymentMethod extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + if (!AppBuildInfo.enablePayments) { + return BaseScreen( + title: 'payment'.i18n, + body: Center( + child: Text( + Failure.featureDisabled('Payment').localizedErrorMessage, + textAlign: TextAlign.center, + ), + ), + ); + } final userPlan = ref.watch(plansProvider.notifier).getSelectedPlan(); final plansAsync = ref.watch(plansProvider); final paymentRedirectInFlight = useState(false); diff --git a/lib/features/auth/confirm_email.dart b/lib/features/auth/confirm_email.dart index 4f6269d071..b4b781ac4e 100644 --- a/lib/features/auth/confirm_email.dart +++ b/lib/features/auth/confirm_email.dart @@ -197,7 +197,8 @@ class ConfirmEmail extends HookConsumerWidget { /// If user is from store version no need to check anything /// send them to create password directly - if (PlatformUtils.isMobile && isStoreVersion()) { + if (!AppBuildInfo.enablePayments || + (PlatformUtils.isMobile && isStoreVersion())) { appRouter.push( CreatePassword(email: email, authFlow: authFlow, code: code), ); diff --git a/lib/features/auth/provider/auth_notifier.dart b/lib/features/auth/provider/auth_notifier.dart index 9698b72e53..64a91e0016 100644 --- a/lib/features/auth/provider/auth_notifier.dart +++ b/lib/features/auth/provider/auth_notifier.dart @@ -1,4 +1,5 @@ import 'package:fpdart/fpdart.dart'; +import 'package:lantern/core/common/app_build_info.dart'; import 'package:lantern/core/utils/failure.dart'; import 'package:lantern/lantern/lantern_service_notifier.dart'; import 'package:lantern/core/models/user.dart'; @@ -14,28 +15,37 @@ class AuthNotifier extends _$AuthNotifier { } Future> oAuthLogin(String provider) async { + if (!AppBuildInfo.enableOAuth) { + return left(Failure.featureDisabled('OAuth login')); + } return ref.read(lanternServiceProvider).getOAuthLoginUrl(provider); } Future> oAuthLoginCallback( - String authToken) async { + String authToken, + ) async { + if (!AppBuildInfo.enableOAuth) { + return left(Failure.featureDisabled('OAuth login')); + } return ref.read(lanternServiceProvider).oAuthLoginCallback(authToken); } Future> signInWithEmail( - String email, String password) async { - return ref.read(lanternServiceProvider).login( - email: email, - password: password, - ); + String email, + String password, + ) async { + return ref + .read(lanternServiceProvider) + .login(email: email, password: password); } Future> signUpWithEmail( - String email, String password) async { - return ref.read(lanternServiceProvider).signUp( - email: email, - password: password, - ); + String email, + String password, + ) async { + return ref + .read(lanternServiceProvider) + .signUp(email: email, password: password); } //Forgot password @@ -44,16 +54,22 @@ class AuthNotifier extends _$AuthNotifier { } Future> validateRecoveryCode( - String email, String code) async { - return ref.read(lanternServiceProvider).validateRecoveryCode( - email: email, - code: code, - ); + String email, + String code, + ) async { + return ref + .read(lanternServiceProvider) + .validateRecoveryCode(email: email, code: code); } Future> completeRecoveryByEmail( - String email, String newPassword, String code) async { - return ref.read(lanternServiceProvider).completeRecoveryByEmail( + String email, + String newPassword, + String code, + ) async { + return ref + .read(lanternServiceProvider) + .completeRecoveryByEmail( email: email, newPassword: newPassword, code: code, @@ -61,20 +77,33 @@ class AuthNotifier extends _$AuthNotifier { } Future> startChangeEmail( - String newEmail, String password) async { + String newEmail, + String password, + ) async { return ref .read(lanternServiceProvider) .startChangeEmail(newEmail, password); } Future> completeChangeEmail( - String newEmail, String password, String code) async { - return ref.read(lanternServiceProvider).completeChangeEmail( - newEmail: newEmail, password: password, code: code); + String newEmail, + String password, + String code, + ) async { + return ref + .read(lanternServiceProvider) + .completeChangeEmail( + newEmail: newEmail, + password: password, + code: code, + ); } Future> deleteAccount( - String email, String password, bool isSSO) async { + String email, + String password, + bool isSSO, + ) async { return ref .read(lanternServiceProvider) .deleteAccount(password: password, email: email, isSSO: isSSO); diff --git a/lib/features/auth/sign_in_email.dart b/lib/features/auth/sign_in_email.dart index 256ee00d0a..44dacfe2dd 100644 --- a/lib/features/auth/sign_in_email.dart +++ b/lib/features/auth/sign_in_email.dart @@ -69,22 +69,32 @@ class SignInEmail extends HookConsumerWidget { onPressed: () => signInWithEmail(emailController.text, context), isTaller: true, ), - SizedBox(height: defaultSize), - DividerSpace(), - SizedBox(height: defaultSize), - OAuthLogin( - methodType: SignUpMethodType.google, - onResult: (token) => - onOAuthResult(token, context, ref, SignUpMethodType.google), - ), - SizedBox(height: defaultSize), - OAuthLogin( - methodType: SignUpMethodType.apple, - onResult: (token) => - onOAuthResult(token, context, ref, SignUpMethodType.apple), - ), - SizedBox(height: defaultSize), - DividerSpace(), + if (AppBuildInfo.enableOAuth) ...[ + SizedBox(height: defaultSize), + DividerSpace(), + SizedBox(height: defaultSize), + OAuthLogin( + methodType: SignUpMethodType.google, + onResult: (token) => onOAuthResult( + token, + context, + ref, + SignUpMethodType.google, + ), + ), + SizedBox(height: defaultSize), + OAuthLogin( + methodType: SignUpMethodType.apple, + onResult: (token) => onOAuthResult( + token, + context, + ref, + SignUpMethodType.apple, + ), + ), + SizedBox(height: defaultSize), + DividerSpace(), + ], SizedBox(height: 32), AppRichText( texts: '${'new_to_lantern_pro'.i18n} ', @@ -116,6 +126,9 @@ class SignInEmail extends HookConsumerWidget { WidgetRef ref, SignUpMethodType type, ) async { + if (!AppBuildInfo.enableOAuth) { + return; + } final token = oauthResult['token']; if (token != null) { context.showLoadingDialog(); diff --git a/lib/features/home/provider/radiance_settings_providers.dart b/lib/features/home/provider/radiance_settings_providers.dart index ae9cee0f6d..89932235a6 100644 --- a/lib/features/home/provider/radiance_settings_providers.dart +++ b/lib/features/home/provider/radiance_settings_providers.dart @@ -102,6 +102,9 @@ class RadianceSettings extends _$RadianceSettings { /// Fetches whether user logged in via OAuth from radiance. @riverpod Future isOAuthLogin(Ref ref) async { + if (!AppBuildInfo.enableOAuth) { + return false; + } final svc = ref.read(lanternServiceProvider); final result = await svc.isOAuthLogin(); return result.fold((_) => false, (v) => v); @@ -110,6 +113,9 @@ Future isOAuthLogin(Ref ref) async { /// Fetches OAuth provider name from radiance. @riverpod Future oAuthProvider(Ref ref) async { + if (!AppBuildInfo.enableOAuth) { + return ''; + } final svc = ref.read(lanternServiceProvider); final result = await svc.getOAuthProvider(); return result.fold((_) => '', (v) => v); @@ -118,6 +124,9 @@ Future oAuthProvider(Ref ref) async { /// Whether the user is an SSO user (OAuth login with a provider set). @riverpod Future isSSOUser(Ref ref) async { + if (!AppBuildInfo.enableOAuth) { + return false; + } final isOAuth = await ref.watch(isOAuthLoginProvider.future); final provider = await ref.watch(oAuthProviderProvider.future); return isOAuth && provider.isNotEmpty; diff --git a/lib/features/plans/plans.dart b/lib/features/plans/plans.dart index 75c909cba4..0852b43821 100644 --- a/lib/features/plans/plans.dart +++ b/lib/features/plans/plans.dart @@ -191,6 +191,9 @@ class _PlansState extends ConsumerState } void onMenuTap() { + if (!AppBuildInfo.enablePayments) { + return; + } final isReferralApplied = ref.read(referralProvider); showAppBottomSheet( context: context, @@ -309,6 +312,10 @@ class _PlansState extends ConsumerState } void onGetLanternProTap() { + if (!AppBuildInfo.enablePayments) { + appRouter.push(AddEmail(authFlow: AuthFlow.lanternProLicense)); + return; + } final userSelectedPlan = ref.read(plansProvider.notifier).getSelectedPlan(); appLogger.info( 'Get Lantern Pro button tapped with plan: ${userSelectedPlan.id}', @@ -376,6 +383,9 @@ class _PlansState extends ConsumerState } Future startInAppPurchaseFlow(Plan plan) async { + if (!AppBuildInfo.enableStorePayments) { + return; + } context.showLoadingDialog(); // Mark a payment as in flight so that ConfirmEmail's back-press guard // (confirm_email.dart) preserves the anonymous account if the user diff --git a/lib/features/plans/provider/payment_notifier.dart b/lib/features/plans/provider/payment_notifier.dart index 6dfde625b0..655a2e7694 100644 --- a/lib/features/plans/provider/payment_notifier.dart +++ b/lib/features/plans/provider/payment_notifier.dart @@ -32,6 +32,9 @@ class PaymentNotifier extends _$PaymentNotifier { required PaymentSuccessCallback onSuccess, required PaymentErrorCallback onError, }) async { + if (!AppBuildInfo.enableStorePayments) { + return left(Failure.featureDisabled('In-app purchase')); + } return ref .read(lanternServiceProvider) .startInAppPurchaseFlow( @@ -45,6 +48,9 @@ class PaymentNotifier extends _$PaymentNotifier { required String purchaseToken, required String planId, }) async { + if (!AppBuildInfo.enableStorePayments) { + return left(Failure.featureDisabled('In-app purchase')); + } return ref .read(lanternServiceProvider) .acknowledgeInAppPurchase(purchaseToken: purchaseToken, planId: planId); @@ -53,6 +59,9 @@ class PaymentNotifier extends _$PaymentNotifier { Future> restoreInAppPurchase({ required String purchaseToken, }) async { + if (!AppBuildInfo.enableStorePayments) { + return left(Failure.featureDisabled('Restore purchase')); + } return ref .read(lanternServiceProvider) .restoreInAppPurchase(purchaseToken: purchaseToken); @@ -63,6 +72,9 @@ class PaymentNotifier extends _$PaymentNotifier { String planId, String email, ) async { + if (!AppBuildInfo.enablePayments) { + return left(Failure.featureDisabled('Payment')); + } final idempotencyKey = generatePaymentRedirectIdempotencyKey(); return ref .read(lanternServiceProvider) @@ -78,6 +90,9 @@ class PaymentNotifier extends _$PaymentNotifier { String planId, String email, ) async { + if (!AppBuildInfo.enablePayments) { + return left(Failure.featureDisabled('Payment')); + } return ref .read(lanternServiceProvider) .stipeSubscription(planId: planId, email: email); @@ -88,6 +103,9 @@ class PaymentNotifier extends _$PaymentNotifier { required String planId, required String email, }) async { + if (!AppBuildInfo.enablePayments) { + return left(Failure.featureDisabled('Payment')); + } final idempotencyKey = generatePaymentRedirectIdempotencyKey(); return ref .read(lanternServiceProvider) @@ -107,6 +125,9 @@ class PaymentNotifier extends _$PaymentNotifier { required PaymentErrorCallback onError, required String provider, }) async { + if (!AppBuildInfo.enablePayments) { + return left(Failure.featureDisabled('Payment')); + } if (_isAndroidStoreBuild) { // Google Play build uses IAP final result = await startInAppPurchaseFlow( diff --git a/lib/features/plans/restore_purchase_mixin.dart b/lib/features/plans/restore_purchase_mixin.dart index dd40c51b8f..7247ef7c4c 100644 --- a/lib/features/plans/restore_purchase_mixin.dart +++ b/lib/features/plans/restore_purchase_mixin.dart @@ -13,6 +13,9 @@ import 'package:lantern/features/plans/provider/payment_notifier.dart'; mixin RestorePurchaseMixin on ConsumerState { Future restorePurchaseFlow() async { + if (!AppBuildInfo.enableStorePayments) { + return; + } if (!mounted) return; context.showLoadingDialog(); try { @@ -38,6 +41,9 @@ mixin RestorePurchaseMixin } Future _onRestoredPurchase(PurchaseDetails purchaseDetails) async { + if (!AppBuildInfo.enableStorePayments) { + return; + } sl().clearCallbacks(); final purchaseToken = purchaseDetails.verificationData.serverVerificationData; diff --git a/lib/features/setting/download_links.dart b/lib/features/setting/download_links.dart index c5561f791c..96bf668fe1 100644 --- a/lib/features/setting/download_links.dart +++ b/lib/features/setting/download_links.dart @@ -3,13 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:lantern/core/common/common.dart'; -enum _PlatformType { - android, - ios, - windows, - macos, - linux, -} +enum _PlatformType { android, ios, windows, macos, linux } @RoutePage(name: 'DownloadLinks') class DownloadLinks extends StatelessWidget { @@ -17,6 +11,12 @@ class DownloadLinks extends StatelessWidget { @override Widget build(BuildContext context) { + if (!AppBuildInfo.enableSocialLinks) { + return BaseScreen( + title: 'download_links'.i18n, + body: const SizedBox.shrink(), + ); + } return BaseScreen(title: 'download_links'.i18n, body: _buildBody(context)); } diff --git a/lib/features/setting/follow_us.dart b/lib/features/setting/follow_us.dart index 3be8027650..581123ad41 100644 --- a/lib/features/setting/follow_us.dart +++ b/lib/features/setting/follow_us.dart @@ -13,6 +13,9 @@ class FollowUs extends StatelessWidget { @override Widget build(BuildContext context) { + if (!AppBuildInfo.enableSocialLinks) { + return BaseScreen(title: 'follow_us'.i18n, body: const SizedBox.shrink()); + } return BaseScreen(title: 'follow_us'.i18n, body: _buildBody()); } diff --git a/lib/features/setting/setting.dart b/lib/features/setting/setting.dart index 027eb40279..9971bcec35 100644 --- a/lib/features/setting/setting.dart +++ b/lib/features/setting/setting.dart @@ -79,7 +79,7 @@ class _SettingState extends ConsumerState body: ListView( padding: EdgeInsets.symmetric(horizontal: defaultSize), children: [ - if (!isUserPro) + if (!isUserPro && AppBuildInfo.enablePayments) Padding( padding: const EdgeInsets.only(top: 16), child: ProButton( @@ -179,35 +179,38 @@ class _SettingState extends ConsumerState icon: AppImagePaths.support, onPressed: () => settingMenuTap(_SettingType.support), ), - FutureBuilder( - future: _canCheckForUpdates, - builder: (context, snapshot) { - final show = - PlatformUtils.isDesktop || - (snapshot.connectionState == ConnectionState.done && - snapshot.data == true); - if (!show) return const SizedBox.shrink(); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - DividerSpace(), - AppTile( - label: 'check_for_updates'.i18n, - icon: AppImagePaths.update, - onPressed: () async => await settingMenuTap( - _SettingType.checkForUpdates, + if (AppBuildInfo.enableAutoUpdate) + FutureBuilder( + future: _canCheckForUpdates, + builder: (context, snapshot) { + final show = + PlatformUtils.isDesktop || + (snapshot.connectionState == ConnectionState.done && + snapshot.data == true); + if (!show) return const SizedBox.shrink(); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DividerSpace(), + AppTile( + label: 'check_for_updates'.i18n, + icon: AppImagePaths.update, + onPressed: () async => await settingMenuTap( + _SettingType.checkForUpdates, + ), ), - ), - ], - ); - }, - ), - DividerSpace(), - AppTile( - label: 'get_30_days_of_pro_free'.i18n, - icon: AppImagePaths.star, - onPressed: () => settingMenuTap(_SettingType.getPro), - ), + ], + ); + }, + ), + if (AppBuildInfo.enableSocialLinks) ...[ + DividerSpace(), + AppTile( + label: 'get_30_days_of_pro_free'.i18n, + icon: AppImagePaths.star, + onPressed: () => settingMenuTap(_SettingType.getPro), + ), + ], if (isStoreVersion() && !isUserPro) ...[ DividerSpace(), AppTile( @@ -233,35 +236,37 @@ class _SettingState extends ConsumerState ), ), }, - const SizedBox(height: defaultSize), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - 'lantern_projects'.i18n, - style: textTheme.labelLarge!.copyWith( - color: context.textSecondary, + if (AppBuildInfo.enableSocialLinks) ...[ + const SizedBox(height: defaultSize), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + 'lantern_projects'.i18n, + style: textTheme.labelLarge!.copyWith( + color: context.textSecondary, + ), ), ), - ), - const SizedBox(height: 4), - Card( - child: AppTile( - minHeight: 72, - icon: AppImagePaths.lanternLogoRounded, - iconUseThemeColor: false, - trailing: AppImage(path: AppImagePaths.outsideBrowser), - label: 'unbounded'.i18n, - subtitle: Text( - 'help_fight_global_internet_censorship'.i18n, - style: textTheme.labelMedium!.copyWith( - color: context.textTertiary, + const SizedBox(height: 4), + Card( + child: AppTile( + minHeight: 72, + icon: AppImagePaths.lanternLogoRounded, + iconUseThemeColor: false, + trailing: AppImage(path: AppImagePaths.outsideBrowser), + label: 'unbounded'.i18n, + subtitle: Text( + 'help_fight_global_internet_censorship'.i18n, + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), ), + onPressed: () { + UrlUtils.openUrl(AppUrls.unbounded); + }, ), - onPressed: () { - UrlUtils.openUrl(AppUrls.unbounded); - }, ), - ), + ], SizedBox(height: defaultSize), ], ), diff --git a/lib/features/support/support.dart b/lib/features/support/support.dart index 39c210bcb8..9bef548b4a 100644 --- a/lib/features/support/support.dart +++ b/lib/features/support/support.dart @@ -40,7 +40,8 @@ class Support extends StatelessWidget { onPressed: () => safePush(context, ReportIssue()), ), const DividerSpace( - padding: EdgeInsets.symmetric(horizontal: 16)), + padding: EdgeInsets.symmetric(horizontal: 16), + ), AppTile( icon: Icons.code_outlined, label: 'diagnostic_logs'.i18n, @@ -54,15 +55,18 @@ class Support extends StatelessWidget { padding: EdgeInsets.zero, child: Column( children: [ - AppTile.link( - icon: Icons.forum_outlined, - label: 'lantern_user_forum'.i18n, - url: AppUrls.lanternForums, - open: (u) => - UrlUtils.tryLaunchExternalUrl(context, Uri.parse(u)), - ), - const DividerSpace( - padding: EdgeInsets.symmetric(horizontal: 16)), + if (AppBuildInfo.enableSocialLinks) ...[ + AppTile.link( + icon: Icons.forum_outlined, + label: 'lantern_user_forum'.i18n, + url: AppUrls.lanternForums, + open: (u) => + UrlUtils.tryLaunchExternalUrl(context, Uri.parse(u)), + ), + const DividerSpace( + padding: EdgeInsets.symmetric(horizontal: 16), + ), + ], AppTile.link( icon: Icons.info_outlined, label: 'frequently_asked_questions'.i18n, @@ -71,7 +75,8 @@ class Support extends StatelessWidget { UrlUtils.tryLaunchExternalUrl(context, Uri.parse(u)), ), const DividerSpace( - padding: EdgeInsets.symmetric(horizontal: 16)), + padding: EdgeInsets.symmetric(horizontal: 16), + ), AppTile.link( icon: Icons.privacy_tip_outlined, label: 'privacy_policy'.i18n, @@ -80,7 +85,8 @@ class Support extends StatelessWidget { UrlUtils.tryLaunchExternalUrl(context, Uri.parse(u)), ), const DividerSpace( - padding: EdgeInsets.symmetric(horizontal: 16)), + padding: EdgeInsets.symmetric(horizontal: 16), + ), AppTile.link( icon: Icons.description_outlined, label: 'terms_of_service'.i18n, @@ -91,34 +97,36 @@ class Support extends StatelessWidget { ], ), ), - const SizedBox(height: defaultSize), - AppCard( - padding: EdgeInsets.zero, - child: Column( - children: [ - DividerSpace(), - AppTile( - label: 'download_links'.i18n, - icon: AppImagePaths.desktop, - onPressed: () { - appRouter.push(DownloadLinks()); - }, - ), - DividerSpace(), - AppTile( - label: 'follow_us'.i18n, - icon: AppImagePaths.thumb, - onPressed: () { - if (PlatformUtils.isDesktop) { - appRouter.push(FollowUs()); - return; - } - showFollowUsBottomSheet(context: context); - }, - ), - ], + if (AppBuildInfo.enableSocialLinks) ...[ + const SizedBox(height: defaultSize), + AppCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + DividerSpace(), + AppTile( + label: 'download_links'.i18n, + icon: AppImagePaths.desktop, + onPressed: () { + appRouter.push(DownloadLinks()); + }, + ), + DividerSpace(), + AppTile( + label: 'follow_us'.i18n, + icon: AppImagePaths.thumb, + onPressed: () { + if (PlatformUtils.isDesktop) { + appRouter.push(FollowUs()); + return; + } + showFollowUsBottomSheet(context: context); + }, + ), + ], + ), ), - ), + ], const SizedBox(height: defaultSize), const AppVersion(), const SizedBox(height: size24), @@ -132,13 +140,12 @@ class Support extends StatelessWidget { showAppBottomSheet( context: context, title: 'follow_us'.i18n, - scrollControlDisabledMaxHeightRatio: - context.isSmallDevice ? 0.39.h : 0.3.h, + scrollControlDisabledMaxHeightRatio: context.isSmallDevice + ? 0.39.h + : 0.3.h, builder: (context, scrollController) { return Flexible( - child: FollowUsListView( - scrollController: scrollController, - ), + child: FollowUsListView(scrollController: scrollController), ); }, ); diff --git a/lib/lantern_app.dart b/lib/lantern_app.dart index 1d43051484..57210c2324 100644 --- a/lib/lantern_app.dart +++ b/lib/lantern_app.dart @@ -42,7 +42,9 @@ class _LanternAppState extends ConsumerState void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - initDeepLinks(); + if (AppBuildInfo.enableAppLinks) { + initDeepLinks(); + } initLifecycleListener(); } @@ -76,11 +78,13 @@ class _LanternAppState extends ConsumerState } Future initDeepLinks() async { + if (!AppBuildInfo.enableAppLinks) return; final appLinks = AppLinks(); _deepLinkSubscription = appLinks.uriLinkStream.listen(_handleDeepLinkUri); } void _handleDeepLinkUri(Uri uri) { + if (!AppBuildInfo.enableAppLinks) return; if (!context.mounted) return; final safeLogUri = uri.replace(query: '').toString(); @@ -119,8 +123,9 @@ class _LanternAppState extends ConsumerState } else { _pushWithHome(ReportIssue()); } - } else if (path.startsWith('/auth') || - (uri.scheme == 'lantern' && uri.host == 'auth')) { + } else if (AppBuildInfo.enableOAuth && + (path.startsWith('/auth') || + (uri.scheme == 'lantern' && uri.host == 'auth'))) { if (uri.queryParameters.containsKey('token')) { sl().handleDeepLink(uri.queryParameters); } diff --git a/lib/main.dart b/lib/main.dart index da502724ea..3e5b351826 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -55,9 +55,9 @@ Future main() async { // (registered at injection_container.dart:40) may not be in the registry, // and the synchronous lookup would throw and prevent runApp. try { - if (sl.isRegistered()) { + if (AppBuildInfo.enableAutoUpdate && sl.isRegistered()) { unawaited(sl().init()); - } else { + } else if (AppBuildInfo.enableAutoUpdate) { appLogger.warning('Updater not registered, skipping init'); } } catch (e, st) {