diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index 6361093de..390ae41fa 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -13,6 +13,7 @@ final class BridgeConnectionController: ObservableObject { private weak var appModel: NodeAppModel? private var cancellables = Set() private var didAutoConnect = false + private var seenStableIDs = Set() init(appModel: NodeAppModel) { self.appModel = appModel @@ -23,6 +24,7 @@ final class BridgeConnectionController: ObservableObject { .sink { [weak self] newValue in guard let self else { return } self.bridges = newValue + self.updateLastDiscoveredBridge(from: newValue) self.maybeAutoConnect() } .store(in: &self.cancellables) @@ -50,9 +52,9 @@ final class BridgeConnectionController: ObservableObject { guard appModel.bridgeServerName == nil else { return } let defaults = UserDefaults.standard - let preferredStableID = defaults.string(forKey: "bridge.preferredStableID")? + let targetStableID = defaults.string(forKey: "bridge.lastDiscoveredStableID")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !preferredStableID.isEmpty else { return } + guard !targetStableID.isEmpty else { return } let instanceId = defaults.string(forKey: "node.instanceId")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -64,12 +66,20 @@ final class BridgeConnectionController: ObservableObject { .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !token.isEmpty else { return } - guard let target = self.bridges.first(where: { $0.stableID == preferredStableID }) else { return } + guard let target = self.bridges.first(where: { $0.stableID == targetStableID }) else { return } self.didAutoConnect = true appModel.connectToBridge(endpoint: target.endpoint, hello: self.makeHello(token: token)) } + private func updateLastDiscoveredBridge(from bridges: [BridgeDiscoveryModel.DiscoveredBridge]) { + let newlyDiscovered = bridges.filter { self.seenStableIDs.insert($0.stableID).inserted } + guard let last = newlyDiscovered.last else { return } + + UserDefaults.standard.set(last.stableID, forKey: "bridge.lastDiscoveredStableID") + BridgeSettingsStore.saveLastDiscoveredBridgeStableID(last.stableID) + } + private func makeHello(token: String) -> BridgeHello { let defaults = UserDefaults.standard let nodeId = defaults.string(forKey: "node.instanceId") ?? "ios-node" diff --git a/apps/ios/Sources/Bridge/BridgeSettingsStore.swift b/apps/ios/Sources/Bridge/BridgeSettingsStore.swift index 653d56280..f73a02637 100644 --- a/apps/ios/Sources/Bridge/BridgeSettingsStore.swift +++ b/apps/ios/Sources/Bridge/BridgeSettingsStore.swift @@ -6,13 +6,16 @@ enum BridgeSettingsStore { private static let instanceIdDefaultsKey = "node.instanceId" private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID" + private static let lastDiscoveredBridgeStableIDDefaultsKey = "bridge.lastDiscoveredStableID" private static let instanceIdAccount = "instanceId" private static let preferredBridgeStableIDAccount = "preferredStableID" + private static let lastDiscoveredBridgeStableIDAccount = "lastDiscoveredStableID" static func bootstrapPersistence() { self.ensureStableInstanceID() self.ensurePreferredBridgeStableID() + self.ensureLastDiscoveredBridgeStableID() } static func loadStableInstanceID() -> String? { @@ -36,6 +39,18 @@ enum BridgeSettingsStore { account: self.preferredBridgeStableIDAccount) } + static func loadLastDiscoveredBridgeStableID() -> String? { + KeychainStore.loadString(service: self.bridgeService, account: self.lastDiscoveredBridgeStableIDAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + static func saveLastDiscoveredBridgeStableID(_ stableID: String) { + _ = KeychainStore.saveString( + stableID, + service: self.bridgeService, + account: self.lastDiscoveredBridgeStableIDAccount) + } + private static func ensureStableInstanceID() { let defaults = UserDefaults.standard @@ -76,4 +91,22 @@ enum BridgeSettingsStore { defaults.set(stored, forKey: self.preferredBridgeStableIDDefaultsKey) } } + + private static func ensureLastDiscoveredBridgeStableID() { + let defaults = UserDefaults.standard + + if let existing = defaults.string(forKey: self.lastDiscoveredBridgeStableIDDefaultsKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !existing.isEmpty + { + if self.loadLastDiscoveredBridgeStableID() == nil { + self.saveLastDiscoveredBridgeStableID(existing) + } + return + } + + if let stored = self.loadLastDiscoveredBridgeStableID(), !stored.isEmpty { + defaults.set(stored, forKey: self.lastDiscoveredBridgeStableIDDefaultsKey) + } + } } diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index d5304d698..de01a2548 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -19,6 +19,7 @@ struct SettingsTab: View { @AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false @AppStorage("camera.enabled") private var cameraEnabled: Bool = true @AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = "" + @AppStorage("bridge.lastDiscoveredStableID") private var lastDiscoveredBridgeStableID: String = "" @StateObject private var connectStatus = ConnectStatusStore() @State private var connectingBridgeID: String? @State private var localIPAddress: String? @@ -207,6 +208,8 @@ struct SettingsTab: View { self.connectingBridgeID = bridge.id self.preferredBridgeStableID = bridge.stableID BridgeSettingsStore.savePreferredBridgeStableID(bridge.stableID) + self.lastDiscoveredBridgeStableID = bridge.stableID + BridgeSettingsStore.saveLastDiscoveredBridgeStableID(bridge.stableID) defer { self.connectingBridgeID = nil } do { diff --git a/docs/ios/connect.md b/docs/ios/connect.md index 8917a3b25..c4aa856a7 100644 --- a/docs/ios/connect.md +++ b/docs/ios/connect.md @@ -54,13 +54,13 @@ More debugging notes: `docs/bonjour.md`. In Iris: - Pick the discovered bridge (or hit refresh). - If not paired yet, Iris will initiate pairing automatically. -- After the first successful pairing, Iris will auto-reconnect to the **last bridge** on launch (including after reinstall), as long as the iOS Keychain entry is still present. +- After the first successful pairing, Iris will auto-reconnect **strictly to the last discovered gateway** on launch (including after reinstall), as long as the iOS Keychain entry is still present. ### Connection indicator (always visible) The Settings tab icon shows a small status dot: - **Green**: connected to the bridge -- **Yellow**: connecting +- **Yellow**: connecting (subtle pulse) - **Red**: not connected / error ## 4) Approve pairing (CLI)