Sesame-iOS/Sesame/ContentView.swift

253 lines
8.0 KiB
Swift
Raw Normal View History

2022-01-29 18:59:42 +01:00
import SwiftUI
import CryptoKit
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
struct ContentView: View {
2022-04-09 17:43:33 +02:00
@AppStorage("counter")
var nextMessageCounter: Int = 0
2022-05-02 17:27:56 +02:00
@AppStorage("compensate")
var isCompensatingDaylightTime: Bool = false
@State
var keyManager = KeyManagement()
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
@State
private var showKeySheet = false
@State
private var showHistorySheet = false
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) :
.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
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) {
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("Keys", action: { showKeySheet = 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()
}
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)
.frame(width: buttonWidth,
height: buttonWidth)
2022-04-09 17:43:33 +02:00
.background(buttonBackground)
.cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
.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
}
.background(state.color)
2022-04-09 17:43:33 +02:00
.onAppear {
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)
.sheet(isPresented: $showKeySheet) {
2022-05-02 17:27:56 +02:00
KeyView(keyManager: $keyManager, isCompensatingDaylightTime: $isCompensatingDaylightTime)
}
.sheet(isPresented: $showHistorySheet) {
HistoryView(manager: history)
}
2022-04-09 17:43:33 +02:00
}
.preferredColorScheme(.dark)
2022-01-29 18:59:42 +01:00
}
func mainButtonPressed() {
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-04-09 17:43:33 +02:00
let count = UInt32(nextMessageCounter)
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)
let historyItem = HistoryItem(sent: message, date: sentTime)
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 {
let (newState, message) = await server.send(message, authToken: token)
let receivedTime = Date.now
responseTime = receivedTime
2022-01-29 18:59:42 +01:00
state = newState
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message)
process(item: finishedItem)
2022-04-09 17:43:33 +02:00
}
}
private func process(item: HistoryItem) {
guard let message = item.incomingMessage else {
save(historyItem: item)
return
}
guard let key = keyManager.get(.deviceKey) else {
save(historyItem: item.notAuthenticated())
2022-04-09 17:43:33 +02:00
return
}
guard message.isValid(using: key) else {
save(historyItem: item.invalidated())
2022-04-09 17:43:33 +02:00
return
}
nextMessageCounter = Int(message.content.id)
save(historyItem: item)
}
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-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) {
guard let authToken = keyManager.get(.authToken) else {
return
}
2022-04-09 17:43:33 +02:00
guard !hasActiveRequest else {
return
}
hasActiveRequest = true
2022-01-29 18:59:42 +01:00
Task {
let newState = await server.deviceStatus(authToken: authToken.data)
2022-04-09 17:43:33 +02:00
hasActiveRequest = false
switch state {
case .noKeyAvailable:
return
case .requestingStatus, .deviceNotAvailable, .ready:
state = newState
case .waitingForResponse:
return
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
// 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))
}
}