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...",
"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...",
"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: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<LoginScreen> {
_isLoading = false;
_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: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<HubScreen> {
} 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<HubScreen> {
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<HubScreen> {
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<HubScreen> {
),
),
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(

View file

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

View file

@ -169,11 +169,10 @@ class _BattleScreenState extends ConsumerState<BattleScreen> {
if (!mounted) return;
_showCompletionDialog(result.xpEarned);
if (result.hasLevelUp) {
_showLevelUpDialog(result.oldLevel!, result.newLevel!);
}
_showCompletionDialog(result.xpEarned);
}
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 {
await _storage.delete(key: AppConstants.keyAuthToken);
await _storage.delete(key: AppConstants.keyUserId);