import 'dart:async'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:matrix/matrix.dart' as mx; import 'package:pyramid/core/fcm_push_service.dart'; import 'package:pyramid/core/matrix_client.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _prefHomeserver = 'homeserver_url'; final homeserverProvider = StateProvider((ref) => null); Future initHomeserver(WidgetRef ref) async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString(_prefHomeserver); ref.read(homeserverProvider.notifier).state = saved; } Future saveHomeserver(WidgetRef ref, String url) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefHomeserver, url); ref.read(homeserverProvider.notifier).state = url; } enum LoginStatus { idle, loading, error } class LoginFormState { final LoginStatus status; final String? errorMessage; const LoginFormState({this.status = LoginStatus.idle, this.errorMessage}); LoginFormState copyWith({LoginStatus? status, String? errorMessage}) => LoginFormState( status: status ?? this.status, errorMessage: errorMessage, ); } class LoginNotifier extends StateNotifier { final Ref _ref; LoginNotifier(this._ref) : super(const LoginFormState()); Future login({ required String username, required String password, required String homeserver, String? deviceName, }) async { state = state.copyWith(status: LoginStatus.loading, errorMessage: null); final client = _ref.read(matrixClientProvider).valueOrNull; if (client == null) { state = state.copyWith( status: LoginStatus.error, errorMessage: 'Client nicht initialisiert', ); return; } if (client.isLogged()) { state = state.copyWith(status: LoginStatus.idle); return; } try { final serverUri = Uri.https( homeserver.replaceAll(RegExp(r'^https?://'), ''), '', ); await client.checkHomeserver(serverUri); await client.login( mx.LoginType.mLoginPassword, identifier: mx.AuthenticationUserIdentifier(user: username), password: password, initialDeviceDisplayName: deviceName ?? 'Pyramid', // Ohne Refresh-Token eskaliert das SDK jeden Soft-Logout (z. B. nach // Ablauf des Access-Tokens) sofort zu einem echten, destruktiven // Logout, weil `refreshAccessToken()` sonst mangels Refresh-Token // fehlschlägt (siehe matrix-Paket Client._handleSoftLogout). Das ist // der wahrscheinlichste Auslöser des Uta-Random-Logout-Bugs. refreshToken: true, ); if (Platform.isAndroid) unawaited(initFcm(client)); state = state.copyWith(status: LoginStatus.idle); } on mx.MatrixException catch (e) { state = state.copyWith( status: LoginStatus.error, errorMessage: e.errorMessage, ); } catch (e) { state = state.copyWith( status: LoginStatus.error, errorMessage: e.toString(), ); } } } final loginNotifierProvider = StateNotifierProvider((ref) { return LoginNotifier(ref); }); final isLoggedInProvider = StreamProvider((ref) async* { final client = await ref.watch(matrixClientProvider.future); yield client.isLogged(); await for (final loginState in client.onLoginStateChanged.stream) { // softLoggedOut ist KEIN Logout: Die Session existiert lokal weiter, das // SDK versucht gerade einen stillen Token-Refresh. Würde hier `false` // geliefert, spränge die UI bei jedem Routine-Refresh kurz auf den // Login-Screen – für Nutzer nicht von einem „Random Logout" zu // unterscheiden. Nur ein echtes loggedOut zählt. yield loginState != mx.LoginState.loggedOut; } });