From 361c43f3c1799967a711528fb0834418ccd3ca00 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Thu, 22 Jan 2026 15:47:01 +0100 Subject: [PATCH] fix: fix errors while refreshing token on app start --- lib/main.dart | 94 +++++++++++--- lib/src/core/routing/app_router.dart | 16 ++- .../data/repositories/auth_repository.dart | 9 +- .../presentation/screens/hub_screen.dart | 6 +- .../data/repositories/party_repository.dart | 69 +++++++--- .../screens/leaderboard_screen.dart | 6 - lib/src/shared/data/remote/api_client.dart | 31 +++-- .../data/remote/custom_http_client.dart | 20 +++ lib/src/shared/data/remote/pb_auth_store.dart | 119 +++++++++++++----- 9 files changed, 280 insertions(+), 90 deletions(-) create mode 100644 lib/src/shared/data/remote/custom_http_client.dart diff --git a/lib/main.dart b/lib/main.dart index 1c58ead..64d3a00 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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((ref) => throw UnimplementedError()); +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/services.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: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 { WidgetsFlutterBinding.ensureInitialized(); + // 1. Env laden 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([ @@ -28,18 +92,13 @@ void main() async { final database = AppDatabase(); + // 2. Auth Store erstellen UND laden (Warten!) final authStore = PbAuthStore(); - await authStore.loadFromStorage(); + await authStore.loadFromStorage(); // Das ist der entscheidende 'await' - if (authStore.isValid && authStore.record == null) { - final tempClient = ApiClient(authStore: authStore); - try { - await tempClient.refreshAuth(); - } catch (e) { - log('Initial auth refresh failed: $e'); - } - } + log("Auth loaded. Valid? ${authStore.isValid}"); // Debug Log + // 3. App starten mit injiziertem Store runApp( ProviderScope( overrides: [ @@ -52,5 +111,6 @@ void main() async { ); } +// Provider Definition für DB (falls noch nicht vorhanden) final appDatabaseProvider = Provider((ref) => throw UnimplementedError()); diff --git a/lib/src/core/routing/app_router.dart b/lib/src/core/routing/app_router.dart index c2ec64b..b56219d 100644 --- a/lib/src/core/routing/app_router.dart +++ b/lib/src/core/routing/app_router.dart @@ -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/lobby_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/profile_screen.dart'; @@ -205,7 +206,20 @@ class _SplashScreenState extends ConsumerState { } Future _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; diff --git a/lib/src/features/authentication/data/repositories/auth_repository.dart b/lib/src/features/authentication/data/repositories/auth_repository.dart index 0840ddc..77da18a 100644 --- a/lib/src/features/authentication/data/repositories/auth_repository.dart +++ b/lib/src/features/authentication/data/repositories/auth_repository.dart @@ -15,7 +15,7 @@ final authRepositoryProvider = Provider((ref) { apiClient.authStateChanges.listen((event) { if (event.token.isEmpty) { - repo.logout(); + repo.clearLocalData(); } }); @@ -115,10 +115,11 @@ class AuthRepository { } Future logout() async { - if (apiClient.getToken() != null) { - await apiClient.logout(); - } + await apiClient.logout(); + await clearLocalData(); + } + Future clearLocalData() async { await _storage.delete(key: AppConstants.keyLastSync); await db.transaction(() async { diff --git a/lib/src/features/dashboard/presentation/screens/hub_screen.dart b/lib/src/features/dashboard/presentation/screens/hub_screen.dart index fc52ca4..75c678d 100644 --- a/lib/src/features/dashboard/presentation/screens/hub_screen.dart +++ b/lib/src/features/dashboard/presentation/screens/hub_screen.dart @@ -108,7 +108,7 @@ class _HubScreenState extends ConsumerState { if (!found) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( + SnackBar( content: Text(AppLocalizations.of(context)!.hubCycleComplete)), ); } @@ -224,7 +224,7 @@ class _HubScreenState extends ConsumerState { ), const SizedBox(height: 8), if (sets >= 20) - Text(l10n.missionBriefingHardcore, + Text(l10n.missionBriefingHardcore, style: const TextStyle( color: AppTheme.errorColor, fontSize: 10, @@ -465,7 +465,7 @@ class _HubScreenState extends ConsumerState { ), ), const SizedBox(height: 16), - const QuestBoardWidget(), + // const QuestBoardWidget(), const Spacer(flex: 2), if (cycle != null) Padding( diff --git a/lib/src/features/multiplayer/data/repositories/party_repository.dart b/lib/src/features/multiplayer/data/repositories/party_repository.dart index 830af6c..9e8f95d 100644 --- a/lib/src/features/multiplayer/data/repositories/party_repository.dart +++ b/lib/src/features/multiplayer/data/repositories/party_repository.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.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_member.dart'; -import 'package:slrpg_app/src/shared/data/repositories/user_repository.dart'; import '../../../../shared/data/remote/api_client.dart'; final partyRepositoryProvider = Provider((ref) { @@ -63,21 +62,39 @@ class PartyRepository { await _api.pb.collection('parties').update(partyId, body: body); } - Stream subscribeToParty(String partyId) async* { - yield await getPartyDetails(partyId); + Stream subscribeToParty(String partyId) { + late StreamController controller; + UnsubscribeFunc? unsubscribe; - final controller = StreamController(); + controller = StreamController( + onListen: () async { + try { + final initial = await getPartyDetails(partyId); + controller.add(initial); + } catch (e) { + controller.addError(e); + } - _api.pb.collection('parties').subscribe(partyId, (e) { - if (e.action == 'update' && e.record != null) { - controller.add(Party.fromJson(e.record!.toJson())); - } - }); + unsubscribe = + await _api.pb.collection('parties').subscribe(partyId, (e) { + if (e.action == 'update' && e.record != null) { + controller.add(Party.fromJson(e.record!.toJson())); + } + }); + }, + onCancel: () async { + await unsubscribe?.call(); + log('🔌 Unsubscribed from party $partyId'); + }, + ); - yield* controller.stream; + return controller.stream; } - Stream> subscribeToMembers(String partyId) async* { + Stream> subscribeToMembers(String partyId) { + late StreamController> controller; + UnsubscribeFunc? unsubscribe; + Future> fetchMembers() async { final records = await _api.pb.collection('party_members').getFullList( filter: 'party_id="$partyId"', @@ -86,17 +103,29 @@ class PartyRepository { return records.map((r) => PartyMember.fromRecord(r.toJson())).toList(); } - yield await fetchMembers(); + controller = StreamController>( + onListen: () async { + try { + controller.add(await fetchMembers()); + } catch (e) { + controller.addError(e); + } - final controller = StreamController>(); + unsubscribe = + await _api.pb.collection('party_members').subscribe('*', (e) async { + if (e.record != null && + e.record!.getStringValue('party_id') == partyId) { + controller.add(await fetchMembers()); + } + }); + }, + onCancel: () async { + await unsubscribe?.call(); + log('🔌 Unsubscribed from party members $partyId'); + }, + ); - _api.pb.collection('party_members').subscribe('*', (e) async { - if (e.record != null && e.record!.getStringValue('party_id') == partyId) { - controller.add(await fetchMembers()); - } - }); - - yield* controller.stream; + return controller.stream; } Future dealDamage(String partyId, int damage) async { diff --git a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart index 5dfda17..3e25561 100644 --- a/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart +++ b/lib/src/features/multiplayer/presentation/screens/leaderboard_screen.dart @@ -179,10 +179,8 @@ class _LeaderboardScreenState extends ConsumerState { builder: (context, snapshot) { final currentUserId = snapshot.data?.serverId ?? ''; - // NEU: RefreshIndicator für Pull-to-Refresh return RefreshIndicator( onRefresh: () async { - // Erzwingt ein Neuladen der Daten return ref.refresh(leaderboardProvider.future); }, child: ListView.builder( @@ -276,8 +274,6 @@ class _LeaderboardScreenState extends ConsumerState { Widget _buildAvatarPreview(LeaderboardEntry entry) { if (entry.avatar != null && entry.avatar!.isNotEmpty) { 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!); return SizedBox( @@ -300,8 +296,6 @@ class _LeaderboardScreenState extends ConsumerState { } } -// 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 { return ref.read(leaderboardRepositoryProvider).getGlobalLeaderboard(); }); diff --git a/lib/src/shared/data/remote/api_client.dart b/lib/src/shared/data/remote/api_client.dart index 1d94790..ff1569b 100644 --- a/lib/src/shared/data/remote/api_client.dart +++ b/lib/src/shared/data/remote/api_client.dart @@ -6,11 +6,13 @@ import 'package:pocketbase/pocketbase.dart'; import '../../../core/constants/app_constants.dart'; import 'pb_auth_store.dart'; -final apiClientProvider = Provider((ref) => ApiClient()); +// final apiClientProvider = Provider((ref) => ApiClient()); +final apiClientProvider = + Provider((ref) => throw UnimplementedError()); class ApiClient { late final PocketBase _pb; - final PbAuthStore _authStore; + // final PbAuthStore _authStore; final Logger _logger; PocketBase get pb => _pb; @@ -18,19 +20,28 @@ class ApiClient { Stream get authStateChanges => _pb.authStore.onChange; ApiClient({ - PbAuthStore? authStore, - FlutterSecureStorage? storage, + required AuthStore authStore, Logger? logger, - }) : _logger = logger ?? Logger(), - _authStore = authStore ?? PbAuthStore(storage: storage) { + }) : _logger = logger ?? Logger() { _pb = PocketBase( 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 _handleRequest(Future Function() request) async { try { diff --git a/lib/src/shared/data/remote/custom_http_client.dart b/lib/src/shared/data/remote/custom_http_client.dart new file mode 100644 index 0000000..bea95f4 --- /dev/null +++ b/lib/src/shared/data/remote/custom_http_client.dart @@ -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; + } +} diff --git a/lib/src/shared/data/remote/pb_auth_store.dart b/lib/src/shared/data/remote/pb_auth_store.dart index ef43b5c..17dc5de 100644 --- a/lib/src/shared/data/remote/pb_auth_store.dart +++ b/lib/src/shared/data/remote/pb_auth_store.dart @@ -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 save(String newToken, dynamic newRecord) async { +// super.save(newToken, newRecord); + +// final encoded = jsonEncode({ +// 'token': newToken, +// 'model': newRecord, +// }); + +// await _storage.write(key: _storageKey, value: encoded); +// } + +// @override +// void clear() { +// super.clear(); +// _storage.delete(key: _storageKey); +// } + +// Future loadFromStorage() async { +// final raw = await _storage.read(key: _storageKey); +// if (raw != null && raw.isNotEmpty) { +// try { +// final decoded = jsonDecode(raw) as Map; +// 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 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:pocketbase/pocketbase.dart'; class PbAuthStore extends AuthStore { final FlutterSecureStorage _storage; - final String _storageKey; + final String _saveKey = 'pb_auth'; - PbAuthStore({ - FlutterSecureStorage? storage, - String key = 'pb_auth', - }) : _storage = storage ?? const FlutterSecureStorage(), - _storageKey = key, - super(); + PbAuthStore({FlutterSecureStorage? storage}) + : _storage = storage ?? + const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); @override - Future save(String newToken, dynamic newRecord) async { - super.save(newToken, newRecord); + Future save(String token, dynamic model) async { + super.save(token, model); final encoded = jsonEncode({ - 'token': newToken, - 'model': newRecord, + 'token': token, + 'model': model, }); - await _storage.write(key: _storageKey, value: encoded); + await _storage.write(key: _saveKey, value: encoded); } @override - void clear() { + Future clear() async { super.clear(); - _storage.delete(key: _storageKey); + await _storage.delete(key: _saveKey); } + // Diese Methode rufen wir VOR App-Start auf! Future loadFromStorage() async { - final raw = await _storage.read(key: _storageKey); + final raw = await _storage.read(key: _saveKey); if (raw != null && raw.isNotEmpty) { try { - final decoded = jsonDecode(raw) as Map; - final token = decoded['token'] as String?; - final model = decoded['model']; + final decoded = jsonDecode(raw); + final token = decoded['token'] as String? ?? ''; + final modelData = decoded['model']; - if (token != null && token.isNotEmpty) { - super.save(token, model); - return; + dynamic model; + if (modelData is Map) { + if (modelData.containsKey('collectionId')) { + model = RecordModel.fromJson(modelData); + } else { + model = RecordModel.fromJson(modelData); + // model = AdminModel.fromJson(modelData); + } } - } catch (_) { - clear(); - } - } - const legacyKey = 'auth_token'; - final legacyToken = await _storage.read(key: legacyKey); - if (legacyToken != null && legacyToken.isNotEmpty) { - super.save(legacyToken, null); + // super.save schreibt nur in den Speicher (RAM) des AuthStores, + // löst aber kein erneutes 'save' (und damit write) aus. + super.save(token, model); + } catch (e) { + // Daten korrupt? Löschen. + await clear(); + } } } }