diff --git a/Sources/StytchCore/KeychainClient/KeychainClient+Item.swift b/Sources/StytchCore/KeychainClient/KeychainClient+Item.swift index b62866e20c2..3370921d7d0 100644 --- a/Sources/StytchCore/KeychainClient/KeychainClient+Item.swift +++ b/Sources/StytchCore/KeychainClient/KeychainClient+Item.swift @@ -59,7 +59,10 @@ extension KeychainClient { } extension KeychainClient.Item { + // The private key registration is central to biometric authentication, and this item should be protected by biometrics unless explicitly specified otherwise by the caller. static let privateKeyRegistration: Self = .init(kind: .privateKey, name: "stytch_private_key_registration") + // This was introduced in version 0.54.0 to store the biometric registration ID in a keychain item that is not protected by biometrics. + static let biometricKeyRegistration: Self = .init(kind: .object, name: "stytch_biometric_key_registration") static let sessionToken: Self = .init(kind: .token, name: SessionToken.Kind.opaque.name) static let sessionJwt: Self = .init(kind: .token, name: SessionToken.Kind.jwt.name) diff --git a/Sources/StytchCore/Networking/NetworkingRouter.swift b/Sources/StytchCore/Networking/NetworkingRouter.swift index 9189922a0d6..a9452f85d75 100644 --- a/Sources/StytchCore/Networking/NetworkingRouter.swift +++ b/Sources/StytchCore/Networking/NetworkingRouter.swift @@ -117,13 +117,6 @@ public extension NetworkingRouter { } } - private func cleanupPotentiallyOrphanedBiometricRegistrations(_ user: User) { - // if we have a local biometric registration that doesn't exist on the user object, delete the local - if let queryResult: KeychainClient.QueryResult = try? keychainClient.get(.privateKeyRegistration).first, let biometricRegistrationId = try? queryResult.generic.map({ try jsonDecoder.decode(KeychainClient.KeyRegistration.self, from: $0) }), !user.biometricRegistrations.map(\.id).contains(biometricRegistrationId.registrationId) { - try? keychainClient.removeItem(.privateKeyRegistration) - } - } - // swiftlint:disable:next function_body_length private func performRequest( _ method: NetworkingClient.Method, @@ -146,7 +139,9 @@ public extension NetworkingRouter { hostUrl: configuration.hostUrl ) userStorage.update(sessionResponse.user) - cleanupPotentiallyOrphanedBiometricRegistrations(sessionResponse.user) + #if !os(tvOS) && !os(watchOS) + StytchClient.biometrics.cleanupPotentiallyOrphanedBiometricRegistrations() + #endif } else if let sessionResponse = dataContainer.data as? B2BAuthenticateResponseType { sessionManager.updateSession( sessionType: .member(sessionResponse.memberSession), diff --git a/Sources/StytchCore/StytchClient/Biometrics/StytchClient+Biometrics.swift b/Sources/StytchCore/StytchClient/Biometrics/StytchClient+Biometrics.swift index 07fe95d042b..496b9043a00 100644 --- a/Sources/StytchCore/StytchClient/Biometrics/StytchClient+Biometrics.swift +++ b/Sources/StytchCore/StytchClient/Biometrics/StytchClient+Biometrics.swift @@ -9,6 +9,11 @@ public extension StytchClient.Biometrics { case availableRegistered } } + +public extension StytchClient { + /// The interface for interacting with biometrics products. + static var biometrics: Biometrics { .init(router: router.scopedRouter { $0.biometrics }) } +} #endif public extension StytchClient { @@ -29,7 +34,7 @@ public extension StytchClient { /// Indicates if there is an existing biometric registration on device. public var registrationAvailable: Bool { - keychainClient.valueExistsForItem(.privateKeyRegistration) + keychainClient.valueExistsForItem(.biometricKeyRegistration) } #if !os(tvOS) && !os(watchOS) @@ -51,17 +56,17 @@ public extension StytchClient { // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling) /// Removes the current device's existing biometric registration from both the device itself and from the server. public func removeRegistration() async throws { - guard let queryResult: KeychainClient.QueryResult = try? keychainClient.get(.privateKeyRegistration).first else { + guard let queryResult = try? keychainClient.get(.biometricKeyRegistration).first else { return } // Delete registration from backend - if let registration = try queryResult.generic.map({ try jsonDecoder.decode(KeychainClient.KeyRegistration.self, from: $0) }) { - _ = try await StytchClient.user.deleteFactor(.biometricRegistration(id: registration.registrationId)) + if let biometricRegistrationId = queryResult.stringValue { + _ = try await StytchClient.user.deleteFactor(.biometricRegistration(id: User.BiometricRegistration.ID(stringLiteral: biometricRegistrationId))) } - // Remove local registration - try keychainClient.removeItem(.privateKeyRegistration) + // Remove local registrations + try clearBiometricRegistrations() } // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling) @@ -74,6 +79,11 @@ public extension StytchClient { throw StytchSDKError.noCurrentSession } + // Attempt to remove the current registration if it exists. + // If we don't remove the current one before creating a new one, the user record will retain an unused biometric registration indefinitely. + // The error thrown here can be safely ignored. If it fails, we don't want to prevent the creation of the new biometric registration. + try? await removeRegistration() + let (privateKey, publicKey) = cryptoClient.generateKeyPair() let startResponse: RegisterStartResponse = try await router.post( @@ -99,23 +109,29 @@ public extension StytchClient { registrationId: finishResponse.biometricRegistrationId ) + // Set the .privateKeyRegistration try keychainClient.set( key: privateKey, registration: registration, accessPolicy: parameters.accessPolicy.keychainValue ) + // Set the .biometricKeyRegistration + try keychainClient.set(registration.registrationId.rawValue, for: .biometricKeyRegistration) + return finishResponse } // sourcery: AsyncVariants, (NOTE: - must use /// doc comment styling) /// If a valid biometric registration exists, this method confirms the current device owner via the device's built-in biometric reader and returns an updated session object by either starting a new session or adding a the biometric factor to an existing session. public func authenticate(parameters: AuthenticateParameters) async throws -> AuthenticateResponse { - guard let queryResult: KeychainClient.QueryResult = try keychainClient.get(.privateKeyRegistration).first else { + guard let privateKeyRegistrationQueryResult: KeychainClient.QueryResult = try keychainClient.get(.privateKeyRegistration).first else { throw StytchSDKError.noBiometricRegistration } - let privateKey = queryResult.data + try copyBiometricRegistrationIDToKeychainIfNeeded(privateKeyRegistrationQueryResult) + + let privateKey = privateKeyRegistrationQueryResult.data let publicKey = try cryptoClient.publicKeyForPrivateKey(privateKey) let startResponse: AuthenticateStartResponse = try await router.post( @@ -123,26 +139,67 @@ public extension StytchClient { parameters: AuthenticateStartParameters(publicKey: publicKey) ) + let authenticateCompleteParameters = AuthenticateCompleteParameters( + signature: try cryptoClient.signChallengeWithPrivateKey(startResponse.challenge, privateKey), + biometricRegistrationId: startResponse.biometricRegistrationId, + sessionDurationMinutes: parameters.sessionDuration + ) + // NOTE: - We could return separate concrete type which deserializes/contains biometric_registration_id, but this doesn't currently add much value - return try await router.post( + let authenticateResponse: AuthenticateResponse = try await router.post( to: .authenticate(.complete), - parameters: AuthenticateCompleteParameters( - signature: cryptoClient.signChallengeWithPrivateKey(startResponse.challenge, privateKey), - biometricRegistrationId: startResponse.biometricRegistrationId, - sessionDurationMinutes: parameters.sessionDuration - ), + parameters: authenticateCompleteParameters, useDFPPA: true ) + return authenticateResponse } - } -} -#if !os(tvOS) && !os(watchOS) -public extension StytchClient { - /// The interface for interacting with biometrics products. - static var biometrics: Biometrics { .init(router: router.scopedRouter { $0.biometrics }) } + // Clear both the .privateKeyRegistration and the .biometricKeyRegistration + func clearBiometricRegistrations() throws { + try keychainClient.removeItem(.privateKeyRegistration) + try keychainClient.removeItem(.biometricKeyRegistration) + } + + // if we have a local biometric registration that doesn't exist on the user object, delete the local + func cleanupPotentiallyOrphanedBiometricRegistrations() { + guard let user = StytchClient.user.getSync() else { + return + } + + if user.biometricRegistrations.isEmpty { + try? clearBiometricRegistrations() + } else { + let queryResult = try? keychainClient.get(.biometricKeyRegistration).first + + // Check if the user's biometric registrations contain the ID + var userBiometricRegistrationIds = [String]() + for biometricRegistration in user.biometricRegistrations { + userBiometricRegistrationIds.append(biometricRegistration.biometricRegistrationId.rawValue) + } + + if let biometricRegistrationId = queryResult?.stringValue, userBiometricRegistrationIds.contains(biometricRegistrationId) == false { + // Remove the orphaned biometric registration + try? clearBiometricRegistrations() + } + } + } + + /* + After introducing the .biometricKeyRegistration keychain item in version 0.54.0, we needed a way for versions prior to 0.54.0 + to copy the value stored in the biometrically protected .privateKeyRegistration keychain item into the non-biometric + .biometricKeyRegistration keychain item without triggering unnecessary Face ID prompts. Since a Face ID prompt is already + being shown here for authentication, we decided to use this as an opportunity to perform the migration by copying the + registration ID into the .biometricKeyRegistration keychain item. For versions after 0.54.0, this action occurs during + registration, and it should only happen here if the .biometricKeyRegistration keychain item is empty. + */ + func copyBiometricRegistrationIDToKeychainIfNeeded(_ queryResult: KeychainClient.QueryResult) throws { + let biometricKeyRegistration = try? keychainClient.get(.biometricKeyRegistration).first + if biometricKeyRegistration == nil, let registration = try queryResult.generic.map({ try jsonDecoder.decode(KeychainClient.KeyRegistration.self, from: $0) }) { + try keychainClient.set(registration.registrationId.rawValue, for: .biometricKeyRegistration) + } + } + } } -#endif public extension StytchClient.Biometrics { typealias RegisterCompleteResponse = Response diff --git a/Stytch/DemoApps/B2BWorkbench/Extensions.swift b/Stytch/DemoApps/B2BWorkbench/Extensions.swift index 7ad9cf0cf92..d003c9ac8ee 100644 --- a/Stytch/DemoApps/B2BWorkbench/Extensions.swift +++ b/Stytch/DemoApps/B2BWorkbench/Extensions.swift @@ -2,10 +2,6 @@ import Foundation import StytchCore import UIKit -enum TextFieldAlertError: Error { - case emptyString -} - extension UIViewController { var organizationId: String? { UserDefaults.standard.string(forKey: Constants.orgIdDefaultsKey) @@ -14,105 +10,6 @@ extension UIViewController { var memberId: String? { StytchB2BClient.member.getSync()?.id.rawValue } - - func presentAlertWithTitle( - alertTitle: String, - buttonTitle: String = "OK", - completion: (() -> Void)? = nil - ) { - let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: buttonTitle, style: .cancel) { _ in - completion?() - } - alertController.addAction(okAction) - present(alertController, animated: true) - } - - func presentTextFieldAlertWithTitle( - alertTitle: String, - buttonTitle: String = "Submit", - cancelButtonTitle: String = "Cancel", - completion: ((String?) -> Void)? = nil - ) { - let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) - alertController.addTextField() - - let submitAction = UIAlertAction(title: buttonTitle, style: .default) { [unowned alertController] _ in - if let text = alertController.textFields?[0].text { - completion?(text) - } - } - alertController.addAction(.init(title: cancelButtonTitle, style: .cancel, handler: { _ in - completion?(nil) - })) - - alertController.addAction(submitAction) - present(alertController, animated: true) - } - - @MainActor - func presentTextFieldAlertWithTitle( - alertTitle: String, - buttonTitle: String = "Submit", - cancelButtonTitle: String = "Cancel" - ) async throws -> String? { - try await withCheckedThrowingContinuation { continuation in - let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) - alertController.addTextField() - - let submitAction = UIAlertAction(title: buttonTitle, style: .default) { [unowned alertController] _ in - if let text = alertController.textFields?[0].text, text.isEmpty == false { - continuation.resume(returning: text) - } else { - continuation.resume(returning: nil) - } - } - - alertController.addAction(submitAction) - - alertController.addAction(.init(title: cancelButtonTitle, style: .cancel, handler: { _ in - continuation.resume(returning: nil) - })) - - present(alertController, animated: true) - } - } - - func presentErrorWithDescription(error: Error, description: String) { - presentAlertWithTitle(alertTitle: "\(description) - \(error.errorInfo)") - } - - func presentAlertAndLogMessage(description: String, object: Any) { - if let error = object as? Error { - presentAlertWithTitle(alertTitle: "\(description)\n\ncheck logs for more info") - print("\(description)\n\(error.errorInfo)\n\(error)\n") - } else { - presentAlertWithTitle(alertTitle: "\(description)\n\ncheck logs for more info") - print("\(description)\n\(object)\n") - } - } -} - -extension UIButton { - convenience init(title: String, primaryAction: UIAction) { - var configuration: UIButton.Configuration = .borderedProminent() - configuration.title = title - self.init(configuration: configuration, primaryAction: primaryAction) - } -} - -extension UITextField { - convenience init(title: String, primaryAction: UIAction, keyboardType: UIKeyboardType = .default, password: Bool = false) { - self.init(frame: .zero, primaryAction: primaryAction) - borderStyle = .roundedRect - placeholder = title - autocorrectionType = .no - autocapitalizationType = .none - self.keyboardType = keyboardType - if password == true { - textContentType = .password - } - } } extension UIStackView { diff --git a/Stytch/DemoApps/Shared/Extensions.swift b/Stytch/DemoApps/Shared/Extensions.swift new file mode 100644 index 00000000000..7e97d54c966 --- /dev/null +++ b/Stytch/DemoApps/Shared/Extensions.swift @@ -0,0 +1,110 @@ +import Foundation +import StytchCore +import UIKit + +enum TextFieldAlertError: Error { + case emptyString +} + +extension UIViewController { + func presentAlertWithTitle( + alertTitle: String, + buttonTitle: String = "OK", + completion: (() -> Void)? = nil + ) { + let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: buttonTitle, style: .cancel) { _ in + completion?() + } + alertController.addAction(okAction) + present(alertController, animated: true) + } + + func presentTextFieldAlertWithTitle( + alertTitle: String, + buttonTitle: String = "Submit", + cancelButtonTitle: String = "Cancel", + completion: ((String?) -> Void)? = nil + ) { + let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) + alertController.addTextField() + + let submitAction = UIAlertAction(title: buttonTitle, style: .default) { [unowned alertController] _ in + if let text = alertController.textFields?[0].text { + completion?(text) + } + } + alertController.addAction(.init(title: cancelButtonTitle, style: .cancel, handler: { _ in + completion?(nil) + })) + + alertController.addAction(submitAction) + present(alertController, animated: true) + } + + @MainActor + func presentTextFieldAlertWithTitle( + alertTitle: String, + buttonTitle: String = "Submit", + cancelButtonTitle: String = "Cancel", + keyboardType: UIKeyboardType = .default + ) async throws -> String? { + try await withCheckedThrowingContinuation { continuation in + let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) + alertController.addTextField() + alertController.textFields?[0].keyboardType = keyboardType + + let submitAction = UIAlertAction(title: buttonTitle, style: .default) { [unowned alertController] _ in + if let text = alertController.textFields?[0].text, text.isEmpty == false { + continuation.resume(returning: text) + } else { + continuation.resume(returning: nil) + } + } + + alertController.addAction(submitAction) + + alertController.addAction(.init(title: cancelButtonTitle, style: .cancel, handler: { _ in + continuation.resume(returning: nil) + })) + + present(alertController, animated: true) + } + } + + func presentErrorWithDescription(error: Error, description: String) { + presentAlertWithTitle(alertTitle: "\(description) - \(error.errorInfo)") + } + + func presentAlertAndLogMessage(description: String, object: Any) { + if let error = object as? Error { + presentAlertWithTitle(alertTitle: "\(description)\n\ncheck logs for more info") + print("\(description)\n\(error.errorInfo)\n\(error)\n") + } else { + presentAlertWithTitle(alertTitle: "\(description)\n\ncheck logs for more info") + print("\(description)\n\(object)\n") + } + } +} + +extension UIButton { + convenience init(title: String, primaryAction: UIAction) { + var configuration: UIButton.Configuration = .borderedProminent() + configuration.title = title + self.init(configuration: configuration, primaryAction: primaryAction) + } +} + +extension UITextField { + convenience init(title: String, primaryAction: UIAction, keyboardType: UIKeyboardType = .default, password: Bool = false) { + self.init(frame: .zero, primaryAction: primaryAction) + borderStyle = .roundedRect + placeholder = title + autocorrectionType = .no + autocapitalizationType = .none + self.keyboardType = keyboardType + if password == true { + textContentType = .password + } + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/AppDelegate.swift b/Stytch/DemoApps/StytchBiometrics/AppDelegate.swift new file mode 100644 index 00000000000..5d0695c4ad0 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/AppDelegate.swift @@ -0,0 +1,23 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + true + } + + // MARK: UISceneSession Lifecycle + + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_: UIApplication, didDiscardSceneSessions _: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AccentColor.colorset/Contents.json b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 1.png b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 1.png new file mode 100644 index 00000000000..d4614d38c92 Binary files /dev/null and b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 1.png differ diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 2.png b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 2.png new file mode 100644 index 00000000000..d4614d38c92 Binary files /dev/null and b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024 2.png differ diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024.png b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024.png new file mode 100644 index 00000000000..d4614d38c92 Binary files /dev/null and b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/App icon1024x1024.png differ diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/Contents.json b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..3443bc4803a --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "App icon1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "App icon1024x1024 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "App icon1024x1024 2.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Contents.json b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Contents.json b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Contents.json new file mode 100644 index 00000000000..0d97a833d56 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Wordmark-light-mode.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Wordmark-light-mode.png b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Wordmark-light-mode.png new file mode 100644 index 00000000000..ebcf8bf5b19 Binary files /dev/null and b/Stytch/DemoApps/StytchBiometrics/Assets.xcassets/Wordmark-light-mode.imageset/Wordmark-light-mode.png differ diff --git a/Stytch/DemoApps/StytchBiometrics/Base.lproj/Main.storyboard b/Stytch/DemoApps/StytchBiometrics/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..dc968310d01 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Base.lproj/Main.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stytch/DemoApps/StytchBiometrics/Base.lproj/StytchBiometricsLaunchScreen.storyboard b/Stytch/DemoApps/StytchBiometrics/Base.lproj/StytchBiometricsLaunchScreen.storyboard new file mode 100644 index 00000000000..1308b0e2daf --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Base.lproj/StytchBiometricsLaunchScreen.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stytch/DemoApps/StytchBiometrics/Info.plist b/Stytch/DemoApps/StytchBiometrics/Info.plist new file mode 100644 index 00000000000..481b073b506 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/Info.plist @@ -0,0 +1,27 @@ + + + + + NSFaceIDUsageDescription + For Authentication + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Stytch/DemoApps/StytchBiometrics/SceneDelegate.swift b/Stytch/DemoApps/StytchBiometrics/SceneDelegate.swift new file mode 100644 index 00000000000..bbe1a8b1bf9 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/SceneDelegate.swift @@ -0,0 +1,40 @@ +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/Stytch/DemoApps/StytchBiometrics/ViewController.swift b/Stytch/DemoApps/StytchBiometrics/ViewController.swift new file mode 100644 index 00000000000..9aee8fce042 --- /dev/null +++ b/Stytch/DemoApps/StytchBiometrics/ViewController.swift @@ -0,0 +1,107 @@ +import StytchCore +import UIKit + +class ViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + StytchClient.configure(publicToken: "public-token-test-728f8b82-2a20-4926-b077-a8ca7d67e1b2") + } + + @IBAction func sendAndAuthenticateOTPTapped(_: Any) { + Task { + do { + guard let phoneNumber = try await presentTextFieldAlertWithTitle(alertTitle: "Enter Your Phone Number In The Format xxxxxxxxxx", keyboardType: .numberPad) else { + throw TextFieldAlertError.emptyString + } + let loginOrCreateResponse = try await StytchClient.otps.loginOrCreate(parameters: .init(deliveryMethod: .sms(phoneNumber: "+1\(phoneNumber)", enableAutofill: false))) + + guard let code = try await presentTextFieldAlertWithTitle(alertTitle: "Enter The OTP Code", keyboardType: .numberPad) else { + throw TextFieldAlertError.emptyString + } + let authenticateResponse = try await StytchClient.otps.authenticate(parameters: .init(code: code, methodId: loginOrCreateResponse.methodId)) + presentAlertWithTitle(alertTitle: "Authetication Success!") + logBiometricRegistrations(user: authenticateResponse.user, identifier: "otps.authenticate") + } catch { + print(error.errorInfo) + } + } + } + + @IBAction func registerBiometoricsTapped(_: Any) { + Task { + do { + _ = try await StytchClient.biometrics.register(parameters: + .init( + identifier: "foo@stytch.com", + accessPolicy: .deviceOwnerAuthentication + ) + ) + presentAlertWithTitle(alertTitle: "Register Biometrics Success!") + logBiometricRegistrations(user: StytchClient.user.getSync(), identifier: "biometrics.register") + } catch { + print(error.errorInfo) + } + } + } + + @IBAction func unregisterBiometoricsTapped(_: Any) { + Task { + do { + try await StytchClient.biometrics.removeRegistration() + presentAlertWithTitle(alertTitle: "Remove Biometrics Registration Success!") + logBiometricRegistrations(user: StytchClient.user.getSync(), identifier: "biometrics.removeRegistration") + } catch { + print(error.errorInfo) + } + } + } + + @IBAction func autheticateBiometoricsTapped(_: Any) { + Task { + do { + let response = try await StytchClient.biometrics.authenticate(parameters: .init()) + presentAlertWithTitle(alertTitle: "Authenticate Biometrics Success!") + logBiometricRegistrations(user: response.user, identifier: "biometrics.authenticate") + } catch { + print(error.errorInfo) + } + } + } + + @IBAction func deleteRegistrationsTapped(_: Any) { + clearAllBiometricRegistrations(user: StytchClient.user.getSync()) + Task { + do { + let user = try await StytchClient.user.get().wrapped + logBiometricRegistrations(user: user, identifier: "delete registrations") + } catch { + print(error.errorInfo) + } + } + } + + func clearAllBiometricRegistrations(user: User?) { + guard let user else { return } + Task { + do { + for registration in user.biometricRegistrations { + _ = try await StytchClient.user.deleteFactor(.biometricRegistration(id: registration.id)) + } + } catch { + print(error.errorInfo) + } + } + } + + func logBiometricRegistrations(user: User?, identifier: String) { + print( + """ + ------------------------------------------------------------------------- + \(identifier) + biometricRegistrations.count: \(user?.biometricRegistrations.count ?? 0) + biometricRegistrations: \(user?.biometricRegistrations.compactMap(\.id.rawValue).joined(separator: ", ") ?? "") + ------------------------------------------------------------------------- + """ + ) + } +} diff --git a/Stytch/Stytch.xcodeproj/project.pbxproj b/Stytch/Stytch.xcodeproj/project.pbxproj index 47e0dff5023..ba4a31c28ae 100644 --- a/Stytch/Stytch.xcodeproj/project.pbxproj +++ b/Stytch/Stytch.xcodeproj/project.pbxproj @@ -52,6 +52,8 @@ 53E128292B50092F00976CAC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 53E128282B50092F00976CAC /* Assets.xcassets */; }; 53E128402B50092F00976CAC /* StytchUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E1283F2B50092F00976CAC /* StytchUITests.swift */; }; 53E128422B50092F00976CAC /* StytchUILaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E128412B50092F00976CAC /* StytchUILaunchTests.swift */; }; + 740F14802D2C34090028180F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7478F7DA2BF6467900BCB233 /* Extensions.swift */; }; + 740F14822D2C35AD0028180F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740F14812D2C35AC0028180F /* Extensions.swift */; }; 741827142CF0FF1E00A475CB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7418270E2CF0FF1E00A475CB /* Preview Assets.xcassets */; }; 741827152CF0FF1E00A475CB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 741827102CF0FF1E00A475CB /* Assets.xcassets */; }; 741827162CF0FF1E00A475CB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741827112CF0FF1E00A475CB /* ContentView.swift */; }; @@ -76,6 +78,13 @@ 7478F7DC2BF6467900BCB233 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7478F7DA2BF6467900BCB233 /* Extensions.swift */; }; 749F88862BFBD63E00D7F386 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 749F88852BFBD63E00D7F386 /* Launch Screen.storyboard */; }; 74A2D1912C21FDF8007F8F20 /* OTPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A2D1902C21FDF8007F8F20 /* OTPViewController.swift */; }; + 74BCCBD72D2C2A97001A1C0A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74BCCBCD2D2C2A97001A1C0A /* AppDelegate.swift */; }; + 74BCCBD82D2C2A97001A1C0A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74BCCBD42D2C2A97001A1C0A /* SceneDelegate.swift */; }; + 74BCCBD92D2C2A97001A1C0A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74BCCBD52D2C2A97001A1C0A /* ViewController.swift */; }; + 74BCCBDA2D2C2A97001A1C0A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 74BCCBCE2D2C2A97001A1C0A /* Assets.xcassets */; }; + 74BCCBDC2D2C2A97001A1C0A /* StytchBiometricsLaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74BCCBD12D2C2A97001A1C0A /* StytchBiometricsLaunchScreen.storyboard */; }; + 74BCCBDD2D2C2A97001A1C0A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74BCCBD32D2C2A97001A1C0A /* Main.storyboard */; }; + 74BCCBDF2D2C2DE7001A1C0A /* StytchCore in Frameworks */ = {isa = PBXBuildFile; productRef = 74BCCBDE2D2C2DE7001A1C0A /* StytchCore */; }; 74C427992C9A25E700EFA1A1 /* SCIMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C427982C9A25E700EFA1A1 /* SCIMViewController.swift */; }; 74DD38CB2C2A2ABA00BEB0DD /* OAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD38CA2C2A2ABA00BEB0DD /* OAuthViewController.swift */; }; 74F0088D2C24C05100E0F863 /* RecoveryCodesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F0088C2C24C05000E0F863 /* RecoveryCodesViewController.swift */; }; @@ -143,6 +152,7 @@ 53E1283F2B50092F00976CAC /* StytchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StytchUITests.swift; sourceTree = ""; }; 53E128412B50092F00976CAC /* StytchUILaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StytchUILaunchTests.swift; sourceTree = ""; }; 53E1284C2B500C7400976CAC /* StytchUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = StytchUI.xctestplan; sourceTree = ""; }; + 740F14812D2C35AC0028180F /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 741827002CF0D8A600A475CB /* StytchB2BUIDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StytchB2BUIDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7418270E2CF0FF1E00A475CB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 741827102CF0FF1E00A475CB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -169,6 +179,14 @@ 748FE40A2C54127D0026F2A2 /* ConsumerWorkbench-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ConsumerWorkbench-Bridging-Header.h"; sourceTree = ""; }; 749F88852BFBD63E00D7F386 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 74A2D1902C21FDF8007F8F20 /* OTPViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPViewController.swift; sourceTree = ""; }; + 74BCCBB82D2C2A8D001A1C0A /* StytchBiometrics.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StytchBiometrics.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 74BCCBCD2D2C2A97001A1C0A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 74BCCBCE2D2C2A97001A1C0A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 74BCCBCF2D2C2A97001A1C0A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 74BCCBD02D2C2A97001A1C0A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StytchBiometricsLaunchScreen.storyboard; sourceTree = ""; }; + 74BCCBD22D2C2A97001A1C0A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 74BCCBD42D2C2A97001A1C0A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 74BCCBD52D2C2A97001A1C0A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 74BF2AFB2CF4EF7200798DCE /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; 74C427982C9A25E700EFA1A1 /* SCIMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCIMViewController.swift; sourceTree = ""; }; 74D1157E2BFBDFFD002EAB79 /* B2BWorkbench-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "B2BWorkbench-Bridging-Header.h"; sourceTree = ""; }; @@ -229,15 +247,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 74BCCBB52D2C2A8D001A1C0A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 74BCCBDF2D2C2DE7001A1C0A /* StytchCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 22315319299EE16D00BA9126 /* B2BWorkbench */ = { isa = PBXGroup; children = ( + 740F14812D2C35AC0028180F /* Extensions.swift */, 2231531A299EE16D00BA9126 /* AppDelegate.swift */, 2231531C299EE16D00BA9126 /* SceneDelegate.swift */, - 7478F7DA2BF6467900BCB233 /* Extensions.swift */, 2254779529A05C93003DF229 /* Constants.swift */, 74F0088E2C25BB8800E0F863 /* ViewControllers */, 22315320299EE16D00BA9126 /* Main.storyboard */, @@ -332,6 +358,7 @@ 53E1283B2B50092F00976CAC /* StytchUITests.xctest */, 745BAB652C88C27600771497 /* StytchDemo.app */, 741827002CF0D8A600A475CB /* StytchB2BUIDemo.app */, + 74BCCBB82D2C2A8D001A1C0A /* StytchBiometrics.app */, ); name = Products; sourceTree = ""; @@ -373,6 +400,14 @@ path = StytchUITests; sourceTree = ""; }; + 740F147F2D2C33F90028180F /* Shared */ = { + isa = PBXGroup; + children = ( + 7478F7DA2BF6467900BCB233 /* Extensions.swift */, + ); + path = Shared; + sourceTree = ""; + }; 7418270F2CF0FF1E00A475CB /* Preview Content */ = { isa = PBXGroup; children = ( @@ -420,17 +455,33 @@ 748FE4092C5410670026F2A2 /* DemoApps */ = { isa = PBXGroup; children = ( + 740F147F2D2C33F90028180F /* Shared */, 745BAB662C88C27600771497 /* StytchDemo */, 53E128232B50092E00976CAC /* StytchUIDemo */, 741827132CF0FF1E00A475CB /* StytchB2BUIDemo */, 22BE99F82847FDC800E29245 /* ConsumerWorkbench */, 22315319299EE16D00BA9126 /* B2BWorkbench */, + 74BCCBD62D2C2A97001A1C0A /* StytchBiometrics */, 53E1283E2B50092F00976CAC /* StytchUITests */, 53E1284C2B500C7400976CAC /* StytchUI.xctestplan */, ); path = DemoApps; sourceTree = ""; }; + 74BCCBD62D2C2A97001A1C0A /* StytchBiometrics */ = { + isa = PBXGroup; + children = ( + 74BCCBCD2D2C2A97001A1C0A /* AppDelegate.swift */, + 74BCCBCE2D2C2A97001A1C0A /* Assets.xcassets */, + 74BCCBCF2D2C2A97001A1C0A /* Info.plist */, + 74BCCBD12D2C2A97001A1C0A /* StytchBiometricsLaunchScreen.storyboard */, + 74BCCBD32D2C2A97001A1C0A /* Main.storyboard */, + 74BCCBD42D2C2A97001A1C0A /* SceneDelegate.swift */, + 74BCCBD52D2C2A97001A1C0A /* ViewController.swift */, + ); + path = StytchBiometrics; + sourceTree = ""; + }; 74F0088E2C25BB8800E0F863 /* ViewControllers */ = { isa = PBXGroup; children = ( @@ -566,6 +617,26 @@ productReference = 745BAB652C88C27600771497 /* StytchDemo.app */; productType = "com.apple.product-type.application"; }; + 74BCCBB72D2C2A8D001A1C0A /* StytchBiometrics */ = { + isa = PBXNativeTarget; + buildConfigurationList = 74BCCBCC2D2C2A8D001A1C0A /* Build configuration list for PBXNativeTarget "StytchBiometrics" */; + buildPhases = ( + 74BCCBB42D2C2A8D001A1C0A /* Sources */, + 74BCCBB52D2C2A8D001A1C0A /* Frameworks */, + 74BCCBB62D2C2A8D001A1C0A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StytchBiometrics; + packageProductDependencies = ( + 74BCCBDE2D2C2DE7001A1C0A /* StytchCore */, + ); + productName = StytchBiometorics; + productReference = 74BCCBB82D2C2A8D001A1C0A /* StytchBiometrics.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -573,7 +644,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1610; + LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1540; TargetAttributes = { 22315317299EE16D00BA9126 = { @@ -597,6 +668,9 @@ 745BAB642C88C27600771497 = { CreatedOnToolsVersion = 15.4; }; + 74BCCBB72D2C2A8D001A1C0A = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = 229B820F2809EA3E007BC3F1 /* Build configuration list for PBXProject "Stytch" */; @@ -623,6 +697,7 @@ 229B82172809EA3F007BC3F1 /* ConsumerWorkbench */, 22315317299EE16D00BA9126 /* B2BWorkbench */, 53E1283A2B50092F00976CAC /* StytchUITests */, + 74BCCBB72D2C2A8D001A1C0A /* StytchBiometrics */, ); }; /* End PBXProject section */ @@ -682,6 +757,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 74BCCBB62D2C2A8D001A1C0A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74BCCBDA2D2C2A97001A1C0A /* Assets.xcassets in Resources */, + 74BCCBDC2D2C2A97001A1C0A /* StytchBiometricsLaunchScreen.storyboard in Resources */, + 74BCCBDD2D2C2A97001A1C0A /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -707,6 +792,7 @@ 2254779829A05D08003DF229 /* MemberViewController.swift in Sources */, 2254779629A05C93003DF229 /* Constants.swift in Sources */, 74F0088D2C24C05100E0F863 /* RecoveryCodesViewController.swift in Sources */, + 740F14822D2C35AD0028180F /* Extensions.swift in Sources */, 74DD38CB2C2A2ABA00BEB0DD /* OAuthViewController.swift in Sources */, 74707A342D15C8F700CCCDC8 /* B2BUIViewController.swift in Sources */, 2231531D299EE16D00BA9126 /* SceneDelegate.swift in Sources */, @@ -780,6 +866,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 74BCCBB42D2C2A8D001A1C0A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74BCCBD72D2C2A97001A1C0A /* AppDelegate.swift in Sources */, + 74BCCBD82D2C2A97001A1C0A /* SceneDelegate.swift in Sources */, + 74BCCBD92D2C2A97001A1C0A /* ViewController.swift in Sources */, + 740F14802D2C34090028180F /* Extensions.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -811,6 +908,22 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 74BCCBD12D2C2A97001A1C0A /* StytchBiometricsLaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 74BCCBD02D2C2A97001A1C0A /* Base */, + ); + name = StytchBiometricsLaunchScreen.storyboard; + sourceTree = ""; + }; + 74BCCBD32D2C2A97001A1C0A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 74BCCBD22D2C2A97001A1C0A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1219,7 +1332,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 22B584A5Q6; ENABLE_PREVIEWS = YES; @@ -1240,7 +1353,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.stytch.StytchB2BUIDemo; + PRODUCT_BUNDLE_IDENTIFIER = stytch.StytchB2BUIDemo; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -1259,7 +1372,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 22B584A5Q6; ENABLE_PREVIEWS = YES; @@ -1280,7 +1393,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.stytch.StytchB2BUIDemo; + PRODUCT_BUNDLE_IDENTIFIER = stytch.StytchB2BUIDemo; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1369,6 +1482,80 @@ }; name = Release; }; + 74BCCBC92D2C2A8D001A1C0A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 22B584A5Q6; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DemoApps/StytchBiometrics/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = StytchBiometricsLaunchScreen.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stytch.StytchBiometrics; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 74BCCBCA2D2C2A8D001A1C0A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 22B584A5Q6; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DemoApps/StytchBiometrics/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = StytchBiometricsLaunchScreen.storyboard; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.stytch.StytchBiometrics; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1435,6 +1622,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 74BCCBCC2D2C2A8D001A1C0A /* Build configuration list for PBXNativeTarget "StytchBiometrics" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 74BCCBC92D2C2A8D001A1C0A /* Debug */, + 74BCCBCA2D2C2A8D001A1C0A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1502,6 +1698,10 @@ isa = XCSwiftPackageProductDependency; productName = StytchUI; }; + 74BCCBDE2D2C2DE7001A1C0A /* StytchCore */ = { + isa = XCSwiftPackageProductDependency; + productName = StytchCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 229B820C2809EA3E007BC3F1 /* Project object */; diff --git a/Tests/StytchCoreTests/BiometricsTestCase.swift b/Tests/StytchCoreTests/BiometricsTestCase.swift index c6541d2bcb4..ade0f1744a1 100644 --- a/Tests/StytchCoreTests/BiometricsTestCase.swift +++ b/Tests/StytchCoreTests/BiometricsTestCase.swift @@ -54,12 +54,16 @@ final class BiometricsTestCase: BaseTestCase { .init((0..