WIP
This commit is contained in:
@@ -207,7 +207,7 @@ class BridgeModule: NSObject, ObservableObject {
|
||||
mode: .default,
|
||||
options: [.mixWithOthers, .allowAirPlay])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
|
||||
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer?.play()
|
||||
} catch {
|
||||
|
||||
172
Werkout_ios/Keychain.swift
Normal file
172
Werkout_ios/Keychain.swift
Normal file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Keychain.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 7/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class KeychainInterface {
|
||||
enum KeychainError: Error {
|
||||
// Attempted read for an item that does not exist.
|
||||
case itemNotFound
|
||||
|
||||
// Attempted save to override an existing item.
|
||||
// Use update instead of save to update existing items
|
||||
case duplicateItem
|
||||
|
||||
// A read of an item in any format other than Data
|
||||
case invalidItemFormat
|
||||
|
||||
// Any operation result status than errSecSuccess
|
||||
case unexpectedStatus(OSStatus)
|
||||
}
|
||||
|
||||
static let serviceID = "werkout.fitness"
|
||||
|
||||
static func save(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to save in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
|
||||
// kSecValueData is the item value to save
|
||||
kSecValueData as String: password as AnyObject
|
||||
]
|
||||
|
||||
// SecItemAdd attempts to add the item identified by
|
||||
// the query to keychain
|
||||
let status = SecItemAdd(
|
||||
query as CFDictionary,
|
||||
nil
|
||||
)
|
||||
|
||||
// errSecDuplicateItem is a special case where the
|
||||
// item identified by the query already exists. Throw
|
||||
// duplicateItem so the client can determine whether
|
||||
// or not to handle this as an error
|
||||
if status == errSecDuplicateItem {
|
||||
throw KeychainError.duplicateItem
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// save operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
static func update(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to update in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
|
||||
// attributes is passed to SecItemUpdate with
|
||||
// kSecValueData as the updated item value
|
||||
let attributes: [String: AnyObject] = [
|
||||
kSecValueData as String: password as AnyObject
|
||||
]
|
||||
|
||||
// SecItemUpdate attempts to update the item identified
|
||||
// by query, overriding the previous value
|
||||
let status = SecItemUpdate(
|
||||
query as CFDictionary,
|
||||
attributes as CFDictionary
|
||||
)
|
||||
|
||||
// errSecItemNotFound is a special status indicating the
|
||||
// item to update does not exist. Throw itemNotFound so
|
||||
// the client can determine whether or not to handle
|
||||
// this as an error
|
||||
guard status != errSecItemNotFound else {
|
||||
throw KeychainError.itemNotFound
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// update operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
static func readPassword(service: String = KeychainInterface.serviceID, account: String) throws -> Data {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to read in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
|
||||
// kSecMatchLimitOne indicates keychain should read
|
||||
// only the most recent item matching this query
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
|
||||
// kSecReturnData is set to kCFBooleanTrue in order
|
||||
// to retrieve the data for the item
|
||||
kSecReturnData as String: kCFBooleanTrue
|
||||
]
|
||||
|
||||
// SecItemCopyMatching will attempt to copy the item
|
||||
// identified by query to the reference itemCopy
|
||||
var itemCopy: AnyObject?
|
||||
let status = SecItemCopyMatching(
|
||||
query as CFDictionary,
|
||||
&itemCopy
|
||||
)
|
||||
|
||||
// errSecItemNotFound is a special status indicating the
|
||||
// read item does not exist. Throw itemNotFound so the
|
||||
// client can determine whether or not to handle
|
||||
// this case
|
||||
guard status != errSecItemNotFound else {
|
||||
throw KeychainError.itemNotFound
|
||||
}
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// read operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
|
||||
// This implementation of KeychainInterface requires all
|
||||
// items to be saved and read as Data. Otherwise,
|
||||
// invalidItemFormat is thrown
|
||||
guard let password = itemCopy as? Data else {
|
||||
throw KeychainError.invalidItemFormat
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
static func deletePassword(service: String = KeychainInterface.serviceID, account: String) throws {
|
||||
let query: [String: AnyObject] = [
|
||||
// kSecAttrService, kSecAttrAccount, and kSecClass
|
||||
// uniquely identify the item to delete in Keychain
|
||||
kSecAttrService as String: service as AnyObject,
|
||||
kSecAttrAccount as String: account as AnyObject,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
|
||||
// SecItemDelete attempts to perform a delete operation
|
||||
// for the item identified by query. The status indicates
|
||||
// if the operation succeeded or failed.
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
// Any status other than errSecSuccess indicates the
|
||||
// delete operation failed.
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.unexpectedStatus(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
import Foundation
|
||||
|
||||
class UserStore: ObservableObject {
|
||||
static let userNameKeychainValue = "username"
|
||||
static let passwordKeychainValue = "password"
|
||||
|
||||
static let userDefaultsRegisteredUserKey = "registeredUserKey"
|
||||
static let shared = UserStore()
|
||||
|
||||
@@ -34,6 +37,13 @@ class UserStore: ObservableObject {
|
||||
LoginFetchable(postData: postData).fetch(completion: { result in
|
||||
switch result {
|
||||
case .success(let model):
|
||||
if let email = postData["email"] as? String,
|
||||
let password = postData["password"] as? String,
|
||||
let data = password.data(using: .utf8) {
|
||||
try? KeychainInterface.save(password: data,
|
||||
account: email)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.registeredUser = model
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
|
||||
@@ -19,6 +19,7 @@ struct LoginView: View {
|
||||
.font(.title)
|
||||
|
||||
TextField("Email", text: $email)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
@@ -26,7 +27,8 @@ struct LoginView: View {
|
||||
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(uiColor: .clear))).background(Color(uiColor: .init(red: 200/255, green: 200/255, blue: 200/255, alpha: 0.2)))
|
||||
.cornerRadius(8)
|
||||
|
||||
TextField("Password", text: $password)
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.password)
|
||||
.autocapitalization(.none)
|
||||
.frame(height: 55)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
|
||||
@@ -330,23 +330,6 @@ struct ExerciseListView: View {
|
||||
videoExercise = obj.exercise
|
||||
}
|
||||
}
|
||||
|
||||
if i == bridgeModule.currentExerciseIdx {
|
||||
HStack {
|
||||
if obj.exercise.isReps {
|
||||
Text("is reps")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
if obj.exercise.isWeight {
|
||||
Text("is weight")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
if obj.exercise.isDuration {
|
||||
Text("is duration")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: bridgeModule.currentExerciseIdx, perform: { newValue in
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>dev.werkout.fitness</string>
|
||||
</array>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
|
||||
Reference in New Issue
Block a user