From 931338c9d36960a75b3e4886ac6e3c848c87c88e Mon Sep 17 00:00:00 2001 From: psmattas Date: Tue, 18 Aug 2026 12:07:02 +0100 Subject: [PATCH] fix(session): don't show signed-in UI when only half the session survived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionStore.init treated a Keychain token alone as "signed in," without checking the paired serverURL in UserDefaults also existed. UserDefaults is scoped to the app's sandboxed container (keyed by bundle ID), while Keychain items can survive a reinstall or bundle ID change independently of it — hit this live after renaming the bundle ID: Keychain still had the old token, the new container had no serverURL, so isSignedIn came back true with apiClient nil. App landed on Home with nothing able to load instead of the login screen. Now requires both to be present to consider the session valid, and clears whichever half survived otherwise so a fresh sign-in rewrites both consistently. --- Outpost/Root/SessionStore.swift | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Outpost/Root/SessionStore.swift b/Outpost/Root/SessionStore.swift index e085a6f..a22bf08 100644 --- a/Outpost/Root/SessionStore.swift +++ b/Outpost/Root/SessionStore.swift @@ -39,11 +39,27 @@ final class SessionStore { init(tokenStore: TokenStoring = KeychainTokenStore(), defaults: UserDefaults = .standard) { self.tokenStore = tokenStore self.defaults = defaults - self.isSignedIn = (try? tokenStore.token()) != nil self.cacheStore = (try? OfflineCacheStore.makeContainer()).map(OfflineCacheStore.init(modelContainer:)) - if isSignedIn, let serverURL { - (apiClient, cachingClient) = Self.makeAPIClient(serverURL: serverURL, tokenStore: tokenStore, cache: cacheStore) + let hasToken = (try? tokenStore.token()) != nil + let storedServerURL = defaults.string(forKey: Self.serverURLDefaultsKey).flatMap(URL.init(string:)) + + if hasToken, let storedServerURL { + isSignedIn = true + (apiClient, cachingClient) = Self.makeAPIClient(serverURL: storedServerURL, tokenStore: tokenStore, cache: cacheStore) + } else { + // Keychain and the sandboxed UserDefaults container don't + // always survive together — a Keychain item written by an + // older-signed build can outlive a reinstall that wipes the + // container (or vice versa), leaving a token with no server or + // a server with no token. Clear whichever half survived rather + // than showing a broken "signed in" UI with no working + // apiClient — a fresh sign-in rewrites both consistently. + if hasToken { + try? tokenStore.clear() + } + defaults.removeObject(forKey: Self.serverURLDefaultsKey) + isSignedIn = false } }