Create Apple Watch App
This commit is contained in:
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
Sesame-Watch Watch App/Assets.xcassets/Contents.json
Normal file
6
Sesame-Watch Watch App/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
150
Sesame-Watch Watch App/ContentView.swift
Normal file
150
Sesame-Watch Watch App/ContentView.swift
Normal file
@ -0,0 +1,150 @@
|
||||
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
|
||||
|
||||
@EnvironmentObject
|
||||
var keyManager: KeyManagement
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
|
||||
@State
|
||||
private var hasActiveRequest = false
|
||||
|
||||
let server = Client()
|
||||
|
||||
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 sidePaddingRatio: CGFloat = 0.05
|
||||
private let buttonSizeRatio: CGFloat = 0.9
|
||||
|
||||
private let smallButtonHeight: CGFloat = 50
|
||||
|
||||
private let smallButtonWidth: CGFloat = 120
|
||||
|
||||
private let smallButtonBorderWidth: CGFloat = 1
|
||||
|
||||
var compensationTime: UInt32 {
|
||||
isCompensatingDaylightTime ? 3600 : 0
|
||||
}
|
||||
|
||||
var isPerformingRequests: Bool {
|
||||
hasActiveRequest ||
|
||||
state == .waitingForResponse
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Spacer()
|
||||
GeometryReader { geo in
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
let buttonWidth = min(geo.size.width, geo.size.height)
|
||||
Text(state.actionText)
|
||||
.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)
|
||||
.onTapGesture(perform: mainButtonPressed)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.background(state.color)
|
||||
.animation(.easeInOut, value: state.color)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
.environmentObject(KeyManagement())
|
||||
}
|
||||
}
|
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
extension Date {
|
||||
|
||||
var timestamp: UInt32 {
|
||||
UInt32(timeIntervalSince1970.rounded())
|
||||
}
|
||||
|
||||
init(timestamp: UInt32) {
|
||||
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
}
|
||||
}
|
13
Sesame-Watch Watch App/HistoryView.swift
Normal file
13
Sesame-Watch Watch App/HistoryView.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView()
|
||||
}
|
||||
}
|
121
Sesame-Watch Watch App/KeyManagement.swift
Normal file
121
Sesame-Watch Watch App/KeyManagement.swift
Normal file
@ -0,0 +1,121 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import SwiftUI
|
||||
|
||||
private let localKey: [UInt8] = [
|
||||
0x98, 0x36, 0x91, 0x09, 0x29, 0xa0, 0x54, 0x44,
|
||||
0x03, 0x0c, 0xa5, 0xb4, 0x20, 0x16, 0x10, 0x0d,
|
||||
0xaf, 0x41, 0x9b, 0x26, 0x4f, 0x75, 0xa4, 0x61,
|
||||
0xed, 0x15, 0x0c, 0xb3, 0x06, 0x39, 0x92, 0x59]
|
||||
|
||||
|
||||
private let remoteKey: [UInt8] = [
|
||||
0xfa, 0x23, 0xf6, 0x98, 0xea, 0x87, 0x23, 0xa0,
|
||||
0xa0, 0xbe, 0x9a, 0xdb, 0x31, 0x28, 0xcb, 0x7d,
|
||||
0xd3, 0xa5, 0x7b, 0xf0, 0xc0, 0xeb, 0x45, 0x65,
|
||||
0x4d, 0x94, 0x50, 0x1a, 0x2f, 0x6f, 0xeb, 0x70]
|
||||
|
||||
private let authToken: [UInt8] = {
|
||||
let s = "Y6QzDK5DaFK1w2oEX5OkzoC0nTqP8w5IxpvWAR1mpro="
|
||||
let t = Data(base64Encoded: s.data(using: .utf8)!)!
|
||||
return Array(t)
|
||||
}()
|
||||
|
||||
extension KeyManagement {
|
||||
|
||||
enum KeyType: String, Identifiable, CaseIterable {
|
||||
|
||||
case deviceKey = "sesame-device"
|
||||
case remoteKey = "sesame-remote"
|
||||
case authToken = "sesame-remote-auth"
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .deviceKey:
|
||||
return "Device Key"
|
||||
case .remoteKey:
|
||||
return "Remote Key"
|
||||
case .authToken:
|
||||
return "Authentication Token"
|
||||
}
|
||||
}
|
||||
|
||||
var keyLength: SymmetricKeySize {
|
||||
.bits256
|
||||
}
|
||||
|
||||
var usesHashing: Bool {
|
||||
switch self {
|
||||
case .authToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyManagement.KeyType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
||||
final class KeyManagement: ObservableObject {
|
||||
|
||||
|
||||
@Published
|
||||
private(set) var hasRemoteKey = true
|
||||
|
||||
@Published
|
||||
private(set) var hasDeviceKey = true
|
||||
|
||||
@Published
|
||||
private(set) var hasAuthToken = true
|
||||
|
||||
var hasAllKeys: Bool {
|
||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
func has(_ type: KeyType) -> Bool {
|
||||
switch type {
|
||||
case .deviceKey:
|
||||
return hasDeviceKey
|
||||
case .remoteKey:
|
||||
return hasRemoteKey
|
||||
case .authToken:
|
||||
return hasAuthToken
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ type: KeyType) -> SymmetricKey? {
|
||||
let bytes: [UInt8] = get(type)
|
||||
return SymmetricKey(data: bytes)
|
||||
}
|
||||
|
||||
private func get(_ type: KeyType) -> [UInt8] {
|
||||
switch type {
|
||||
case .deviceKey:
|
||||
return remoteKey
|
||||
case .remoteKey:
|
||||
return localKey
|
||||
case .authToken:
|
||||
return authToken
|
||||
}
|
||||
}
|
||||
|
||||
func delete(_ type: KeyType) {
|
||||
|
||||
}
|
||||
|
||||
func generate(_ type: KeyType) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
19
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
19
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct Sesame_Watch_Watch_AppApp: App {
|
||||
|
||||
let keyManagement = KeyManagement()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
TabView {
|
||||
ContentView()
|
||||
.environmentObject(keyManagement)
|
||||
SettingsView()
|
||||
HistoryView()
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
19
Sesame-Watch Watch App/SettingsView.swift
Normal file
19
Sesame-Watch Watch App/SettingsView.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
.previewDevice("Apple Watch Series 7 - 41mm")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user