import SwiftUI import CryptoKit struct ContentView: View { @AppStorage("server") var serverPath: String = "https://christophhagen.de/sesame/" @AppStorage("localIP") var localAddress: String = "192.168.178.104/" @AppStorage("counter") var nextMessageCounter: Int = 0 @AppStorage("compensate") var isCompensatingDaylightTime: Bool = false @AppStorage("local") private var useLocalConnection = false @AppStorage("deviceID") private var deviceID: Int = 0 @ObservedObject var keyManager = KeyManagement() let history = HistoryManager() @State var state: ClientState = .noKeyAvailable @State private var timer: Timer? @State private var hasActiveRequest = false @State private var responseTime: Date? = nil @State private var showSettingsSheet = false @State private var showHistorySheet = false @State private var didShowKeySheetOnce = false let server = Client() var compensationTime: UInt32 { isCompensatingDaylightTime ? 3600 : 0 } var isPerformingRequests: Bool { hasActiveRequest || state == .waitingForResponse } var buttonBackground: Color { state.allowsAction ? .white.opacity(0.2) : .black.opacity(0.2) } let buttonBorderWidth: CGFloat = 3 var buttonColor: Color { state.allowsAction ? .white : .gray } private let buttonWidth: CGFloat = 250 private let smallButtonHeight: CGFloat = 50 private let smallButtonWidth: CGFloat = 120 private let smallButtonBorderWidth: CGFloat = 1 var body: some View { GeometryReader { geo in VStack(spacing: 20) { HStack { Button("History", action: { showHistorySheet = true }) .frame(width: smallButtonWidth, height: smallButtonHeight) .background(.white.opacity(0.2)) .cornerRadius(smallButtonHeight / 2) .overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white)) .foregroundColor(.white) .font(.title2) .padding() Spacer() Button("Settings", action: { showSettingsSheet = true }) .frame(width: smallButtonWidth, height: smallButtonHeight) .background(.white.opacity(0.2)) .cornerRadius(smallButtonHeight / 2) .overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white)) .foregroundColor(.white) .font(.title2) .padding() } Spacer() if state.requiresDescription { Text(state.description) .padding() } Button(state.actionText, action: mainButtonPressed) .frame(width: buttonWidth, height: buttonWidth) .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) } .background(state.color) .onAppear { if keyManager.hasAllKeys { state = .requestingStatus } startRegularStatusUpdates() } .onDisappear { endRegularStatusUpdates() } .frame(width: geo.size.width, height: geo.size.height) .animation(.easeInOut, value: state.color) .sheet(isPresented: $showSettingsSheet) { SettingsView( keyManager: keyManager, serverAddress: $serverPath, localAddress: $localAddress, deviceID: $deviceID, nextMessageCounter: $nextMessageCounter, isCompensatingDaylightTime: $isCompensatingDaylightTime, useLocalConnection: $useLocalConnection) } .sheet(isPresented: $showHistorySheet) { HistoryView(history: history) } } .preferredColorScheme(.dark) } func mainButtonPressed() { guard let key = keyManager.get(.remoteKey), let token = keyManager.get(.authToken)?.data, let deviceId = UInt8(exactly: deviceID) else { return } let count = UInt32(nextMessageCounter) let sentTime = Date() // Add time to compensate that the device is using daylight savings time let content = Message.Content( time: sentTime.timestamp + compensationTime, id: count, device: deviceId) let message = content.authenticate(using: key) let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection) state = .waitingForResponse print("Sending message \(count)") Task { let (newState, responseMessage) = await send(message, authToken: token) let receivedTime = Date.now responseTime = receivedTime state = newState let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content) guard let key = keyManager.get(.deviceKey) else { save(historyItem: finishedItem.notAuthenticated()) return } guard let responseMessage else { save(historyItem: finishedItem) return } guard responseMessage.isValid(using: key) else { save(historyItem: finishedItem.invalidated()) return } nextMessageCounter = Int(responseMessage.content.id) save(historyItem: finishedItem) } } private func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) { if useLocalConnection { return await server.sendMessageOverLocalNetwork(message, server: localAddress) } else { return await server.send(message, server: serverPath, authToken: authToken) } } private func save(historyItem: HistoryItem) { do { try history.save(item: historyItem) } catch { print("Failed to save item: \(error)") } } 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 !useLocalConnection else { return } guard let authToken = keyManager.get(.authToken) else { if !didShowKeySheetOnce { didShowKeySheetOnce = true //showSettingsSheet = true } return } guard !hasActiveRequest else { return } hasActiveRequest = true Task { let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath) hasActiveRequest = false switch state { case .noKeyAvailable: return case .requestingStatus, .deviceNotAvailable, .ready: state = newState case .waitingForResponse: return case .messageRejected, .openSesame, .internalError, .responseRejected: guard let time = responseTime else { state = newState return } responseTime = nil // Wait at least 5 seconds after these states have been reached before changing the // interface to allow sufficient time to see the result 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 } } } } } 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)) } }