import Foundation import SwiftUI enum ConnectionError { case serverNotReached case deviceDisconnected } extension ConnectionError: CustomStringConvertible { var description: String { switch self { case .serverNotReached: return "Server unavailable" case .deviceDisconnected: return "Device disconnected" } } } enum RejectionCause { case invalidCounter case invalidTime case invalidAuthentication case timeout } extension RejectionCause: CustomStringConvertible { var description: String { switch self { case .invalidCounter: return "Invalid counter" case .invalidTime: return "Invalid time" case .invalidAuthentication: return "Invalid authentication" case .timeout: return "Device not responding" } } } enum ClientState { /// There is no key stored locally on the client. A new key must be generated before use. case noKeyAvailable /// The device status is being requested case requestingStatus /// The remote device is not connected (no socket opened) case deviceNotAvailable(ConnectionError) /// The device is connected and ready to receive a message case ready /// The message is being transmitted and a response is awaited case waitingForResponse /// The transmitted message was rejected (multiple possible reasons) case messageRejected(RejectionCause) /// The device responded that the opening action was started case openSesame case internalError(String) var canSendKey: Bool { switch self { case .ready, .openSesame, .messageRejected: return true default: return false } } init(keyResult: MessageResult) { switch keyResult { case .messageAuthenticationFailed: self = .messageRejected(.invalidAuthentication) case .messageTimeMismatch: self = .messageRejected(.invalidTime) case .messageCounterInvalid: self = .messageRejected(.invalidCounter) case .deviceTimedOut: self = .messageRejected(.timeout) case .messageAccepted: self = .openSesame case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent: self = .internalError(keyResult.description) case .deviceNotConnected: self = .deviceNotAvailable(.deviceDisconnected) case .operationInProgress: self = .waitingForResponse case .deviceConnected: self = .ready } } var actionText: String { "Unlock" } var requiresDescription: Bool { switch self { case .deviceNotAvailable, .messageRejected, .internalError: return true default: return false } } var color: Color { switch self { case .noKeyAvailable: return .gray case .requestingStatus: return .yellow case .deviceNotAvailable: return Color(red: 1.0, green: 0.6, blue: 0.6) case .messageRejected: return .red case .internalError: return Color(red: 0.7, green: 0, blue: 0) case .ready: return Color(red: 0.7, green: 1.0, blue: 0.5) case .waitingForResponse: return Color(red: 0.9, green: 1.0, blue: 0.5) case .openSesame: return .green } } var allowsAction: Bool { switch self { case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable: return false default: return true } } } extension ClientState: Equatable { } extension ClientState: CustomStringConvertible { var description: String { switch self { case .noKeyAvailable: return "No key set." case .requestingStatus: return "Checking device status" case .deviceNotAvailable(let status): return status.description case .ready: return "Ready" case .waitingForResponse: return "Unlocking..." case .messageRejected(let cause): return cause.description case .openSesame: return "Unlocked" case .internalError(let e): return "Error: \(e)" } } }