diff --git a/assets/locales/en.po b/assets/locales/en.po index e668600711..edacdb9175 100644 --- a/assets/locales/en.po +++ b/assets/locales/en.po @@ -1523,3 +1523,15 @@ msgstr "Got it" msgid "vpn_conflict_connect_anyway" msgstr "Connect Anyway" + +msgid "err_check_connection" +msgstr "Unable to connect. Check your internet connection." + +msgid "err_service_unavailable" +msgstr "Service temporarily unavailable. Trying again..." + +msgid "err_connection_failed" +msgstr "Connection failed. Please try again." + +msgid "err_ruleset_failed" +msgstr "Unable to load routing configuration. Retrying..." diff --git a/lib/core/extensions/error.dart b/lib/core/extensions/error.dart index c3024a0f45..ba40f2ce3f 100644 --- a/lib/core/extensions/error.dart +++ b/lib/core/extensions/error.dart @@ -78,18 +78,69 @@ extension ErrorExetension on Object { if (description.contains('Cannot use your own code for promotion')) { return "referral_code_own_invalid".i18n; } - return description; + + final categoryKey = _classifyVpnError(description); + if (categoryKey != null) return categoryKey.i18n; + + return "an_error_occurred".i18n; } if (this is StateError) { - return (this as StateError).message; + final categoryKey = _classifyVpnError((this as StateError).message); + if (categoryKey != null) return categoryKey.i18n; + return "an_error_occurred".i18n; } if (this is Exception) { - return (this as Exception).toString(); + final categoryKey = _classifyVpnError((this as Exception).toString()); + if (categoryKey != null) return categoryKey.i18n; + return "an_error_occurred".i18n; } - return "error_occurred".i18n; + return "an_error_occurred".i18n; + } +} + +/// Classifies VPN-related errors into user-friendly +/// categories based on regex patterns. +final List<(RegExp, String)> _vpnErrorPatterns = [ + ( + RegExp( + r'no such host|dns|network is unreachable|i/o timeout|no route to host|connection refused', + caseSensitive: false, + ), + 'err_check_connection', + ), + ( + RegExp(r'\b503\b|service unavailable', caseSensitive: false), + 'err_service_unavailable', + ), + ( + RegExp(r'ruleset|geosite|geoip|smart routing', caseSensitive: false), + 'err_ruleset_failed', + ), + ( + RegExp( + r'tunnel|tun device|setup failed|failed to start vpn|libbox', + caseSensitive: false, + ), + 'err_connection_failed', + ), +]; + +String? _classifyVpnError(String description) { + if (description.isEmpty) return null; + for (final (pattern, key) in _vpnErrorPatterns) { + if (pattern.hasMatch(description)) return key; } + return null; +} + +/// Returns a localized user-facing message for a raw error string. Use this +/// at boundaries where errors arrive as plain strings (e.g. FFI results) +/// rather than as `Exception` instances, instead of wrapping them in +/// `Exception(...)` just to route through `localizedDescription`. +String localizeRawError(String rawError) { + return (_classifyVpnError(rawError) ?? 'an_error_occurred').i18n; } /// Strips the radiance IPC prefix from error messages. diff --git a/lib/core/services/app_purchase.dart b/lib/core/services/app_purchase.dart index 0f01fd3727..69ca1e4178 100644 --- a/lib/core/services/app_purchase.dart +++ b/lib/core/services/app_purchase.dart @@ -309,7 +309,7 @@ class AppPurchase { final fetchResult = await lanternService.fetchUserData(); final fetchedUser = fetchResult.fold((failure) { appLogger.warning( - '[AppPurchase] Failed to fetch latest user data for purchase check: ${failure.localizedErrorMessage}', + '[AppPurchase] Failed to fetch latest user data for purchase check: ${failure.error}', ); return null; }, (user) => user); @@ -317,7 +317,7 @@ class AppPurchase { final user = fetchedUser ?? (await lanternService.getUserData()).fold((failure) { appLogger.warning( - '[AppPurchase] Failed to load cached user data for purchase check: ${failure.localizedErrorMessage}', + '[AppPurchase] Failed to load cached user data for purchase check: ${failure.error}', ); return null; }, (user) => user); diff --git a/lib/features/account/account.dart b/lib/features/account/account.dart index 4868fc1d6e..458d3a2e7d 100644 --- a/lib/features/account/account.dart +++ b/lib/features/account/account.dart @@ -437,7 +437,7 @@ class Account extends HookConsumerWidget { result.fold( (l) { context.hideLoadingDialog(); - appLogger.error('Logout error: ${l.localizedErrorMessage}'); + appLogger.error('Logout error: ${l.error}'); context.showSnackBar(l.localizedErrorMessage); }, (user) { diff --git a/lib/features/account/delete_account.dart b/lib/features/account/delete_account.dart index 7ec12ace12..f077197748 100644 --- a/lib/features/account/delete_account.dart +++ b/lib/features/account/delete_account.dart @@ -171,7 +171,7 @@ class _DeleteAccountState extends ConsumerState { result.fold( (failure) { appLogger - .error('Account deletion failed: ${failure.localizedErrorMessage}'); + .error('Account deletion failed: ${failure.error}'); context.hideLoadingDialog(); context.showSnackBarError(failure.localizedErrorMessage); }, diff --git a/lib/features/auth/choose_payment_method.dart b/lib/features/auth/choose_payment_method.dart index ab79b877e5..935f96379f 100644 --- a/lib/features/auth/choose_payment_method.dart +++ b/lib/features/auth/choose_payment_method.dart @@ -215,6 +215,7 @@ class ChoosePaymentMethod extends HookConsumerWidget { }, onError: (error) { finishPaymentRedirect(paymentRedirectInFlight); + ///error while subscribing appLogger.error('Error subscribing to plan: $error'); if (error is StripeException) { @@ -322,9 +323,7 @@ class ChoosePaymentMethod extends HookConsumerWidget { await result.fold>( (failure) async { context.hideLoadingDialog(); - appLogger.error( - 'Error redirecting to payment: ${failure.localizedErrorMessage}', - ); + appLogger.error('Error redirecting to payment: ${failure.error}'); context.showSnackBar(failure.localizedErrorMessage); }, (url) async { diff --git a/lib/features/auth/create_password.dart b/lib/features/auth/create_password.dart index 900faed08b..17b1debc23 100644 --- a/lib/features/auth/create_password.dart +++ b/lib/features/auth/create_password.dart @@ -104,7 +104,7 @@ class CreatePassword extends HookConsumerWidget { (failure) { context.hideLoadingDialog(); appLogger.error( - 'Failed to create password: ${failure.localizedErrorMessage}', + 'Failed to create password: ${failure.error}', ); context.showSnackBarError(failure.localizedErrorMessage); }, diff --git a/lib/features/home/provider/feature_flag_notifier.dart b/lib/features/home/provider/feature_flag_notifier.dart index 4e14a46783..4ad098232e 100644 --- a/lib/features/home/provider/feature_flag_notifier.dart +++ b/lib/features/home/provider/feature_flag_notifier.dart @@ -20,7 +20,7 @@ class FeatureFlagNotifier extends _$FeatureFlagNotifier { result.fold( (failure) { appLogger.error( - 'Error fetching feature flags: ${failure.localizedErrorMessage}'); + 'Error fetching feature flags: ${failure.error}'); }, (flags) { try { diff --git a/lib/features/home/provider/home_notifier.dart b/lib/features/home/provider/home_notifier.dart index 4461da9881..af7a814f86 100644 --- a/lib/features/home/provider/home_notifier.dart +++ b/lib/features/home/provider/home_notifier.dart @@ -20,7 +20,7 @@ class HomeNotifier extends _$HomeNotifier { return result.fold( (failure) { appLogger.error( - 'Error getting user data: ${failure.localizedErrorMessage}', + 'Error getting user data: ${failure.error}', ); throw Exception('Failed to get user data'); }, @@ -38,7 +38,7 @@ class HomeNotifier extends _$HomeNotifier { result.fold( (failure) { appLogger.error( - 'Error fetching user data: ${failure.localizedErrorMessage}', + 'Error fetching user data: ${failure.error}', ); }, (userData) { @@ -56,7 +56,7 @@ class HomeNotifier extends _$HomeNotifier { result.fold( (failure) { appLogger.error( - 'Error refreshing user data: ${failure.localizedErrorMessage}', + 'Error refreshing user data: ${failure.error}', ); state = AsyncValue.error(failure, StackTrace.current); }, diff --git a/lib/features/language/language.dart b/lib/features/language/language.dart index dc8bdcfb0f..f52f6942bb 100644 --- a/lib/features/language/language.dart +++ b/lib/features/language/language.dart @@ -104,7 +104,7 @@ class LanguageListView extends HookConsumerWidget { either.fold( (failure) { appLogger - .error('Error updating locale: ${failure.localizedErrorMessage}'); + .error('Error updating locale: ${failure.error}'); }, (r) { appLogger.debug('Locale updated to: $newLocale'); diff --git a/lib/features/macos_extension/macos_extension_dialog.dart b/lib/features/macos_extension/macos_extension_dialog.dart index 4fe481fabd..865cc227ec 100644 --- a/lib/features/macos_extension/macos_extension_dialog.dart +++ b/lib/features/macos_extension/macos_extension_dialog.dart @@ -141,7 +141,7 @@ class _MacOSExtensionDialogState extends ConsumerState { result.fold( (failure) { - appLogger.error("Failure: ${failure.localizedErrorMessage}"); + appLogger.error("Failure: ${failure.error}"); AppDialog.errorDialog( context: context, title: 'error'.i18n, diff --git a/lib/features/plans/provider/plans_notifier.dart b/lib/features/plans/provider/plans_notifier.dart index 44d8701ac3..c341bab680 100644 --- a/lib/features/plans/provider/plans_notifier.dart +++ b/lib/features/plans/provider/plans_notifier.dart @@ -93,7 +93,6 @@ class PlansNotifier extends _$PlansNotifier { } void setSelectedPlan(Plan plan) { - appLogger.info('[PlansNotifier] setSelectedPlan: ${plan.id}'); userSelectedPlan = plan; } diff --git a/lib/features/private_server/manually_server_setup.dart b/lib/features/private_server/manually_server_setup.dart index d7a5c9fff8..0a309b7ef8 100644 --- a/lib/features/private_server/manually_server_setup.dart +++ b/lib/features/private_server/manually_server_setup.dart @@ -181,7 +181,7 @@ class _ManuallyServerSetupState extends ConsumerState { result.fold( (failure) { appLogger - .error("Failed to add server: ${failure.localizedErrorMessage}"); + .error("Failed to add server: ${failure.error}"); context.hideLoadingDialog(); context.showSnackBar(failure.localizedErrorMessage); }, diff --git a/lib/features/private_server/private_server_deploy.dart b/lib/features/private_server/private_server_deploy.dart index 8f6fb6e5f8..0010f2384f 100644 --- a/lib/features/private_server/private_server_deploy.dart +++ b/lib/features/private_server/private_server_deploy.dart @@ -179,7 +179,7 @@ class _PrivateServerDeployState extends ConsumerState { context.hideLoadingDialog(); // Handle failure case, e.g., show an error message appLogger - .error("Failed to cancel deployment: ${l.localizedErrorMessage}"); + .error("Failed to cancel deployment: ${l.error}"); context.showSnackBar(l.localizedErrorMessage); }, (r) { diff --git a/lib/features/system_tray/provider/system_tray_notifier.dart b/lib/features/system_tray/provider/system_tray_notifier.dart index 06c6bcf0ea..7779e32b49 100644 --- a/lib/features/system_tray/provider/system_tray_notifier.dart +++ b/lib/features/system_tray/provider/system_tray_notifier.dart @@ -128,7 +128,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { .connectToServer(ServerLocationType.lanternLocation, server.tag); result.fold( (failure) => appLogger.error( - 'Failed to connect: ${failure.localizedErrorMessage}', + 'Failed to connect: ${failure.error}', ), (success) { appLogger.info('Connecting to ${server.location.country} - ${server.location.city}'); diff --git a/lib/features/vpn/provider/available_servers_notifier.dart b/lib/features/vpn/provider/available_servers_notifier.dart index fcfe42b396..1294c23466 100644 --- a/lib/features/vpn/provider/available_servers_notifier.dart +++ b/lib/features/vpn/provider/available_servers_notifier.dart @@ -16,7 +16,7 @@ class AvailableServersNotifier extends _$AvailableServersNotifier { return result.fold( (failure) { appLogger.error( - 'Error getting available servers: ${failure.localizedErrorMessage}', + 'Error getting available servers: ${failure.error}', ); throw Exception('Failed to load available servers'); }, @@ -40,7 +40,7 @@ class AvailableServersNotifier extends _$AvailableServersNotifier { result.fold( (failure) { appLogger.error( - 'Error getting available servers: ${failure.localizedErrorMessage}', + 'Error getting available servers: ${failure.error}', ); }, (servers) { diff --git a/lib/features/vpn/server_selection.dart b/lib/features/vpn/server_selection.dart index 8386c6129d..4c0480d5d2 100644 --- a/lib/features/vpn/server_selection.dart +++ b/lib/features/vpn/server_selection.dart @@ -239,7 +239,7 @@ class _ServerSelectionState extends ConsumerState { retryResult.fold((failure) { context.showSnackBar(failure.localizedErrorMessage); appLogger.error( - "Error changing VPN state: ${failure.localizedErrorMessage}", + "Error changing VPN state: ${failure.error}", ); }, (_) => appRouter.popUntilRoot()); }, diff --git a/lib/features/vpn/vpn_switch.dart b/lib/features/vpn/vpn_switch.dart index 057d6b0e30..bb5f1920c5 100644 --- a/lib/features/vpn/vpn_switch.dart +++ b/lib/features/vpn/vpn_switch.dart @@ -113,7 +113,7 @@ class VPNSwitch extends HookConsumerWidget { (failure) { context.showSnackBar(failure.localizedErrorMessage); appLogger.error( - "Error changing VPN state: ${failure.localizedErrorMessage}"); + "Error changing VPN state: ${failure.error}"); }, (_) => null, ); @@ -122,7 +122,7 @@ class VPNSwitch extends HookConsumerWidget { } else { context.showSnackBar(failure.localizedErrorMessage); appLogger.error( - "Error changing VPN state: ${failure.localizedErrorMessage}"); + "Error changing VPN state: ${failure.error}"); } }, (_) => null, diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index a533960699..cc114b4522 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -523,7 +523,12 @@ class LanternFFIService implements LanternCoreService { // Not JSON — fall through with the raw string. } appLogger.error('$action split tunnel error: $errMsg'); - return left(Failure(error: errMsg, localizedErrorMessage: errMsg)); + return left( + Failure( + error: errMsg, + localizedErrorMessage: localizeRawError(errMsg), + ), + ); } catch (e) { return left( Failure( @@ -589,7 +594,13 @@ class LanternFFIService implements LanternCoreService { } }); if (result.isNotEmpty && !_ffiOkResults.contains(result)) { - return left(Failure(error: result, localizedErrorMessage: result)); + appLogger.error('startVPN error: $result'); + return left( + Failure( + error: result, + localizedErrorMessage: localizeRawError(result), + ), + ); } appLogger.debug('startVPN result: $result'); return right(result.isEmpty ? 'ok' : result); @@ -670,7 +681,13 @@ class LanternFFIService implements LanternCoreService { } }); if (result.isNotEmpty && !_ffiOkResults.contains(result)) { - return left(Failure(error: result, localizedErrorMessage: result)); + appLogger.error('stopVPN error: $result'); + return left( + Failure( + error: result, + localizedErrorMessage: localizeRawError(result), + ), + ); } appLogger.debug('stopVPN result: $result'); return right(result.isEmpty ? 'ok' : result); diff --git a/test/core/models/app_setting_auth_session_test.dart b/test/core/models/app_setting_auth_session_test.dart deleted file mode 100644 index d95b55f007..0000000000 --- a/test/core/models/app_setting_auth_session_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:lantern/core/models/app_setting.dart'; - -void main() { - group('AppSetting.clearAuthSessionData', () { - test('clears auth/session fields by default', () { - const before = AppSetting( - isPro: true, - userLoggedIn: true, - oAuthToken: 'token-123', - oAuthLoginProvider: 'google', - email: 'user@example.com', - environment: 'stage', - locale: 'en_US', - ); - - final after = before.clearAuthSessionData(); - - expect(after.isPro, isFalse); - expect(after.userLoggedIn, isFalse); - expect(after.oAuthToken, isEmpty); - expect(after.oAuthLoginProvider, isEmpty); - expect(after.email, isEmpty); - expect(after.environment, equals('stage')); - expect(after.locale, equals('en_US')); - }); - - test('can preserve email when requested', () { - const before = AppSetting( - isPro: true, - userLoggedIn: true, - oAuthToken: 'token-123', - oAuthLoginProvider: 'apple', - email: 'user@example.com', - ); - - final after = before.clearAuthSessionData(clearEmail: false); - - expect(after.isPro, isFalse); - expect(after.userLoggedIn, isFalse); - expect(after.oAuthToken, isEmpty); - expect(after.oAuthLoginProvider, isEmpty); - expect(after.email, equals('user@example.com')); - }); - }); -}