fix: fix errors while refreshing token on app start

This commit is contained in:
Patryk Hegenberg 2026-01-22 15:47:01 +01:00
parent b58b7ca57a
commit 361c43f3c1
9 changed files with 280 additions and 90 deletions

View file

@ -1,24 +1,88 @@
import 'dart:developer'; // import 'dart:developer';
// import 'package:flutter/material.dart';
// import 'package:flutter/services.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// import 'package:slrpg_app/src/shared/data/remote/secure_auth_store.dart';
// import 'src/app.dart';
// import 'src/shared/data/local/app_database.dart';
// import 'src/shared/data/remote/api_client.dart';
// import 'src/shared/data/remote/pb_auth_store.dart';
// import 'package:flutter_dotenv/flutter_dotenv.dart';
// void main() async {
// WidgetsFlutterBinding.ensureInitialized();
// try {
// await dotenv.load(fileName: '.env');
// log('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
// log('API URL: ${dotenv.env['API_BASE_URL']}');
// } catch (e) {
// log('Could not load .env file: $e');
// log('Using default production values');
// }
// await SystemChrome.setPreferredOrientations([
// DeviceOrientation.portraitUp,
// DeviceOrientation.portraitDown,
// ]);
// final database = AppDatabase();
// const secureStorage = FlutterSecureStorage(
// aOptions: AndroidOptions(encryptedSharedPreferences: true));
// final authStore = PbAuthStore();
// // final authStore = SecureAuthStore(storage: secureStorage);
// await authStore.loadFromStorage();
// runApp(
// ProviderScope(
// overrides: [
// // Datenbank Override (wie gehabt)
// appDatabaseProvider.overrideWithValue(database),
// // ApiClient Override: Wir geben den BEREITS GELADENEN Store rein
// apiClientProvider.overrideWith((ref) => ApiClient(
// authStore: authStore, // Hier injizieren!
// storage: secureStorage)),
// ],
// child: const SLRPGApp(), // Dein Root Widget (Name prüfen, falls anders)
// ),
// );
// // }
// // runApp(
// // ProviderScope(
// // overrides: [
// // appDatabaseProvider.overrideWithValue(database),
// // apiClientProvider
// // .overrideWith((ref) => ApiClient(authStore: authStore)),
// // ],
// // child: const SLRPGApp(),
// // ),
// // );
// }
// final appDatabaseProvider =
// Provider<AppDatabase>((ref) => throw UnimplementedError());
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'src/app.dart';
import 'src/shared/data/local/app_database.dart';
import 'src/shared/data/remote/api_client.dart';
import 'src/shared/data/remote/pb_auth_store.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:slrpg_app/src/app.dart';
import 'package:slrpg_app/src/shared/data/local/app_database.dart';
import 'package:slrpg_app/src/shared/data/remote/api_client.dart';
import 'package:slrpg_app/src/shared/data/remote/pb_auth_store.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 1. Env laden
try { try {
await dotenv.load(fileName: '.env'); await dotenv.load(fileName: '.env');
log('Environment loaded: ${dotenv.env['ENVIRONMENT']}');
log('API URL: ${dotenv.env['API_BASE_URL']}');
} catch (e) { } catch (e) {
log('Could not load .env file: $e'); log('Could not load .env file: $e');
log('Using default production values');
} }
await SystemChrome.setPreferredOrientations([ await SystemChrome.setPreferredOrientations([
@ -28,18 +92,13 @@ void main() async {
final database = AppDatabase(); final database = AppDatabase();
// 2. Auth Store erstellen UND laden (Warten!)
final authStore = PbAuthStore(); final authStore = PbAuthStore();
await authStore.loadFromStorage(); await authStore.loadFromStorage(); // Das ist der entscheidende 'await'
if (authStore.isValid && authStore.record == null) { log("Auth loaded. Valid? ${authStore.isValid}"); // Debug Log
final tempClient = ApiClient(authStore: authStore);
try {
await tempClient.refreshAuth();
} catch (e) {
log('Initial auth refresh failed: $e');
}
}
// 3. App starten mit injiziertem Store
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [ overrides: [
@ -52,5 +111,6 @@ void main() async {
); );
} }
// Provider Definition für DB (falls noch nicht vorhanden)
final appDatabaseProvider = final appDatabaseProvider =
Provider<AppDatabase>((ref) => throw UnimplementedError()); Provider<AppDatabase>((ref) => throw UnimplementedError());

View file

@ -6,6 +6,7 @@ import 'package:slrpg_app/src/features/authentication/data/repositories/auth_rep
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/leaderboard_screen.dart'; import 'package:slrpg_app/src/features/multiplayer/presentation/screens/leaderboard_screen.dart';
import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_screen.dart'; import 'package:slrpg_app/src/features/multiplayer/presentation/screens/lobby_screen.dart';
import 'package:slrpg_app/src/features/settings/presentation/screens/privacy_policy_screen.dart'; import 'package:slrpg_app/src/features/settings/presentation/screens/privacy_policy_screen.dart';
import 'package:slrpg_app/src/shared/data/remote/api_client.dart';
import '../../features/authentication/presentation/screens/login_screen.dart'; import '../../features/authentication/presentation/screens/login_screen.dart';
import '../../features/authentication/presentation/screens/profile_screen.dart'; import '../../features/authentication/presentation/screens/profile_screen.dart';
@ -205,7 +206,20 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
} }
Future<void> _checkInitialRoute() async { Future<void> _checkInitialRoute() async {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
final apiClient = ref.read(apiClientProvider);
final authStore = apiClient.pb.authStore;
if (authStore.isValid && authStore.record == null) {
try {
await apiClient.refreshAuth();
} catch (e) {
// If refresh fails, user will be redirected to login by the router logic (authStore cleared)
}
}
if (!mounted) return; if (!mounted) return;

View file

@ -15,7 +15,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
apiClient.authStateChanges.listen((event) { apiClient.authStateChanges.listen((event) {
if (event.token.isEmpty) { if (event.token.isEmpty) {
repo.logout(); repo.clearLocalData();
} }
}); });
@ -115,10 +115,11 @@ class AuthRepository {
} }
Future<void> logout() async { Future<void> logout() async {
if (apiClient.getToken() != null) {
await apiClient.logout(); await apiClient.logout();
await clearLocalData();
} }
Future<void> clearLocalData() async {
await _storage.delete(key: AppConstants.keyLastSync); await _storage.delete(key: AppConstants.keyLastSync);
await db.transaction(() async { await db.transaction(() async {

View file

@ -465,7 +465,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const QuestBoardWidget(), // const QuestBoardWidget(),
const Spacer(flex: 2), const Spacer(flex: 2),
if (cycle != null) if (cycle != null)
Padding( Padding(

View file

@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pocketbase/pocketbase.dart'; import 'package:pocketbase/pocketbase.dart';
import 'package:slrpg_app/src/features/multiplayer/domain/entities/party.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/party.dart';
import 'package:slrpg_app/src/features/multiplayer/domain/entities/party_member.dart'; import 'package:slrpg_app/src/features/multiplayer/domain/entities/party_member.dart';
import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart';
import '../../../../shared/data/remote/api_client.dart'; import '../../../../shared/data/remote/api_client.dart';
final partyRepositoryProvider = Provider((ref) { final partyRepositoryProvider = Provider((ref) {
@ -63,21 +62,39 @@ class PartyRepository {
await _api.pb.collection('parties').update(partyId, body: body); await _api.pb.collection('parties').update(partyId, body: body);
} }
Stream<Party> subscribeToParty(String partyId) async* { Stream<Party> subscribeToParty(String partyId) {
yield await getPartyDetails(partyId); late StreamController<Party> controller;
UnsubscribeFunc? unsubscribe;
final controller = StreamController<Party>(); controller = StreamController<Party>(
onListen: () async {
try {
final initial = await getPartyDetails(partyId);
controller.add(initial);
} catch (e) {
controller.addError(e);
}
_api.pb.collection('parties').subscribe(partyId, (e) { unsubscribe =
await _api.pb.collection('parties').subscribe(partyId, (e) {
if (e.action == 'update' && e.record != null) { if (e.action == 'update' && e.record != null) {
controller.add(Party.fromJson(e.record!.toJson())); controller.add(Party.fromJson(e.record!.toJson()));
} }
}); });
},
onCancel: () async {
await unsubscribe?.call();
log('🔌 Unsubscribed from party $partyId');
},
);
yield* controller.stream; return controller.stream;
} }
Stream<List<PartyMember>> subscribeToMembers(String partyId) async* { Stream<List<PartyMember>> subscribeToMembers(String partyId) {
late StreamController<List<PartyMember>> controller;
UnsubscribeFunc? unsubscribe;
Future<List<PartyMember>> fetchMembers() async { Future<List<PartyMember>> fetchMembers() async {
final records = await _api.pb.collection('party_members').getFullList( final records = await _api.pb.collection('party_members').getFullList(
filter: 'party_id="$partyId"', filter: 'party_id="$partyId"',
@ -86,17 +103,29 @@ class PartyRepository {
return records.map((r) => PartyMember.fromRecord(r.toJson())).toList(); return records.map((r) => PartyMember.fromRecord(r.toJson())).toList();
} }
yield await fetchMembers(); controller = StreamController<List<PartyMember>>(
onListen: () async {
try {
controller.add(await fetchMembers());
} catch (e) {
controller.addError(e);
}
final controller = StreamController<List<PartyMember>>(); unsubscribe =
await _api.pb.collection('party_members').subscribe('*', (e) async {
_api.pb.collection('party_members').subscribe('*', (e) async { if (e.record != null &&
if (e.record != null && e.record!.getStringValue('party_id') == partyId) { e.record!.getStringValue('party_id') == partyId) {
controller.add(await fetchMembers()); controller.add(await fetchMembers());
} }
}); });
},
onCancel: () async {
await unsubscribe?.call();
log('🔌 Unsubscribed from party members $partyId');
},
);
yield* controller.stream; return controller.stream;
} }
Future<void> dealDamage(String partyId, int damage) async { Future<void> dealDamage(String partyId, int damage) async {

View file

@ -179,10 +179,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
builder: (context, snapshot) { builder: (context, snapshot) {
final currentUserId = snapshot.data?.serverId ?? ''; final currentUserId = snapshot.data?.serverId ?? '';
// NEU: RefreshIndicator für Pull-to-Refresh
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
// Erzwingt ein Neuladen der Daten
return ref.refresh(leaderboardProvider.future); return ref.refresh(leaderboardProvider.future);
}, },
child: ListView.builder( child: ListView.builder(
@ -276,8 +274,6 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
Widget _buildAvatarPreview(LeaderboardEntry entry) { Widget _buildAvatarPreview(LeaderboardEntry entry) {
if (entry.avatar != null && entry.avatar!.isNotEmpty) { if (entry.avatar != null && entry.avatar!.isNotEmpty) {
try { try {
// Hier prüfen, ob es ein JSON String oder eine Map ist, falls nötig.
// Da wir im Repository .toJson() aufrufen, ist es hier sicher eine Map.
final config = AvatarConfig.fromJson(entry.avatar!); final config = AvatarConfig.fromJson(entry.avatar!);
return SizedBox( return SizedBox(
@ -300,8 +296,6 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
} }
} }
// WICHTIG: .autoDispose sorgt dafür, dass die Daten neu geladen werden,
// wenn der Screen verlassen und wieder betreten wird.
final leaderboardProvider = FutureProvider.autoDispose((ref) async { final leaderboardProvider = FutureProvider.autoDispose((ref) async {
return ref.read(leaderboardRepositoryProvider).getGlobalLeaderboard(); return ref.read(leaderboardRepositoryProvider).getGlobalLeaderboard();
}); });

View file

@ -6,11 +6,13 @@ import 'package:pocketbase/pocketbase.dart';
import '../../../core/constants/app_constants.dart'; import '../../../core/constants/app_constants.dart';
import 'pb_auth_store.dart'; import 'pb_auth_store.dart';
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient()); // final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
final apiClientProvider =
Provider<ApiClient>((ref) => throw UnimplementedError());
class ApiClient { class ApiClient {
late final PocketBase _pb; late final PocketBase _pb;
final PbAuthStore _authStore; // final PbAuthStore _authStore;
final Logger _logger; final Logger _logger;
PocketBase get pb => _pb; PocketBase get pb => _pb;
@ -18,19 +20,28 @@ class ApiClient {
Stream<AuthStoreEvent> get authStateChanges => _pb.authStore.onChange; Stream<AuthStoreEvent> get authStateChanges => _pb.authStore.onChange;
ApiClient({ ApiClient({
PbAuthStore? authStore, required AuthStore authStore,
FlutterSecureStorage? storage,
Logger? logger, Logger? logger,
}) : _logger = logger ?? Logger(), }) : _logger = logger ?? Logger() {
_authStore = authStore ?? PbAuthStore(storage: storage) {
_pb = PocketBase( _pb = PocketBase(
AppConstants.apiBaseUrl, AppConstants.apiBaseUrl,
authStore: _authStore, authStore: authStore, // Hier kommt der geladene Store rein
); );
if (authStore == null) {
_authStore.loadFromStorage();
}
} }
// ApiClient({
// PbAuthStore? authStore,
// FlutterSecureStorage? storage,
// Logger? logger,
// }) : _logger = logger ?? Logger(),
// _authStore = authStore ?? PbAuthStore(storage: storage) {
// _pb = PocketBase(
// AppConstants.apiBaseUrl,
// authStore: _authStore,
// );
// if (authStore == null) {
// _authStore.loadFromStorage();
// }
// }
Future<T> _handleRequest<T>(Future<T> Function() request) async { Future<T> _handleRequest<T>(Future<T> Function() request) async {
try { try {

View file

@ -0,0 +1,20 @@
import 'dart:io';
class CustomHttpClient {
static HttpClient createWithTimeout({
Duration connectionTimeout = const Duration(seconds: 10),
Duration receiveTimeout = const Duration(seconds: 30),
}) {
final client = HttpClient();
client.connectionTimeout = connectionTimeout;
client.idleTimeout = const Duration(seconds: 15);
client.badCertificateCallback = (cert, host, port) {
return false;
};
return client;
}
}

View file

@ -1,57 +1,118 @@
// import 'dart:convert';
// import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// import 'package:pocketbase/pocketbase.dart';
// class PbAuthStore extends AuthStore {
// final FlutterSecureStorage _storage;
// final String _storageKey;
// PbAuthStore({
// FlutterSecureStorage? storage,
// String key = 'pb_auth',
// }) : _storage = storage ?? const FlutterSecureStorage(),
// _storageKey = key,
// super();
// @override
// Future<void> save(String newToken, dynamic newRecord) async {
// super.save(newToken, newRecord);
// final encoded = jsonEncode(<String, dynamic>{
// 'token': newToken,
// 'model': newRecord,
// });
// await _storage.write(key: _storageKey, value: encoded);
// }
// @override
// void clear() {
// super.clear();
// _storage.delete(key: _storageKey);
// }
// Future<void> loadFromStorage() async {
// final raw = await _storage.read(key: _storageKey);
// if (raw != null && raw.isNotEmpty) {
// try {
// final decoded = jsonDecode(raw) as Map<String, dynamic>;
// final token = decoded['token'] as String?;
// final model = decoded['model'];
// if (token != null && token.isNotEmpty) {
// super.save(token, model);
// return;
// }
// } catch (_) {
// clear();
// }
// }
// const legacyKey = 'auth_token';
// final legacyToken = await _storage.read(key: legacyKey);
// if (legacyToken != null && legacyToken.isNotEmpty) {
// super.save(legacyToken, null);
// }
// }
// }
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:pocketbase/pocketbase.dart'; import 'package:pocketbase/pocketbase.dart';
class PbAuthStore extends AuthStore { class PbAuthStore extends AuthStore {
final FlutterSecureStorage _storage; final FlutterSecureStorage _storage;
final String _storageKey; final String _saveKey = 'pb_auth';
PbAuthStore({ PbAuthStore({FlutterSecureStorage? storage})
FlutterSecureStorage? storage, : _storage = storage ??
String key = 'pb_auth', const FlutterSecureStorage(
}) : _storage = storage ?? const FlutterSecureStorage(), aOptions: AndroidOptions(encryptedSharedPreferences: true),
_storageKey = key, );
super();
@override @override
Future<void> save(String newToken, dynamic newRecord) async { Future<void> save(String token, dynamic model) async {
super.save(newToken, newRecord); super.save(token, model);
final encoded = jsonEncode(<String, dynamic>{ final encoded = jsonEncode(<String, dynamic>{
'token': newToken, 'token': token,
'model': newRecord, 'model': model,
}); });
await _storage.write(key: _storageKey, value: encoded); await _storage.write(key: _saveKey, value: encoded);
} }
@override @override
void clear() { Future<void> clear() async {
super.clear(); super.clear();
_storage.delete(key: _storageKey); await _storage.delete(key: _saveKey);
} }
// Diese Methode rufen wir VOR App-Start auf!
Future<void> loadFromStorage() async { Future<void> loadFromStorage() async {
final raw = await _storage.read(key: _storageKey); final raw = await _storage.read(key: _saveKey);
if (raw != null && raw.isNotEmpty) { if (raw != null && raw.isNotEmpty) {
try { try {
final decoded = jsonDecode(raw) as Map<String, dynamic>; final decoded = jsonDecode(raw);
final token = decoded['token'] as String?; final token = decoded['token'] as String? ?? '';
final model = decoded['model']; final modelData = decoded['model'];
if (token != null && token.isNotEmpty) { dynamic model;
if (modelData is Map<String, dynamic>) {
if (modelData.containsKey('collectionId')) {
model = RecordModel.fromJson(modelData);
} else {
model = RecordModel.fromJson(modelData);
// model = AdminModel.fromJson(modelData);
}
}
// super.save schreibt nur in den Speicher (RAM) des AuthStores,
// löst aber kein erneutes 'save' (und damit write) aus.
super.save(token, model); super.save(token, model);
return; } catch (e) {
// Daten korrupt? Löschen.
await clear();
} }
} catch (_) {
clear();
}
}
const legacyKey = 'auth_token';
final legacyToken = await _storage.read(key: legacyKey);
if (legacyToken != null && legacyToken.isNotEmpty) {
super.save(legacyToken, null);
} }
} }
} }