bugfixes, better background operation robustness and platform-specific error handling

This commit is contained in:
2026-07-12 19:17:49 +02:00
parent babc347b18
commit a7111844b1
6 changed files with 41 additions and 21 deletions
@@ -36,7 +36,15 @@ class MarianumConnectAuthInterceptor extends Interceptor {
// Token mitschicken statt ein eigenes 401 einzufangen.
final pending = _pendingReLogin;
if (pending != null) await pending;
final token = await _tokenStorage.readToken();
// Reading the keystore can throw while the device is locked (iOS
// errSecInteractionNotAllowed on background requests). Degrade to an
// unauthenticated request instead of surfacing a platform error.
String? token;
try {
token = await _tokenStorage.readToken();
} catch (_) {
token = null;
}
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
@@ -1,5 +1,13 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// `first_unlock` accessibility so the token can be read during background
/// requests (telemetry heartbeat, push-triggered syncs) after the first device
/// unlock following a reboot. The keychain default (`whenUnlocked`) throws
/// `-25308 errSecInteractionNotAllowed` when the device is locked.
const IOSOptions _mcIosOptions = IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
);
/// Persists the Marianum-Connect bearer token in the platform keystore. Kept
/// separate from `AccountData` because the username/password live on (Nextcloud
/// + MHSL still need them) while the MC token is short-lived and per-endpoint.
@@ -11,7 +19,7 @@ class MarianumConnectTokenStorage {
final FlutterSecureStorage _storage;
const MarianumConnectTokenStorage([
this._storage = const FlutterSecureStorage(),
this._storage = const FlutterSecureStorage(iOptions: _mcIosOptions),
]);
Future<String?> readToken() => _storage.read(key: _tokenKey);
+12 -3
View File
@@ -121,10 +121,19 @@ Future<void> main() async {
),
);
// Diagnostic log only. getToken() can fail transiently during cold start
// (Android: "IOException: FCM Registration failed!" from Play Services,
// iOS: apns-token-not-set until APNS registration completes), so tolerate
// the failure instead of letting it surface as an uncaught async error that
// gets reported to the telemetry backend as noise.
unawaited(
FirebaseMessaging.instance.getToken().then(
(token) => log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
),
FirebaseMessaging.instance
.getToken()
.then(
(token) =>
log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
)
.onError((e, _) => log('Firebase token unavailable: $e')),
);
// Warm up the Nextcloud root listing in the background while the user is
-9
View File
@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -57,14 +56,6 @@ class AccountData {
.convert(utf8.encode('${getUsername()}:${getPassword()}'))
.toString();
Future<String> getDeviceId() async => sha512
.convert(
utf8.encode(
'${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}',
),
)
.toString();
Future<void> setData(String username, String password) async {
await _secureStorage.write(key: _usernameField, value: username);
await _secureStorage.write(key: _passwordField, value: password);
+1 -1
View File
@@ -9,7 +9,7 @@ class DataCleaner {
.get();
cacheData?.forEach((key, value) async {
final lastUpdate = DateTime.fromMillisecondsSinceEpoch(
value['lastupdate'] as int,
((value['lastupdate'] as num?) ?? 0).toInt(),
);
if (DateTime.now()
.subtract(const Duration(days: 200))
@@ -27,8 +27,12 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
}
});
void emitConnectivity(List<ConnectivityResult> result) =>
add(ConnectivityChanged(LoadableStateState(connections: result)));
void emitConnectivity(List<ConnectivityResult> result) {
// The initial checkConnectivity() future is not cancellable and may
// resolve after the bloc was disposed, so guard against a closed sink.
if (isClosed) return;
add(ConnectivityChanged(LoadableStateState(connections: result)));
}
Connectivity().checkConnectivity().then(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(
@@ -48,10 +52,10 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
// Re-check connectivity so the resulting [ConnectivityChanged] handler
// clears a stale error bar and triggers [reFetch] once reachable again.
unawaited(
Connectivity().checkConnectivity().then(
(result) =>
add(ConnectivityChanged(LoadableStateState(connections: result))),
),
Connectivity().checkConnectivity().then((result) {
if (isClosed) return;
add(ConnectivityChanged(LoadableStateState(connections: result)));
}),
);
}