feat(ios): reconnect to last discovered gateway
This commit is contained in:
@@ -13,6 +13,7 @@ final class BridgeConnectionController: ObservableObject {
|
|||||||
private weak var appModel: NodeAppModel?
|
private weak var appModel: NodeAppModel?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var didAutoConnect = false
|
private var didAutoConnect = false
|
||||||
|
private var seenStableIDs = Set<String>()
|
||||||
|
|
||||||
init(appModel: NodeAppModel) {
|
init(appModel: NodeAppModel) {
|
||||||
self.appModel = appModel
|
self.appModel = appModel
|
||||||
@@ -23,6 +24,7 @@ final class BridgeConnectionController: ObservableObject {
|
|||||||
.sink { [weak self] newValue in
|
.sink { [weak self] newValue in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.bridges = newValue
|
self.bridges = newValue
|
||||||
|
self.updateLastDiscoveredBridge(from: newValue)
|
||||||
self.maybeAutoConnect()
|
self.maybeAutoConnect()
|
||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
@@ -50,9 +52,9 @@ final class BridgeConnectionController: ObservableObject {
|
|||||||
guard appModel.bridgeServerName == nil else { return }
|
guard appModel.bridgeServerName == nil else { return }
|
||||||
|
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
let preferredStableID = defaults.string(forKey: "bridge.preferredStableID")?
|
let targetStableID = defaults.string(forKey: "bridge.lastDiscoveredStableID")?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
guard !preferredStableID.isEmpty else { return }
|
guard !targetStableID.isEmpty else { return }
|
||||||
|
|
||||||
let instanceId = defaults.string(forKey: "node.instanceId")?
|
let instanceId = defaults.string(forKey: "node.instanceId")?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
@@ -64,12 +66,20 @@ final class BridgeConnectionController: ObservableObject {
|
|||||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
guard !token.isEmpty else { return }
|
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
|
self.didAutoConnect = true
|
||||||
appModel.connectToBridge(endpoint: target.endpoint, hello: self.makeHello(token: token))
|
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 {
|
private func makeHello(token: String) -> BridgeHello {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
let nodeId = defaults.string(forKey: "node.instanceId") ?? "ios-node"
|
let nodeId = defaults.string(forKey: "node.instanceId") ?? "ios-node"
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ enum BridgeSettingsStore {
|
|||||||
|
|
||||||
private static let instanceIdDefaultsKey = "node.instanceId"
|
private static let instanceIdDefaultsKey = "node.instanceId"
|
||||||
private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID"
|
private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID"
|
||||||
|
private static let lastDiscoveredBridgeStableIDDefaultsKey = "bridge.lastDiscoveredStableID"
|
||||||
|
|
||||||
private static let instanceIdAccount = "instanceId"
|
private static let instanceIdAccount = "instanceId"
|
||||||
private static let preferredBridgeStableIDAccount = "preferredStableID"
|
private static let preferredBridgeStableIDAccount = "preferredStableID"
|
||||||
|
private static let lastDiscoveredBridgeStableIDAccount = "lastDiscoveredStableID"
|
||||||
|
|
||||||
static func bootstrapPersistence() {
|
static func bootstrapPersistence() {
|
||||||
self.ensureStableInstanceID()
|
self.ensureStableInstanceID()
|
||||||
self.ensurePreferredBridgeStableID()
|
self.ensurePreferredBridgeStableID()
|
||||||
|
self.ensureLastDiscoveredBridgeStableID()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func loadStableInstanceID() -> String? {
|
static func loadStableInstanceID() -> String? {
|
||||||
@@ -36,6 +39,18 @@ enum BridgeSettingsStore {
|
|||||||
account: self.preferredBridgeStableIDAccount)
|
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() {
|
private static func ensureStableInstanceID() {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
|
||||||
@@ -76,4 +91,22 @@ enum BridgeSettingsStore {
|
|||||||
defaults.set(stored, forKey: self.preferredBridgeStableIDDefaultsKey)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct SettingsTab: View {
|
|||||||
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
||||||
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
||||||
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
|
||||||
|
@AppStorage("bridge.lastDiscoveredStableID") private var lastDiscoveredBridgeStableID: String = ""
|
||||||
@StateObject private var connectStatus = ConnectStatusStore()
|
@StateObject private var connectStatus = ConnectStatusStore()
|
||||||
@State private var connectingBridgeID: String?
|
@State private var connectingBridgeID: String?
|
||||||
@State private var localIPAddress: String?
|
@State private var localIPAddress: String?
|
||||||
@@ -207,6 +208,8 @@ struct SettingsTab: View {
|
|||||||
self.connectingBridgeID = bridge.id
|
self.connectingBridgeID = bridge.id
|
||||||
self.preferredBridgeStableID = bridge.stableID
|
self.preferredBridgeStableID = bridge.stableID
|
||||||
BridgeSettingsStore.savePreferredBridgeStableID(bridge.stableID)
|
BridgeSettingsStore.savePreferredBridgeStableID(bridge.stableID)
|
||||||
|
self.lastDiscoveredBridgeStableID = bridge.stableID
|
||||||
|
BridgeSettingsStore.saveLastDiscoveredBridgeStableID(bridge.stableID)
|
||||||
defer { self.connectingBridgeID = nil }
|
defer { self.connectingBridgeID = nil }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ More debugging notes: `docs/bonjour.md`.
|
|||||||
In Iris:
|
In Iris:
|
||||||
- Pick the discovered bridge (or hit refresh).
|
- Pick the discovered bridge (or hit refresh).
|
||||||
- If not paired yet, Iris will initiate pairing automatically.
|
- 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)
|
### Connection indicator (always visible)
|
||||||
|
|
||||||
The Settings tab icon shows a small status dot:
|
The Settings tab icon shows a small status dot:
|
||||||
- **Green**: connected to the bridge
|
- **Green**: connected to the bridge
|
||||||
- **Yellow**: connecting
|
- **Yellow**: connecting (subtle pulse)
|
||||||
- **Red**: not connected / error
|
- **Red**: not connected / error
|
||||||
|
|
||||||
## 4) Approve pairing (CLI)
|
## 4) Approve pairing (CLI)
|
||||||
|
|||||||
Reference in New Issue
Block a user