bugfixes, better background operation robustness and platform-specific error handling
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) =>
|
||||
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)));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user