import SwiftUI import CryptoKit let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!) struct ContentView: View { @AppStorage("counter") var nextMessageCounter: Int = 0 @State var state: ClientState = .noKeyAvailable @State private var timer: Timer? @State private var hasActiveRequest = false @State private var responseTime: Date? = nil var isPerformingRequests: Bool { hasActiveRequest || state == .waitingForResponse } var buttonBackground: Color { state.allowsAction ? .white.opacity(0.2) : .gray.opacity(0.2) } let buttonBorderWidth: CGFloat = 3 var buttonColor: Color { state.allowsAction ? .white : .gray } private let buttonWidth: CGFloat = 250 var body: some View { GeometryReader { geo in VStack(spacing: 20) { Spacer() if state.requiresDescription { Text(state.description) .padding() } Button(state.actionText, action: mainButtonPressed) .frame(width: buttonWidth, height: buttonWidth, alignment: .center) .background(buttonBackground) .cornerRadius(buttonWidth / 2) .overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor)) .foregroundColor(buttonColor) .font(.title) .disabled(!state.allowsAction) .padding(.bottom, (geo.size.width-buttonWidth) / 2) } .onAppear { if KeyManagement.hasKey { state = .requestingStatus } startRegularStatusUpdates() } .onDisappear { endRegularStatusUpdates() } .frame(width: geo.size.width, height: geo.size.height) .background(state.color) .animation(.easeInOut, value: state.color) } } func mainButtonPressed() { guard let key = KeyManagement.key?.remote else { generateKey() return } sendMessage(using: key) } func sendMessage(using key: SymmetricKey) { let count = UInt32(nextMessageCounter) let now = Date() let content = Message.Content( time: now.timestamp, id: count) let message = content.authenticate(using: key) state = .waitingForResponse print("Sending message \(count)") Task { let (newState, message) = try await server.send(message) responseTime = now state = newState if let message = message { processResponse(message, sendTime: now) } } } private func processResponse(_ message: Message, sendTime: Date) { guard let key = KeyManagement.key?.device else { return } guard message.isValid(using: key) else { return } nextMessageCounter = Int(message.content.id) print("Next counter is \(message.content.id)") let now = Date() let total = now.timeIntervalSince(sendTime) print("Total time: \(Int(total * 1000)) ms") let deviceTime = Date(timestamp: message.content.time) let time1 = deviceTime.timeIntervalSince(sendTime) let time2 = now.timeIntervalSince(deviceTime) if time1 < 0 { print("Device time behind by at least \(Int(-time1 * 1000)) ms behind") } else if time2 < 0 { print("Device time behind by at least \(Int(-time2 * 1000)) ms ahead") } else { print("Device time synchronized") } } private func startRegularStatusUpdates() { guard timer == nil else { return } DispatchQueue.main.async { timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus) timer!.fire() } } private func endRegularStatusUpdates() { timer?.invalidate() timer = nil } func checkDeviceStatus(_ timer: Timer) { guard !hasActiveRequest else { return } hasActiveRequest = true print("Checking device status") Task { let newState = await server.deviceStatus() hasActiveRequest = false switch state { case .noKeyAvailable: return case .requestingStatus, .deviceNotAvailable, .ready: state = newState case .waitingForResponse: return case .messageRejected, .openSesame, .internalError: guard let time = responseTime else { state = newState return } responseTime = nil // Wait at least 5 seconds after these states have been reached before changing the let elapsed = Date.now.timeIntervalSince(time) guard elapsed < 5 else { state = newState return } let secondsToWait = Int(elapsed.rounded(.up)) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) { state = newState } } } } func generateKey() { print("Regenerate key") KeyManagement.generateNewKeys() state = .requestingStatus } func shareKey() { } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .previewDevice("iPhone 8") } } extension Date { var timestamp: UInt32 { UInt32(timeIntervalSince1970.rounded()) } init(timestamp: UInt32) { self.init(timeIntervalSince1970: TimeInterval(timestamp)) } }