fix: fix errors while refreshing token on app start
This commit is contained in:
parent
b58b7ca57a
commit
361c43f3c1
9 changed files with 280 additions and 90 deletions
|
|
@ -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/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<AppDatabase>((ref) => throw UnimplementedError());
|
||||
|
|
|
|||
|
|
@ -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<SplashScreen> {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||
|
||||
apiClient.authStateChanges.listen((event) {
|
||||
if (event.token.isEmpty) {
|
||||
repo.logout();
|
||||
repo.clearLocalData();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -115,10 +115,11 @@ class AuthRepository {
|
|||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
if (apiClient.getToken() != null) {
|
||||
await apiClient.logout();
|
||||
await clearLocalData();
|
||||
}
|
||||
|
||||
Future<void> clearLocalData() async {
|
||||
await _storage.delete(key: AppConstants.keyLastSync);
|
||||
|
||||
await db.transaction(() async {
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const QuestBoardWidget(),
|
||||
// const QuestBoardWidget(),
|
||||
const Spacer(flex: 2),
|
||||
if (cycle != null)
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -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<Party> subscribeToParty(String partyId) async* {
|
||||
yield await getPartyDetails(partyId);
|
||||
Stream<Party> subscribeToParty(String 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) {
|
||||
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 {
|
||||
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<List<PartyMember>>(
|
||||
onListen: () async {
|
||||
try {
|
||||
controller.add(await fetchMembers());
|
||||
} catch (e) {
|
||||
controller.addError(e);
|
||||
}
|
||||
|
||||
final controller = StreamController<List<PartyMember>>();
|
||||
|
||||
_api.pb.collection('party_members').subscribe('*', (e) async {
|
||||
if (e.record != null && e.record!.getStringValue('party_id') == partyId) {
|
||||
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');
|
||||
},
|
||||
);
|
||||
|
||||
yield* controller.stream;
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<void> dealDamage(String partyId, int damage) async {
|
||||
|
|
|
|||
|
|
@ -179,10 +179,8 @@ class _LeaderboardScreenState extends ConsumerState<LeaderboardScreen> {
|
|||
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<LeaderboardScreen> {
|
|||
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<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 {
|
||||
return ref.read(leaderboardRepositoryProvider).getGlobalLeaderboard();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import 'package:pocketbase/pocketbase.dart';
|
|||
import '../../../core/constants/app_constants.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 {
|
||||
late final PocketBase _pb;
|
||||
final PbAuthStore _authStore;
|
||||
// final PbAuthStore _authStore;
|
||||
final Logger _logger;
|
||||
|
||||
PocketBase get pb => _pb;
|
||||
|
|
@ -18,19 +20,28 @@ class ApiClient {
|
|||
Stream<AuthStoreEvent> 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<T> _handleRequest<T>(Future<T> Function() request) async {
|
||||
try {
|
||||
|
|
|
|||
20
lib/src/shared/data/remote/custom_http_client.dart
Normal file
20
lib/src/shared/data/remote/custom_http_client.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '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<void> save(String newToken, dynamic newRecord) async {
|
||||
super.save(newToken, newRecord);
|
||||
Future<void> save(String token, dynamic model) async {
|
||||
super.save(token, model);
|
||||
|
||||
final encoded = jsonEncode(<String, dynamic>{
|
||||
'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<void> clear() async {
|
||||
super.clear();
|
||||
_storage.delete(key: _storageKey);
|
||||
await _storage.delete(key: _saveKey);
|
||||
}
|
||||
|
||||
// Diese Methode rufen wir VOR App-Start auf!
|
||||
Future<void> 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<String, dynamic>;
|
||||
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) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
const legacyKey = 'auth_token';
|
||||
final legacyToken = await _storage.read(key: legacyKey);
|
||||
if (legacyToken != null && legacyToken.isNotEmpty) {
|
||||
super.save(legacyToken, null);
|
||||
} catch (e) {
|
||||
// Daten korrupt? Löschen.
|
||||
await clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue