Sesame-iOS/Sesame/ContentView.swift
Christoph Hagen 0e0166f308 Clean UI
2022-04-13 14:55:47 +02:00

205 lines
6.0 KiB
Swift

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))
}
}