merged develop
This commit is contained in:
@@ -10,8 +10,7 @@ import '../api/marianumconnect/queries/auth_logout/auth_logout.dart';
|
||||
import '../api/marianumconnect/queries/auth_me/auth_me.dart';
|
||||
import '../auth_link/guardian_link_listener.dart';
|
||||
import '../background/widget_background_task.dart';
|
||||
import '../push/chat_thread_store.dart';
|
||||
import '../push/nid_store.dart';
|
||||
import '../model/session_wipe.dart';
|
||||
import '../push/push_registration.dart';
|
||||
import '../storage/account_storage.dart';
|
||||
import '../widget_data/widget_sync.dart';
|
||||
@@ -41,10 +40,11 @@ abstract final class SessionLifecycle {
|
||||
}
|
||||
await AuthLogout().run();
|
||||
final removed = await SessionManager().signOut();
|
||||
SessionWipe.clearImmediate();
|
||||
// A login link that arrived while signed in must not be replayed on the
|
||||
// login screen that follows.
|
||||
GuardianLinkListener.clear();
|
||||
await _clearPushStores();
|
||||
await SessionWipe.accountLeft();
|
||||
if (removed != null) await AccountStorage.delete(removed.namespace);
|
||||
|
||||
for (
|
||||
@@ -78,14 +78,22 @@ abstract final class SessionLifecycle {
|
||||
log('Switch: push deactivation failed: $e');
|
||||
}
|
||||
await SessionManager().stashActive();
|
||||
SessionWipe.clearImmediate();
|
||||
var restored = false;
|
||||
try {
|
||||
await SessionManager().activate(id);
|
||||
} on Object {
|
||||
if (previous != null) await SessionManager().activate(previous.id);
|
||||
if (previous != null) {
|
||||
await SessionManager().activate(previous.id);
|
||||
restored = true;
|
||||
}
|
||||
rethrow;
|
||||
} finally {
|
||||
await _clearPushStores();
|
||||
await SessionWipe.accountLeft();
|
||||
await AccountStorage.activate(SessionManager().activeAccount);
|
||||
// Nothing remounts for the account that stays, so its push would
|
||||
// remain unregistered until the next start.
|
||||
if (restored) unawaited(PushRegistration().register());
|
||||
}
|
||||
await _resetWidget();
|
||||
}
|
||||
@@ -94,6 +102,10 @@ abstract final class SessionLifecycle {
|
||||
static Future<void> beginAddAccount() async {
|
||||
await SessionManager().stashActive();
|
||||
_addReturnId = SessionManager().activeAccount?.id;
|
||||
SessionWipe.clearImmediate();
|
||||
// Whatever runs behind the login screen must not write into the parked
|
||||
// account's data.
|
||||
await AccountStorage.activate(null);
|
||||
}
|
||||
|
||||
static bool get isAddingAccount => _addReturnId != null;
|
||||
@@ -109,6 +121,7 @@ abstract final class SessionLifecycle {
|
||||
await SessionManager().signOut();
|
||||
}
|
||||
await SessionManager().activate(returnId);
|
||||
await AccountStorage.activate(SessionManager().activeAccount);
|
||||
}
|
||||
|
||||
/// Called once a login finished. After "add account" the previous account's
|
||||
@@ -127,7 +140,7 @@ abstract final class SessionLifecycle {
|
||||
log('Add account: push deactivation failed: $e');
|
||||
}
|
||||
await SessionManager().activate(added.id);
|
||||
await _clearPushStores();
|
||||
await SessionWipe.accountLeft();
|
||||
}
|
||||
await AccountStorage.activate(SessionManager().activeAccount);
|
||||
return added.id;
|
||||
@@ -191,14 +204,4 @@ abstract final class SessionLifecycle {
|
||||
log('Widget reset failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Tray bookkeeping of the previous account's pushes.
|
||||
static Future<void> _clearPushStores() async {
|
||||
try {
|
||||
await NidStore().clear();
|
||||
await ChatThreadStore().clearAll();
|
||||
} on Object catch (e) {
|
||||
log('Push store cleanup failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,21 @@ class SessionManager {
|
||||
|
||||
Session? get current => _current;
|
||||
|
||||
int _sessionEpoch = 0;
|
||||
|
||||
/// Bumped whenever the active account changes (sign-out, switch, another
|
||||
/// account signing in). Async work captures it when it starts and drops its
|
||||
/// result when it changed meanwhile, so a request of the previous account
|
||||
/// cannot land in the next account's state or cache.
|
||||
int get sessionEpoch => _sessionEpoch;
|
||||
|
||||
bool isCurrentSession(int epoch) => epoch == _sessionEpoch;
|
||||
|
||||
bool _isUiEngine = false;
|
||||
|
||||
/// Called from `main()`; background entry points never run it.
|
||||
void markUiEngine() => _isUiEngine = true;
|
||||
|
||||
/// Every signed-in account and which one is active.
|
||||
final ValueNotifier<AccountIndex> accounts = ValueNotifier(
|
||||
AccountIndex.empty,
|
||||
@@ -115,6 +130,7 @@ class SessionManager {
|
||||
}
|
||||
|
||||
Future<void> _writeActive(Session session) async {
|
||||
if (!_isSameAccount(_current, session)) _sessionEpoch++;
|
||||
await Future.wait([
|
||||
for (final MapEntry(:key, :value) in encodeSessionFields(session).entries)
|
||||
_writeSecret(key, value),
|
||||
@@ -134,6 +150,7 @@ class SessionManager {
|
||||
/// stay in their vaults; see [activate].
|
||||
Future<AccountEntry?> signOut() async {
|
||||
final removed = activeAccount;
|
||||
_sessionEpoch++;
|
||||
_loaded = Completer();
|
||||
_current = null;
|
||||
await Future.wait([
|
||||
@@ -193,6 +210,11 @@ class SessionManager {
|
||||
await _saveIndex(accounts.value.remove(id));
|
||||
}
|
||||
|
||||
static bool _isSameAccount(Session? a, Session? b) {
|
||||
if (a == null || b == null) return a == b;
|
||||
return identityOf(a) == identityOf(b) && a.isDemo == b.isDemo;
|
||||
}
|
||||
|
||||
Future<void> _saveIndex(AccountIndex index) async {
|
||||
accounts.value = index;
|
||||
await _secureStorage.write(key: _indexKey, value: index.encode());
|
||||
@@ -280,8 +302,15 @@ class SessionManager {
|
||||
Future<void> _migrateAndLoad() async {
|
||||
await _migrateFromLegacyStorage();
|
||||
await _migrateKeychainAccessibility();
|
||||
// On the startup critical path (and every background wake): read in
|
||||
// parallel instead of one keychain round-trip after the other.
|
||||
_current = await _readActive();
|
||||
await _loadIndex();
|
||||
if (!_loaded.isCompleted) _loaded.complete();
|
||||
}
|
||||
|
||||
/// The session in the active slots. On the startup critical path (and every
|
||||
/// background wake): read in parallel instead of one keychain round-trip
|
||||
/// after the other.
|
||||
Future<Session?> _readActive() async {
|
||||
final values = await Future.wait(
|
||||
_sessionFields.map((field) => _secureStorage.read(key: field)),
|
||||
);
|
||||
@@ -296,9 +325,35 @@ class SessionManager {
|
||||
} on Object {
|
||||
// Group keystore unavailable: fall back to the real password.
|
||||
}
|
||||
_current = decodeSession(raw);
|
||||
await _loadIndex();
|
||||
if (!_loaded.isCompleted) _loaded.complete();
|
||||
return decodeSession(raw);
|
||||
}
|
||||
|
||||
/// Username currently in the active slots. Other engines (widget task, push
|
||||
/// isolates) sign out, in or switch without this instance noticing.
|
||||
Future<String?> readStoredUsername() =>
|
||||
_secureStorage.read(key: SessionKeys.username);
|
||||
|
||||
/// Re-reads the session for long-lived background engines: they load once
|
||||
/// and would otherwise keep acting with an account that signed out or was
|
||||
/// switched away from in the app meanwhile. Keeps the known state when the
|
||||
/// keystore is unreadable.
|
||||
Future<void> reloadFromStorage() async {
|
||||
// The UI engine performs sign-in, sign-out and switches itself, so its
|
||||
// state is current; re-reading in between could resurrect the removed
|
||||
// account.
|
||||
if (_isUiEngine) return;
|
||||
try {
|
||||
final session = await _readActive();
|
||||
final index = AccountIndex.decode(
|
||||
await _secureStorage.read(key: _indexKey),
|
||||
);
|
||||
if (!_isSameAccount(_current, session)) _sessionEpoch++;
|
||||
_current = session;
|
||||
accounts.value = index;
|
||||
if (!_loaded.isCompleted) _loaded.complete();
|
||||
} on Object catch (e) {
|
||||
log('Session reload failed, keeping loaded state: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadIndex() async {
|
||||
|
||||
Reference in New Issue
Block a user