import SwiftUI import SFSafeSymbols import CryptoKit struct ContentView: View { @Binding var didLaunchFromComplication: Bool @AppStorage("connectionType") var connectionType: ConnectionStrategy = .remoteFirst @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("deviceId") private var deviceId: Int = 0 @EnvironmentObject var keyManager: KeyManagement @EnvironmentObject var history: HistoryManager @State var state: ClientState = .noKeyAvailable @State var stateResetTimer: Timer? let server = Client() private var firstTryIsLocalConnection: Bool { switch connectionType { case .local, .localFirst: return true case .remote, .remoteFirst: return false } } private var hasSecondTry: Bool { switch connectionType { case .localFirst, .remoteFirst: return true default: return false } } private var secondTryIsLocalConnection: Bool { switch connectionType { case .local, .localFirst: return false case .remote, .remoteFirst: return true } } var buttonBackground: Color { state.allowsAction ? .white.opacity(0.2) : .black.opacity(0.2) } var buttonColor: Color { state.allowsAction ? .white : .gray } var body: some View { HStack { Spacer() VStack(alignment: .center) { Image(systemSymbol: .lock) .resizable() .aspectRatio(contentMode: .fit) .fontWeight(.ultraLight) .padding() .onTapGesture(perform: mainButtonPressed) .disabled(!state.allowsAction) if state == .waitingForResponse { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .frame(width: 20, height: 20) } else { Text(state.actionText) .font(.subheadline) } } Spacer() } .background(state.color) .animation(.easeInOut, value: state.color) .onAppear { if state == .noKeyAvailable, keyManager.hasAllKeys { state = .ready } } .onChange(of: didLaunchFromComplication) { launched in guard launched else { return } didLaunchFromComplication = false mainButtonPressed() } } func mainButtonPressed() { guard let keys = keyManager.getAllKeys(), let deviceId = UInt8(exactly: deviceId) else { return } sendMessage(from: deviceId, using: keys, isFirstTry: true) } private func sendMessage(from deviceId: UInt8, using keys: KeySet, isFirstTry: Bool) { preventStateReset() state = .waitingForResponse let localConnection = isFirstTry ? firstTryIsLocalConnection : secondTryIsLocalConnection Task { let response = await send( count: UInt32(nextMessageCounter), from: deviceId, using: keys, to: server, over: localConnection, while: isCompensatingDaylightTime, localAddress: localAddress, remoteAddress: serverPath) DispatchQueue.main.async { state = response.response scheduleStateReset() if let counter = response.responseMessage?.id { nextMessageCounter = Int(counter) } } save(historyItem: response) guard isFirstTry, hasSecondTry else { return } DispatchQueue.main.async { sendMessage(from: deviceId, using: keys, isFirstTry: false) } } } private func preventStateReset() { stateResetTimer?.invalidate() stateResetTimer = nil } private func scheduleStateReset() { stateResetTimer?.invalidate() stateResetTimer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false) { _ in DispatchQueue.main.async { resetState() } } } private func resetState() { state = keyManager.hasAllKeys ? .ready : .noKeyAvailable preventStateReset() } private func save(historyItem: HistoryItem) { do { try history.save(item: historyItem) } catch { print("Failed to save item: \(error)") } } } private func send(count: UInt32, from deviceId: UInt8, using keys: KeySet, to server: Client, over localConnection: Bool, while compensatingTime: Bool, localAddress: String, remoteAddress: String) async -> HistoryItem { let sentTime = Date() // Add time to compensate that the device is using daylight savings time let timeCompensation: UInt32 = compensatingTime ? 3600 : 0 let content = Message.Content( time: sentTime.timestamp + timeCompensation, id: count, device: deviceId) let message = content.authenticate(using: keys.remote) print("Sending message \(count)") let address = localConnection ? localAddress : remoteAddress let (newState, responseMessage) = await send(message, to: server, using: keys.server, local: localConnection, address: address) var historyItem = HistoryItem( sent: message.content, sentDate: sentTime, local: localConnection, response: newState, responseDate: .now, responseMessage: responseMessage?.content) guard let responseMessage else { return historyItem } guard responseMessage.isValid(using: keys.device) else { historyItem.response = .responseRejected(.invalidAuthentication) return historyItem } return historyItem } private func send(_ message: Message, to server: Client, using authToken: Data, local: Bool, address: String) async -> (state: ClientState, response: Message?) { if local { return await server.sendMessageOverLocalNetwork(message, server: address) } else { return await server.send(message, server: address, authToken: authToken) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(didLaunchFromComplication: .constant(false)) .environmentObject(KeyManagement()) .environmentObject(HistoryManager()) } }