From e5b0755d9bab9fe848cba41e1e5cf0c4b41ca98b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 20:16:13 +0000 Subject: [PATCH] fix: secondary external-signer login lands on onboarding when switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LocalPreferences.setDefaultAccount called setCurrentAccount before saveToEncryptedStorage. setCurrentAccount emits the new list onto the savedAccounts MutableStateFlow, which AlwaysOnNotificationServiceManager collects and reacts to by calling loadAccountConfigFromEncryptedStorage for every saved account — including the just-added one. That call hit encryptedPreferences(newNpub) before NOSTR_PUBKEY had been written, got null, and cached the null in cachedAccounts. cachedAccounts is a process-lifetime map, so the poisoned entry survived the eventual disk write. Every subsequent switchUser to that account took the cached null path, fell through to requestLoginUI(), and AccountScreen rendered LoggedOffSetup — the onboarding screen with TOS unchecked, asking the user to re-do the Amber handshake. Write the per-npub file first, then seed the cache with the in-memory AccountSettings, then publish onto the savedAccounts flow. Also stop caching null returns in loadAccountConfigFromEncryptedStorage so any future racy reader can't poison the cache either. --- .../amethyst/LocalPreferences.kt | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9b21971808..14ea7901c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -323,8 +323,16 @@ object LocalPreferences { } suspend fun setDefaultAccount(accountSettings: AccountSettings) { - setCurrentAccount(accountSettings) + // Save the per-npub file before emitting onto the savedAccounts flow. + // Otherwise a collector (e.g. AlwaysOnNotificationServiceManager) can race in + // and call loadAccountConfigFromEncryptedStorage(npub) before NOSTR_PUBKEY is + // written, get null back, and poison `cachedAccounts[npub] = null` for the + // rest of the session — making every later switch to this account land on + // LoggedOff instead of LoggedIn. saveToEncryptedStorage(accountSettings) + val npub = accountSettings.keyPair.pubKey.toNpub() + mutex.withLock { cachedAccounts.put(npub, accountSettings) } + setCurrentAccount(accountSettings) } suspend fun allSavedAccounts(): List = savedAccounts() @@ -489,19 +497,20 @@ object LocalPreferences { suspend fun loadAccountConfigFromEncryptedStorage(npub: String): AccountSettings? { // if already loaded, return right away - if (cachedAccounts.containsKey(npub)) { - return cachedAccounts[npub] - } + cachedAccounts[npub]?.let { return it } return withContext(Dispatchers.IO) { mutex.withLock { - if (cachedAccounts.containsKey(npub)) { - return@withContext cachedAccounts.get(npub) - } + cachedAccounts[npub]?.let { return@withContext it } val accountSettings = innerLoadCurrentAccountFromEncryptedStorage(npub) - cachedAccounts.put(npub, accountSettings) + // Only cache successful loads. Caching null would leave the account + // permanently unreachable for the rest of the session if a reader + // raced in before the per-npub file finished being written. + if (accountSettings != null) { + cachedAccounts.put(npub, accountSettings) + } return@withContext accountSettings }