diff --git a/Sources/InkletMac/Models/AppModel.swift b/Sources/InkletMac/Models/AppModel.swift index 0751db3..6ea6bfb 100644 --- a/Sources/InkletMac/Models/AppModel.swift +++ b/Sources/InkletMac/Models/AppModel.swift @@ -318,10 +318,10 @@ final class AppModel { return devices.first { $0.id == device.id } ?? device } - /// Claims a hardware display with the bind URI it handed over during - /// Bluetooth setup, and puts it at the top of the list. - func bindDisplay(uri: String) async throws -> Device { - guard let credential = DisplayBindCredential(uri: uri) else { + /// Claims a hardware display with the credential it handed over during + /// Bluetooth setup (`inklet://bind?…`), and puts it at the top of the list. + func bindDisplay(credential raw: String) async throws -> Device { + guard let credential = DisplayBindCredential(uri: raw) else { throw DisplayBindError.invalidCredential } let dto: DeviceDTO diff --git a/Sources/InkletMac/Views/PairDisplayView.swift b/Sources/InkletMac/Views/PairDisplayView.swift index 1ff5b4c..66181ed 100644 --- a/Sources/InkletMac/Views/PairDisplayView.swift +++ b/Sources/InkletMac/Views/PairDisplayView.swift @@ -35,7 +35,7 @@ struct PairDisplayView: View { .background(Ink.bg) .navigationTitle("Pair Display") .onAppear { - flow.bindHandler = { uri in try await model.bindDisplay(uri: uri).id } + flow.bindHandler = { credential in try await model.bindDisplay(credential: credential).id } flow.start() } .onDisappear { flow.stop() } diff --git a/Sources/InkletSetupKit/SetupFlow.swift b/Sources/InkletSetupKit/SetupFlow.swift index 3d79f50..5ccda2b 100644 --- a/Sources/InkletSetupKit/SetupFlow.swift +++ b/Sources/InkletSetupKit/SetupFlow.swift @@ -7,7 +7,7 @@ import Foundation /// pick display (or scan its QR) → enter the code on its screen → pick the /// Wi-Fi it should join → wait for it to join → claim it for this account. /// -/// Claiming uses the display's bind credential (the `inklet://bind?…` URI its +/// Claiming uses the display's credential (the `inklet://bind?…` URI its /// NFC tag carries), handed over inside the code-secured session; the app /// supplies `bindHandler` to post it to `POST /api/devices/bind/nfc`. @MainActor @@ -41,7 +41,7 @@ public final class InkletSetupFlow: ObservableObject { /// next action. @Published public var errorMessage: String? - /// Claims the display with its bind URI; returns the new device id if known. + /// Claims the display with its credential; returns the new device id if known. public var bindHandler: (@MainActor (String) async throws -> String?)? private var session: InkletSetupSession? @@ -138,13 +138,13 @@ public final class InkletSetupFlow: ObservableObject { self.session = nil central.stopScanning() - guard let uri = info?.bind, !uri.isEmpty, let bindHandler else { + guard let credential = info?.credential, !credential.isEmpty, let bindHandler else { step = .finished(.wifiOnly) return } step = .binding do { - step = .finished(.bound(deviceID: try await bindHandler(uri))) + step = .finished(.bound(deviceID: try await bindHandler(credential))) } catch { let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription step = .finished(.bindFailed(message)) diff --git a/Sources/InkletSetupKit/SetupModels.swift b/Sources/InkletSetupKit/SetupModels.swift index 9b0b9a4..039fc4d 100644 --- a/Sources/InkletSetupKit/SetupModels.swift +++ b/Sources/InkletSetupKit/SetupModels.swift @@ -29,14 +29,14 @@ public struct InkletWiFiNetwork: Identifiable, Hashable, Sendable { } } -/// What the display reports about itself over the secured session. `bind` is -/// the display's claim credential — the same `inklet://bind?v=2&…` URI its NFC -/// tag carries — when the factory issued one. +/// What the display reports about itself over the secured session. +/// `credential` is its claim credential — `inklet://bind?v=2&…`, the same one +/// its NFC tag carries — when it has been issued one. public struct InkletDisplayInfo: Decodable, Equatable, Sendable { public let hwId: String public let thingName: String public let firmware: String? - public let bind: String? + public let credential: String? } /// The setup screen's QR code: Espressif's standard provisioning JSON, diff --git a/Tests/InkletSetupKitTests/SetupKitTests.swift b/Tests/InkletSetupKitTests/SetupKitTests.swift index 54de05d..5aa7f85 100644 --- a/Tests/InkletSetupKitTests/SetupKitTests.swift +++ b/Tests/InkletSetupKitTests/SetupKitTests.swift @@ -91,3 +91,13 @@ private func hex(_ s: String) -> Data { #expect(InkletSetupQR("inklet://bind?v=2") == nil) #expect(InkletSetupQR(#"{"name":"INKLET-2078","pop":"X","transport":"softap"}"#) == nil) } + +// The display hands its claim credential over as "credential"; absent means +// none was issued. +@Test func deviceInfoReadsTheCredential() throws { + let uri = "inklet://bind?v=2&kid=nfc-master-2026-01&hw=92be8ee85ee74c7b9ea7de00b6eb3801&cv=1&p=fsArTXVRLZ4Oc9EqH-zb0w" + let issued = #"{"hwId":"92be","thingName":"inklet-92be","credential":"\#(uri)"}"# + let none = #"{"hwId":"92be","thingName":"inklet-92be","firmware":"x"}"# + #expect(try JSONDecoder().decode(InkletDisplayInfo.self, from: Data(issued.utf8)).credential == uri) + #expect(try JSONDecoder().decode(InkletDisplayInfo.self, from: Data(none.utf8)).credential == nil) +}