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.
|
// Token mitschicken statt ein eigenes 401 einzufangen.
|
||||||
final pending = _pendingReLogin;
|
final pending = _pendingReLogin;
|
||||||
if (pending != null) await pending;
|
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) {
|
if (token != null && token.isNotEmpty) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
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
|
/// Persists the Marianum-Connect bearer token in the platform keystore. Kept
|
||||||
/// separate from `AccountData` because the username/password live on (Nextcloud
|
/// separate from `AccountData` because the username/password live on (Nextcloud
|
||||||
/// + MHSL still need them) while the MC token is short-lived and per-endpoint.
|
/// + MHSL still need them) while the MC token is short-lived and per-endpoint.
|
||||||
@@ -11,7 +19,7 @@ class MarianumConnectTokenStorage {
|
|||||||
final FlutterSecureStorage _storage;
|
final FlutterSecureStorage _storage;
|
||||||
|
|
||||||
const MarianumConnectTokenStorage([
|
const MarianumConnectTokenStorage([
|
||||||
this._storage = const FlutterSecureStorage(),
|
this._storage = const FlutterSecureStorage(iOptions: _mcIosOptions),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Future<String?> readToken() => _storage.read(key: _tokenKey);
|
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(
|
unawaited(
|
||||||
FirebaseMessaging.instance.getToken().then(
|
FirebaseMessaging.instance
|
||||||
(token) => log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
|
.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
|
// Warm up the Nextcloud root listing in the background while the user is
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@@ -57,14 +56,6 @@ class AccountData {
|
|||||||
.convert(utf8.encode('${getUsername()}:${getPassword()}'))
|
.convert(utf8.encode('${getUsername()}:${getPassword()}'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
Future<String> getDeviceId() async => sha512
|
|
||||||
.convert(
|
|
||||||
utf8.encode(
|
|
||||||
'${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toString();
|
|
||||||
|
|
||||||
Future<void> setData(String username, String password) async {
|
Future<void> setData(String username, String password) async {
|
||||||
await _secureStorage.write(key: _usernameField, value: username);
|
await _secureStorage.write(key: _usernameField, value: username);
|
||||||
await _secureStorage.write(key: _passwordField, value: password);
|
await _secureStorage.write(key: _passwordField, value: password);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class DataCleaner {
|
|||||||
.get();
|
.get();
|
||||||
cacheData?.forEach((key, value) async {
|
cacheData?.forEach((key, value) async {
|
||||||
final lastUpdate = DateTime.fromMillisecondsSinceEpoch(
|
final lastUpdate = DateTime.fromMillisecondsSinceEpoch(
|
||||||
value['lastupdate'] as int,
|
((value['lastupdate'] as num?) ?? 0).toInt(),
|
||||||
);
|
);
|
||||||
if (DateTime.now()
|
if (DateTime.now()
|
||||||
.subtract(const Duration(days: 200))
|
.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)));
|
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||||
|
}
|
||||||
|
|
||||||
Connectivity().checkConnectivity().then(emitConnectivity);
|
Connectivity().checkConnectivity().then(emitConnectivity);
|
||||||
_updateStream = Connectivity().onConnectivityChanged.listen(
|
_updateStream = Connectivity().onConnectivityChanged.listen(
|
||||||
@@ -48,10 +52,10 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
|||||||
// Re-check connectivity so the resulting [ConnectivityChanged] handler
|
// Re-check connectivity so the resulting [ConnectivityChanged] handler
|
||||||
// clears a stale error bar and triggers [reFetch] once reachable again.
|
// clears a stale error bar and triggers [reFetch] once reachable again.
|
||||||
unawaited(
|
unawaited(
|
||||||
Connectivity().checkConnectivity().then(
|
Connectivity().checkConnectivity().then((result) {
|
||||||
(result) =>
|
if (isClosed) return;
|
||||||
add(ConnectivityChanged(LoadableStateState(connections: result))),
|
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user