From e43e5b11bf97221309d57198beee6d2607b86228 Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 5 Jul 2023 22:08:47 -0500 Subject: [PATCH] WIP --- Werkout-ios-Info.plist | 18 +- Werkout_ios.xcodeproj/project.pbxproj | 8 + Werkout_ios/BridgeModule.swift | 2 +- Werkout_ios/Keychain.swift | 172 ++++++++++++++++++ Werkout_ios/UserStore.swift | 10 + Werkout_ios/Views/Login/LoginView.swift | 4 +- .../WorkoutDetail/WorkoutDetailView.swift | 17 -- Werkout_ios/Werkout_ios.entitlements | 4 + 8 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 Werkout_ios/Keychain.swift 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