From cdc5e44bb39c786f06e4e9a609d59616432b97f8 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 21 Jan 2026 12:24:52 +0100 Subject: [PATCH] feat: add error_handler and email verification request --- lib/l10n/app_de.arb | 10 +++- lib/l10n/app_en.arb | 10 +++- lib/src/core/utils/error_handler.dart | 56 +++++++++++++++++++ .../presentation/screens/login_screen.dart | 2 + .../presentation/screens/hub_screen.dart | 41 +++++++------- .../screens/avatar_setup_screen.dart | 1 + .../presentation/screens/battle_screen.dart | 3 +- lib/src/shared/data/remote/api_client.dart | 13 +++++ 8 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 lib/src/core/utils/error_handler.dart diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 39d31f8..0fa31f7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -383,5 +383,13 @@ "lobbyStatusEntering": "Betrete das Schlachtfeld...", "connectivityError": "Keine Internetverbindung verfügbar.", - "connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt." + "connectivityMultiplayerError": "Für Multiplayer wird eine Internetverbindung benötigt.", + + "errorNoInternet": "Keine Internetverbindung", + "errorGeneric": "Etwas ist schiefgelaufen.", + "errorUnauthorized": "Zugriff verweigert. Bitte neu einloggen.", + "errorNotFound": "Daten konnten nicht gefunden werden.", + "errorEntryNotUnique": "Dieser Eintrag ist bereits vergeben.", + "errorAuthenticationFailed": "E-Mail oder Passwort falsch.", + "errorIllegalRequest": "Ungültige Anfrage." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f9134ff..70e3548 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -397,5 +397,13 @@ "lobbyStatusEntering": "Entering Battle...", "connectivityError": "No internet connection available.", - "connectivityMultiplayerError": "Active internet connection required for multiplayer." + "connectivityMultiplayerError": "Active internet connection required for multiplayer.", + + "errorNoInternet": "No internet connection", + "errorGeneric": "Something went wrong", + "errorUnauthorized": "Access denied. Please relogin.", + "errorNotFound": "Data not found.", + "errorEntryNotUnique": "Entry already exists.", + "errorAuthenticationFailed": "E-Mail or Passwort wrong.", + "errorIllegalRequest": "Illegal Request." } diff --git a/lib/src/core/utils/error_handler.dart b/lib/src/core/utils/error_handler.dart new file mode 100644 index 0000000..08b27e7 --- /dev/null +++ b/lib/src/core/utils/error_handler.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:slrpg_app/l10n/app_localizations.dart'; +import 'package:slrpg_app/src/core/theme/app_theme.dart'; + +class ErrorHandler { + static String getReadableError(BuildContext context, Object error) { + final l10n = AppLocalizations.of(context); + if (l10n == null) return error.toString(); + + final e = error.toString(); + + if (e.contains('SocketException') || + e.contains('Connection refused') || + e.contains('ClientException') || + e.contains('HandshakeException')) { + return l10n.errorNoInternet; + } + + if (e.contains('401') || e.contains('403')) { + return l10n.errorUnauthorized; + } + + if (e.contains('404')) { + return l10n.errorNotFound; + } + + if (e.contains('400')) { + if (e.contains('validation_not_unique')) { + return l10n.errorEntryNotUnique; + } + if (e.contains('Failed to authenticate')) { + return l10n.errorAuthenticationFailed; + } + return l10n.errorIllegalRequest; + } + + return l10n.errorGeneric; + } + + static void showErrorSnackBar(BuildContext context, Object error) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(getReadableError(context, error)), + backgroundColor: AppTheme.errorColor, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 4), + action: SnackBarAction( + label: 'OK', + textColor: Colors.white, + onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), + ), + ), + ); + } +} diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart index 5e8b772..634fe5a 100644 --- a/lib/src/features/authentication/presentation/screens/login_screen.dart +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:slrpg_app/l10n/app_localizations.dart'; +import 'package:slrpg_app/src/core/utils/error_handler.dart'; import '../../../../shared/data/repositories/user_repository.dart'; import '../../../../core/theme/app_theme.dart'; @@ -60,6 +61,7 @@ class _LoginScreenState extends ConsumerState { _isLoading = false; _errorMessage = _parseErrorMessage(e.toString()); }); + ErrorHandler.showErrorSnackBar(context, e); } } } diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index dca520c..8150536 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:slrpg_app/l10n/app_localizations.dart'; +import 'package:slrpg_app/src/core/utils/error_handler.dart'; import 'package:slrpg_app/src/features/multiplayer/data/repositories/party_repository.dart'; import '../../../../core/constants/app_constants.dart'; @@ -159,9 +160,7 @@ class _HubScreenState extends ConsumerState { } catch (e) { log('Failed to start workout: $e'); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error: $e')), - ); + if (mounted) ErrorHandler.showErrorSnackBar(context, e); } } } @@ -276,8 +275,7 @@ class _HubScreenState extends ConsumerState { if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error: $e'))); + if (mounted) ErrorHandler.showErrorSnackBar(context, e); } } }, @@ -321,8 +319,7 @@ class _HubScreenState extends ConsumerState { if (mounted) context.go('/lobby/${party.id}'); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Error: $e'))); + if (mounted) ErrorHandler.showErrorSnackBar(context, e); } } } @@ -497,21 +494,21 @@ class _HubScreenState extends ConsumerState { ), ), const SizedBox(height: 24), - if (cycle != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _StatBox( - label: l10n.hubCycleLabel, - value: '#${cycle.cycleNumber}'), - _StatBox( - label: l10n.hubActiveLabel, - value: l10n.hubActiveYes), - ], - ), - ), + // if (cycle != null) + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 32), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // children: [ + // _StatBox( + // label: l10n.hubCycleLabel, + // value: '#${cycle.cycleNumber}'), + // _StatBox( + // label: l10n.hubActiveLabel, + // value: l10n.hubActiveYes), + // ], + // ), + // ), const Spacer(flex: 1), Container( padding: const EdgeInsets.symmetric( diff --git a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart index 52946b2..4bfb933 100644 --- a/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart +++ b/lib/src/features/onboarding/presentation/screens/avatar_setup_screen.dart @@ -62,6 +62,7 @@ class _AvatarSetupScreenState extends ConsumerState { ); await Future.delayed(const Duration(milliseconds: 100)); user = await userRepo.getLocalUser(); + await ref.read(apiClientProvider).requestVerification(email); if (user == null) { throw Exception( diff --git a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart index 5973233..099a94d 100644 --- a/lib/src/features/workout_runner/presentation/screens/battle_screen.dart +++ b/lib/src/features/workout_runner/presentation/screens/battle_screen.dart @@ -169,11 +169,10 @@ class _BattleScreenState extends ConsumerState { if (!mounted) return; + _showCompletionDialog(result.xpEarned); if (result.hasLevelUp) { _showLevelUpDialog(result.oldLevel!, result.newLevel!); } - - _showCompletionDialog(result.xpEarned); } void _showLevelUpDialog(int oldLevel, int newLevel) { diff --git a/lib/src/shared/data/remote/api_client.dart b/lib/src/shared/data/remote/api_client.dart index 8d6c705..34c0f7b 100644 --- a/lib/src/shared/data/remote/api_client.dart +++ b/lib/src/shared/data/remote/api_client.dart @@ -236,6 +236,19 @@ class ApiClient { } } + Future requestVerification(String email) async { + try { + await _dio.post( + '/api/collections/users/request-verification', + data: {'email': email}, + ); + _logger.i('Verification email requested for $email'); + } catch (e) { + _logger.e('Request verification failed', error: e); + rethrow; + } + } + Future logout() async { await _storage.delete(key: AppConstants.keyAuthToken); await _storage.delete(key: AppConstants.keyUserId);