import Foundation import SwiftUI import SFSafeSymbols 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 invalidDeviceId case invalidCounter case invalidTime case invalidAuthentication case timeout case missingKey } extension RejectionCause: CustomStringConvertible { var description: String { switch self { case .invalidDeviceId: return "Invalid device ID" case .invalidCounter: return "Invalid counter" case .invalidTime: return "Invalid time" case .invalidAuthentication: return "Invalid authentication" case .timeout: return "Device not responding" case .missingKey: return "No key to verify message" } } } 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) case responseRejected(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 .messageDeviceInvalid: self = .messageRejected(.invalidDeviceId) case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication: print("Unexpected internal error: \(keyResult)") 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, .responseRejected: return true default: return false } } var color: Color { switch self { case .noKeyAvailable: return Color(red: 50/255, green: 50/255, blue: 50/255) case .deviceNotAvailable: return Color(red: 150/255, green: 90/255, blue: 90/255) case .messageRejected, .responseRejected: return Color(red: 160/255, green: 30/255, blue: 30/255) case .internalError: return Color(red: 100/255, green: 0/255, blue: 0/255) case .ready: return Color(red: 115/255, green: 140/255, blue: 90/255) case .requestingStatus, .waitingForResponse: return Color(red: 160/255, green: 170/255, blue: 110/255) case .openSesame: return Color(red: 65/255, green: 110/255, blue: 60/255) } } var allowsAction: Bool { switch self { case .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)" case .responseRejected(let cause): switch cause { case .invalidAuthentication: return "Device message not authenticated" default: return cause.description } } } } // MARK: Coding extension ClientState { var encoded: Data { Data([code]) } var code: UInt8 { switch self { case .noKeyAvailable: return 1 case .requestingStatus: return 2 case .deviceNotAvailable(let connectionError): switch connectionError { case .serverNotReached: return 3 case .deviceDisconnected: return 4 } case .ready: return 5 case .waitingForResponse: return 6 case .messageRejected(let rejectionCause): switch rejectionCause { case .invalidDeviceId: return 19 case .invalidCounter: return 7 case .invalidTime: return 8 case .invalidAuthentication: return 9 case .timeout: return 10 case .missingKey: return 11 } case .responseRejected(let rejectionCause): switch rejectionCause { case .invalidCounter: return 12 case .invalidTime: return 13 case .invalidAuthentication: return 14 case .timeout: return 15 case .missingKey: return 16 case .invalidDeviceId: return 20 } case .openSesame: return 17 case .internalError: return 18 } } init(code: UInt8) { switch code { case 1: self = .noKeyAvailable case 2: self = .requestingStatus case 3: self = .deviceNotAvailable(.serverNotReached) case 4: self = .deviceNotAvailable(.deviceDisconnected) case 5: self = .ready case 6: self = .waitingForResponse case 7: self = .messageRejected(.invalidCounter) case 8: self = .messageRejected(.invalidTime) case 9: self = .messageRejected(.invalidAuthentication) case 10: self = .messageRejected(.timeout) case 11: self = .messageRejected(.missingKey) case 12: self = .responseRejected(.invalidCounter) case 13: self = .responseRejected(.invalidTime) case 14: self = .responseRejected(.invalidAuthentication) case 15: self = .responseRejected(.timeout) case 16: self = .responseRejected(.missingKey) case 17: self = .openSesame case 18: self = .internalError("") case 19: self = .messageRejected(.invalidDeviceId) case 20: self = .responseRejected(.invalidDeviceId) default: self = .internalError("Unknown code \(code)") } } } extension ClientState { @available(iOS 16, *) var symbol: SFSymbol { switch self { case .deviceNotAvailable: return .wifiExclamationmark case .internalError: return .applewatchSlash case .noKeyAvailable: return .lockTrianglebadgeExclamationmark case .openSesame: return .lockOpen case .messageRejected: return .nosign case .responseRejected: return .exclamationmarkTriangle case .requestingStatus, .ready, .waitingForResponse: return .wifiExclamationmark } } }