2022-01-29 18:59:42 +01:00
|
|
|
import SwiftUI
|
|
|
|
import CryptoKit
|
|
|
|
|
|
|
|
struct ContentView: View {
|
2023-04-11 18:18:31 +02:00
|
|
|
|
|
|
|
@AppStorage("server")
|
|
|
|
var serverPath: String = "https://christophhagen.de/sesame/"
|
|
|
|
|
|
|
|
@AppStorage("localIP")
|
|
|
|
var localAddress: String = "192.168.178.104/"
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
@AppStorage("counter")
|
|
|
|
var nextMessageCounter: Int = 0
|
2022-05-01 14:07:43 +02:00
|
|
|
|
2022-05-02 17:27:56 +02:00
|
|
|
@AppStorage("compensate")
|
|
|
|
var isCompensatingDaylightTime: Bool = false
|
2023-04-11 18:18:31 +02:00
|
|
|
|
|
|
|
@AppStorage("local")
|
|
|
|
private var useLocalConnection = false
|
2022-05-02 17:27:56 +02:00
|
|
|
|
2022-05-01 14:07:43 +02:00
|
|
|
@State
|
|
|
|
var keyManager = KeyManagement()
|
2022-05-01 18:30:30 +02:00
|
|
|
|
|
|
|
let history = HistoryManager()
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
@State
|
|
|
|
var state: ClientState = .noKeyAvailable
|
|
|
|
|
|
|
|
@State
|
|
|
|
private var timer: Timer?
|
|
|
|
|
|
|
|
@State
|
|
|
|
private var hasActiveRequest = false
|
|
|
|
|
|
|
|
@State
|
|
|
|
private var responseTime: Date? = nil
|
|
|
|
|
2022-05-01 14:07:43 +02:00
|
|
|
@State
|
2023-04-11 18:18:31 +02:00
|
|
|
private var showSettingsSheet = false
|
2022-05-01 14:07:43 +02:00
|
|
|
|
|
|
|
@State
|
|
|
|
private var showHistorySheet = false
|
2023-04-11 18:18:31 +02:00
|
|
|
|
|
|
|
@State
|
|
|
|
private var didShowKeySheetOnce = false
|
|
|
|
|
|
|
|
let server = Client()
|
2022-05-01 14:07:43 +02:00
|
|
|
|
2022-05-02 17:27:56 +02:00
|
|
|
var compensationTime: UInt32 {
|
|
|
|
isCompensatingDaylightTime ? 3600 : 0
|
|
|
|
}
|
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
var isPerformingRequests: Bool {
|
2022-04-09 17:43:33 +02:00
|
|
|
hasActiveRequest ||
|
|
|
|
state == .waitingForResponse
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
var buttonBackground: Color {
|
2022-04-13 14:55:47 +02:00
|
|
|
state.allowsAction ?
|
2022-04-09 17:43:33 +02:00
|
|
|
.white.opacity(0.2) :
|
2022-05-01 14:07:43 +02:00
|
|
|
.black.opacity(0.2)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
let buttonBorderWidth: CGFloat = 3
|
|
|
|
|
|
|
|
var buttonColor: Color {
|
2022-04-13 14:55:47 +02:00
|
|
|
state.allowsAction ? .white : .gray
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
|
2022-04-13 14:55:47 +02:00
|
|
|
private let buttonWidth: CGFloat = 250
|
2022-04-09 17:43:33 +02:00
|
|
|
|
2022-05-01 14:07:43 +02:00
|
|
|
private let smallButtonHeight: CGFloat = 50
|
|
|
|
|
|
|
|
private let smallButtonWidth: CGFloat = 120
|
|
|
|
|
|
|
|
private let smallButtonBorderWidth: CGFloat = 1
|
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
var body: some View {
|
2022-04-09 17:43:33 +02:00
|
|
|
GeometryReader { geo in
|
|
|
|
VStack(spacing: 20) {
|
2022-05-01 14:07:43 +02:00
|
|
|
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()
|
2023-04-11 18:18:31 +02:00
|
|
|
Button("Settings", action: { showSettingsSheet = true })
|
2022-05-01 14:07:43 +02:00
|
|
|
.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()
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
Spacer()
|
2022-04-13 14:55:47 +02:00
|
|
|
if state.requiresDescription {
|
2022-04-09 17:43:33 +02:00
|
|
|
Text(state.description)
|
|
|
|
.padding()
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-04-13 14:55:47 +02:00
|
|
|
Button(state.actionText, action: mainButtonPressed)
|
2022-05-01 14:07:43 +02:00
|
|
|
.frame(width: buttonWidth,
|
|
|
|
height: buttonWidth)
|
2022-04-09 17:43:33 +02:00
|
|
|
.background(buttonBackground)
|
|
|
|
.cornerRadius(buttonWidth / 2)
|
2023-04-11 18:18:31 +02:00
|
|
|
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
|
|
|
|
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
2022-04-09 17:43:33 +02:00
|
|
|
.foregroundColor(buttonColor)
|
|
|
|
.font(.title)
|
2022-04-13 14:55:47 +02:00
|
|
|
.disabled(!state.allowsAction)
|
|
|
|
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-05-01 14:07:43 +02:00
|
|
|
.background(state.color)
|
2022-04-09 17:43:33 +02:00
|
|
|
.onAppear {
|
2022-05-01 14:07:43 +02:00
|
|
|
if keyManager.hasAllKeys {
|
2022-04-09 17:43:33 +02:00
|
|
|
state = .requestingStatus
|
|
|
|
}
|
|
|
|
startRegularStatusUpdates()
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
.onDisappear {
|
|
|
|
endRegularStatusUpdates()
|
|
|
|
}
|
|
|
|
.frame(width: geo.size.width, height: geo.size.height)
|
2022-04-13 14:55:47 +02:00
|
|
|
.animation(.easeInOut, value: state.color)
|
2023-04-11 18:18:31 +02:00
|
|
|
.sheet(isPresented: $showSettingsSheet) {
|
|
|
|
SettingsView(
|
|
|
|
keyManager: $keyManager,
|
|
|
|
serverAddress: $serverPath,
|
|
|
|
localAddress: $localAddress,
|
|
|
|
isCompensatingDaylightTime: $isCompensatingDaylightTime,
|
|
|
|
useLocalConnection: $useLocalConnection)
|
2022-05-01 14:07:43 +02:00
|
|
|
}
|
2022-05-01 18:30:30 +02:00
|
|
|
.sheet(isPresented: $showHistorySheet) {
|
|
|
|
HistoryView(manager: history)
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
2022-05-01 18:30:30 +02:00
|
|
|
.preferredColorScheme(.dark)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func mainButtonPressed() {
|
2022-05-01 14:07:43 +02:00
|
|
|
guard let key = keyManager.get(.remoteKey),
|
|
|
|
let token = keyManager.get(.authToken)?.data else {
|
2022-04-09 17:43:33 +02:00
|
|
|
return
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2022-05-01 14:07:43 +02:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
let count = UInt32(nextMessageCounter)
|
2022-05-01 18:30:30 +02:00
|
|
|
let sentTime = Date()
|
2022-05-02 17:27:56 +02:00
|
|
|
// Add time to compensate that the device is using daylight savings time
|
2022-04-09 17:43:33 +02:00
|
|
|
let content = Message.Content(
|
2022-05-02 17:27:56 +02:00
|
|
|
time: sentTime.timestamp + compensationTime,
|
2022-04-09 17:43:33 +02:00
|
|
|
id: count)
|
|
|
|
let message = content.authenticate(using: key)
|
2023-04-11 18:18:31 +02:00
|
|
|
let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
|
2022-01-29 18:59:42 +01:00
|
|
|
state = .waitingForResponse
|
2022-04-09 17:43:33 +02:00
|
|
|
print("Sending message \(count)")
|
2022-01-29 18:59:42 +01:00
|
|
|
Task {
|
2023-04-11 18:18:31 +02:00
|
|
|
let (newState, responseMessage) = await send(message, authToken: token)
|
2022-05-01 18:30:30 +02:00
|
|
|
let receivedTime = Date.now
|
|
|
|
responseTime = receivedTime
|
2022-01-29 18:59:42 +01:00
|
|
|
state = newState
|
2023-04-11 18:18:31 +02:00
|
|
|
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)
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
}
|
2023-04-11 18:18:31 +02:00
|
|
|
|
|
|
|
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)
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
2022-05-01 18:30:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private func save(historyItem: HistoryItem) {
|
|
|
|
do {
|
|
|
|
try history.save(item: historyItem)
|
|
|
|
} catch {
|
|
|
|
print("Failed to save item: \(error)")
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-01 18:30:30 +02:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
private func startRegularStatusUpdates() {
|
|
|
|
guard timer == nil else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
|
|
|
|
timer!.fire()
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
private func endRegularStatusUpdates() {
|
|
|
|
timer?.invalidate()
|
|
|
|
timer = nil
|
|
|
|
}
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2022-04-09 17:43:33 +02:00
|
|
|
func checkDeviceStatus(_ timer: Timer) {
|
2023-04-11 18:18:31 +02:00
|
|
|
guard !useLocalConnection else {
|
|
|
|
return
|
|
|
|
}
|
2022-05-01 14:07:43 +02:00
|
|
|
guard let authToken = keyManager.get(.authToken) else {
|
2023-04-11 18:18:31 +02:00
|
|
|
if !didShowKeySheetOnce {
|
|
|
|
didShowKeySheetOnce = true
|
|
|
|
//showSettingsSheet = true
|
|
|
|
}
|
2022-05-01 14:07:43 +02:00
|
|
|
return
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
guard !hasActiveRequest else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
hasActiveRequest = true
|
2022-01-29 18:59:42 +01:00
|
|
|
Task {
|
2023-04-11 18:18:31 +02:00
|
|
|
let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath)
|
2022-04-09 17:43:33 +02:00
|
|
|
hasActiveRequest = false
|
|
|
|
switch state {
|
|
|
|
case .noKeyAvailable:
|
|
|
|
return
|
|
|
|
case .requestingStatus, .deviceNotAvailable, .ready:
|
|
|
|
state = newState
|
|
|
|
case .waitingForResponse:
|
|
|
|
return
|
2022-05-01 18:30:30 +02:00
|
|
|
case .messageRejected, .openSesame, .internalError, .responseRejected:
|
2022-04-09 17:43:33 +02:00
|
|
|
guard let time = responseTime else {
|
|
|
|
state = newState
|
2022-01-29 18:59:42 +01:00
|
|
|
return
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
|
|
|
responseTime = nil
|
|
|
|
// Wait at least 5 seconds after these states have been reached before changing the
|
2022-05-01 18:30:30 +02:00
|
|
|
// interface to allow sufficient time to see the result
|
2022-04-09 17:43:33 +02:00
|
|
|
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)) {
|
2022-01-29 18:59:42 +01:00
|
|
|
state = newState
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ContentView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
ContentView()
|
|
|
|
.previewDevice("iPhone 8")
|
|
|
|
}
|
|
|
|
}
|
2022-04-09 17:43:33 +02:00
|
|
|
|
|
|
|
extension Date {
|
|
|
|
|
|
|
|
var timestamp: UInt32 {
|
|
|
|
UInt32(timeIntervalSince1970.rounded())
|
|
|
|
}
|
|
|
|
|
|
|
|
init(timestamp: UInt32) {
|
|
|
|
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
|
|
|
}
|
|
|
|
}
|