diff --git a/Werkout-ios-Info.plist b/Werkout-ios-Info.plist
index fbd83bd..9d20ca2 100644
--- a/Werkout-ios-Info.plist
+++ b/Werkout-ios-Info.plist
@@ -2,6 +2,15 @@
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ UIBackgroundModes
+
+ audio
+
UILaunchScreen
UIColorName
@@ -9,14 +18,5 @@
UIImageName
AppIcon
- NSHealthShareUsageDescription
- Read your heart reate
- NSHealthUpdateUsageDescription
- Read your heart reate
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
diff --git a/Werkout_ios.xcodeproj/project.pbxproj b/Werkout_ios.xcodeproj/project.pbxproj
index 42a3b27..ffb9ad1 100644
--- a/Werkout_ios.xcodeproj/project.pbxproj
+++ b/Werkout_ios.xcodeproj/project.pbxproj
@@ -15,6 +15,7 @@
1C485C8A2A492BB400A6F896 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C485C892A492BB400A6F896 /* LoginView.swift */; };
1C485C8C2A49D65600A6F896 /* WorkoutHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C485C8B2A49D65600A6F896 /* WorkoutHistoryView.swift */; };
1C485C8D2A49D95700A6F896 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF65A822A42347D0042FFBD /* Extensions.swift */; };
+ 1C6BF28F2A56602B00450FD7 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C6BF28E2A56602B00450FD7 /* Keychain.swift */; };
1CAF4D8A2A5132F900B00E50 /* PlannedWorkout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAF4D892A5132F900B00E50 /* PlannedWorkout.swift */; };
1CAF4D8C2A51339200B00E50 /* PlannedWorkouts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1CAF4D8B2A51339200B00E50 /* PlannedWorkouts.json */; };
1CAF4D952A52180600B00E50 /* PlanWorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAF4D942A52180600B00E50 /* PlanWorkoutView.swift */; };
@@ -111,6 +112,7 @@
1C485C862A4915C400A6F896 /* CreateWorkoutItemPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateWorkoutItemPickerView.swift; sourceTree = ""; };
1C485C892A492BB400A6F896 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; };
1C485C8B2A49D65600A6F896 /* WorkoutHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHistoryView.swift; sourceTree = ""; };
+ 1C6BF28E2A56602B00450FD7 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; };
1C6D0A3C2A4BEC9700D98B06 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS9.4.sdk/System/Library/Frameworks/AVKit.framework; sourceTree = DEVELOPER_DIR; };
1C6D0A3D2A4BEC9700D98B06 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS9.4.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
1C6D0A402A4BECA400D98B06 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.4.sdk/System/Library/Frameworks/AVKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -238,6 +240,7 @@
1CF65A4F2A3A1EA90042FFBD /* BridgeModule.swift */,
1CF65A802A412AA30042FFBD /* DataStore.swift */,
1CF65AB92A4894430042FFBD /* UserStore.swift */,
+ 1C6BF28E2A56602B00450FD7 /* Keychain.swift */,
1CF65A3F2A3973840042FFBD /* Views */,
1CF65A3E2A39737D0042FFBD /* APIModels */,
1CF65A3D2A3973760042FFBD /* Network */,
@@ -520,6 +523,7 @@
1CF65A6B2A3C1EAC0042FFBD /* CreateWorkoutMainView.swift in Sources */,
1CF65A7B2A3F83440042FFBD /* CreateWorkoutSupersetActionsView.swift in Sources */,
1CF65A262A3972840042FFBD /* Werkout_iosApp.swift in Sources */,
+ 1C6BF28F2A56602B00450FD7 /* Keychain.swift in Sources */,
1CF65A3C2A3972CE0042FFBD /* ExternalWorkoutDetailView.swift in Sources */,
1CF65A632A3BF6A30042FFBD /* AllWorkoutsView.swift in Sources */,
1CF65A692A3C018F0042FFBD /* AccountView.swift in Sources */,
@@ -708,6 +712,8 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Werkout-ios-Info.plist";
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "Read your heart reate";
+ INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Read your heart reate";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -748,6 +754,8 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Werkout-ios-Info.plist";
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "Read your heart reate";
+ INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Read your heart reate";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
diff --git a/Werkout_ios/BridgeModule.swift b/Werkout_ios/BridgeModule.swift
index ddaaf57..d7b80c6 100644
--- a/Werkout_ios/BridgeModule.swift
+++ b/Werkout_ios/BridgeModule.swift
@@ -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 {
diff --git a/Werkout_ios/Keychain.swift b/Werkout_ios/Keychain.swift
new file mode 100644
index 0000000..7cdc80b
--- /dev/null
+++ b/Werkout_ios/Keychain.swift
@@ -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)
+ }
+ }
+}
diff --git a/Werkout_ios/UserStore.swift b/Werkout_ios/UserStore.swift
index e2b718d..d17e425 100644
--- a/Werkout_ios/UserStore.swift
+++ b/Werkout_ios/UserStore.swift
@@ -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)
diff --git a/Werkout_ios/Views/Login/LoginView.swift b/Werkout_ios/Views/Login/LoginView.swift
index de6b7e5..daf7ba0 100644
--- a/Werkout_ios/Views/Login/LoginView.swift
+++ b/Werkout_ios/Views/Login/LoginView.swift
@@ -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())
diff --git a/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift b/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift
index c0d8888..188462e 100644
--- a/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift
+++ b/Werkout_ios/Views/WorkoutDetail/WorkoutDetailView.swift
@@ -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
diff --git a/Werkout_ios/Werkout_ios.entitlements b/Werkout_ios/Werkout_ios.entitlements
index 73896b4..823c765 100644
--- a/Werkout_ios/Werkout_ios.entitlements
+++ b/Werkout_ios/Werkout_ios.entitlements
@@ -2,6 +2,10 @@
+ com.apple.developer.associated-domains
+
+ dev.werkout.fitness
+
com.apple.developer.healthkit
com.apple.developer.healthkit.access