feat: add error_handler and email verification request

This commit is contained in:
Patryk Hegenberg 2026-01-21 12:24:52 +01:00
parent defecf958a
commit cdc5e44bb3
8 changed files with 110 additions and 26 deletions

View file

@ -383,5 +383,13 @@
"lobbyStatusEntering": "Betrete das Schlachtfeld...", "lobbyStatusEntering": "Betrete das Schlachtfeld...",
"connectivityError": "Keine Internetverbindung verfügbar.", "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."
} }

View file

@ -397,5 +397,13 @@
"lobbyStatusEntering": "Entering Battle...", "lobbyStatusEntering": "Entering Battle...",
"connectivityError": "No internet connection available.", "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."
} }

View file

@ -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(),
),
),
);
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:slrpg_app/l10n/app_localizations.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 '../../../../shared/data/repositories/user_repository.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
@ -60,6 +61,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
_isLoading = false; _isLoading = false;
_errorMessage = _parseErrorMessage(e.toString()); _errorMessage = _parseErrorMessage(e.toString());
}); });
ErrorHandler.showErrorSnackBar(context, e);
} }
} }
} }

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:slrpg_app/l10n/app_localizations.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 'package:slrpg_app/src/features/multiplayer/data/repositories/party_repository.dart';
import '../../../../core/constants/app_constants.dart'; import '../../../../core/constants/app_constants.dart';
@ -159,9 +160,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
} catch (e) { } catch (e) {
log('Failed to start workout: $e'); log('Failed to start workout: $e');
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( if (mounted) ErrorHandler.showErrorSnackBar(context, e);
SnackBar(content: Text('Error: $e')),
);
} }
} }
} }
@ -276,8 +275,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
if (mounted) context.go('/lobby/${party.id}'); if (mounted) context.go('/lobby/${party.id}');
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) if (mounted) ErrorHandler.showErrorSnackBar(context, e);
.showSnackBar(SnackBar(content: Text('Error: $e')));
} }
} }
}, },
@ -321,8 +319,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
if (mounted) context.go('/lobby/${party.id}'); if (mounted) context.go('/lobby/${party.id}');
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) if (mounted) ErrorHandler.showErrorSnackBar(context, e);
.showSnackBar(SnackBar(content: Text('Error: $e')));
} }
} }
} }
@ -497,21 +494,21 @@ class _HubScreenState extends ConsumerState<HubScreen> {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (cycle != null) // if (cycle != null)
Padding( // Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), // padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row( // child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ // children: [
_StatBox( // _StatBox(
label: l10n.hubCycleLabel, // label: l10n.hubCycleLabel,
value: '#${cycle.cycleNumber}'), // value: '#${cycle.cycleNumber}'),
_StatBox( // _StatBox(
label: l10n.hubActiveLabel, // label: l10n.hubActiveLabel,
value: l10n.hubActiveYes), // value: l10n.hubActiveYes),
], // ],
), // ),
), // ),
const Spacer(flex: 1), const Spacer(flex: 1),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(

View file

@ -62,6 +62,7 @@ class _AvatarSetupScreenState extends ConsumerState<AvatarSetupScreen> {
); );
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
user = await userRepo.getLocalUser(); user = await userRepo.getLocalUser();
await ref.read(apiClientProvider).requestVerification(email);
if (user == null) { if (user == null) {
throw Exception( throw Exception(

View file

@ -169,11 +169,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
if (!mounted) return; if (!mounted) return;
_showCompletionDialog(result.xpEarned);
if (result.hasLevelUp) { if (result.hasLevelUp) {
_showLevelUpDialog(result.oldLevel!, result.newLevel!); _showLevelUpDialog(result.oldLevel!, result.newLevel!);
} }
_showCompletionDialog(result.xpEarned);
} }
void _showLevelUpDialog(int oldLevel, int newLevel) { void _showLevelUpDialog(int oldLevel, int newLevel) {

View file

@ -236,6 +236,19 @@ class ApiClient {
} }
} }
Future<void> 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<void> logout() async { Future<void> logout() async {
await _storage.delete(key: AppConstants.keyAuthToken); await _storage.delete(key: AppConstants.keyAuthToken);
await _storage.delete(key: AppConstants.keyUserId); await _storage.delete(key: AppConstants.keyUserId);