Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Comment on lines +1527 to +1537

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new VPN error message keys (err_check_connection, err_service_unavailable, err_connection_failed, err_ruleset_failed) are only added to en.po. If the app locale is set to something else, i18n lookups for these keys may fall back to showing the raw msgid (e.g., "err_check_connection") to users. Please add these msgids to the other locale .po files (even as English placeholders) or implement a fallback to English for missing keys.

Copilot uses AI. Check for mistakes.
59 changes: 55 additions & 4 deletions lib/core/extensions/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions lib/core/services/app_purchase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,15 @@ 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);

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);
Expand Down
2 changes: 1 addition & 1 deletion lib/features/account/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/account/delete_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class _DeleteAccountState extends ConsumerState<DeleteAccount> {
result.fold(
(failure) {
appLogger
.error('Account deletion failed: ${failure.localizedErrorMessage}');
.error('Account deletion failed: ${failure.error}');
context.hideLoadingDialog();
context.showSnackBarError(failure.localizedErrorMessage);
},
Expand Down
5 changes: 2 additions & 3 deletions lib/features/auth/choose_payment_method.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -322,9 +323,7 @@ class ChoosePaymentMethod extends HookConsumerWidget {
await result.fold<Future<void>>(
(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 {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/auth/create_password.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
2 changes: 1 addition & 1 deletion lib/features/home/provider/feature_flag_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions lib/features/home/provider/home_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand All @@ -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) {
Expand All @@ -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);
},
Expand Down
2 changes: 1 addition & 1 deletion lib/features/language/language.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion lib/features/macos_extension/macos_extension_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class _MacOSExtensionDialogState extends ConsumerState<MacOSExtensionDialog> {

result.fold(
(failure) {
appLogger.error("Failure: ${failure.localizedErrorMessage}");
appLogger.error("Failure: ${failure.error}");
AppDialog.errorDialog(
context: context,
title: 'error'.i18n,
Expand Down
1 change: 0 additions & 1 deletion lib/features/plans/provider/plans_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ class PlansNotifier extends _$PlansNotifier {
}

void setSelectedPlan(Plan plan) {
appLogger.info('[PlansNotifier] setSelectedPlan: ${plan.id}');
userSelectedPlan = plan;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/features/private_server/manually_server_setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class _ManuallyServerSetupState extends ConsumerState<ManuallyServerSetup> {
result.fold(
(failure) {
appLogger
.error("Failed to add server: ${failure.localizedErrorMessage}");
.error("Failed to add server: ${failure.error}");
context.hideLoadingDialog();
context.showSnackBar(failure.localizedErrorMessage);
},
Expand Down
2 changes: 1 addition & 1 deletion lib/features/private_server/private_server_deploy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class _PrivateServerDeployState extends ConsumerState<PrivateServerDeploy> {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}');
Expand Down
4 changes: 2 additions & 2 deletions lib/features/vpn/provider/available_servers_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/vpn/server_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class _ServerSelectionState extends ConsumerState<ServerSelection> {
retryResult.fold((failure) {
context.showSnackBar(failure.localizedErrorMessage);
appLogger.error(
"Error changing VPN state: ${failure.localizedErrorMessage}",
"Error changing VPN state: ${failure.error}",
);
}, (_) => appRouter.popUntilRoot());
},
Expand Down
4 changes: 2 additions & 2 deletions lib/features/vpn/vpn_switch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand All @@ -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,
Expand Down
23 changes: 20 additions & 3 deletions lib/lantern/lantern_ffi_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 0 additions & 46 deletions test/core/models/app_setting_auth_session_test.dart

This file was deleted.

Loading