diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a10012..9c1a643 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ Note: Tests will fail if the types generated by the swift-openapi-generator are The Turnkey SDK project is structured into several key directories: - **Sources/TurnkeySDK**: Contains the source files for the SDK. -- **Sources/TurnkeySDK/Generated**: This directory is used to store auto-generated Swift files. It is populated by running the `swift-openapi-generator`. +- **Sources/TurnkeySDK/Generated**: This directory is used to store auto-generated Swift files. It is populated by running the `make turnkey_client_types` command. - **templates**: Holds the Stencil templates used by Sourcery for code generation. The main template is `TurnkeyClient.stencil`. ## Makefile Commands diff --git a/README.md b/README.md index d3b2c05..aee0d2b 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,9 @@ The TurnkeySDK is built to support macOS, iOS, tvOS, watchOS, and visionOS, maki To integrate the TurnkeySDK into your Swift project, you need to add it as a dependency in your Package.swift file: ```swift -dependencies: [ - .package(url: "https://github.com/your-organization/swift-sdk", .upToNextMajor(from: "1.0.0")) -] +.package(url: "https://github.com/tkhq/swift-sdk", from: "1.0.0") ``` -Ensure to replace `"https://github.com/your-organization/swift-sdk"` with the actual URL of the Swift SDK repository. - -## Usage - -Here's a quick guide on how to use the TurnkeySDK in your Swift project: - ## Contributing For guidelines on how to contribute to the Swift SDK, please refer to the [contributing guide](CONTRIBUTING.md). diff --git a/Sources/Shared/PasskeyManager.swift b/Sources/Shared/PasskeyManager.swift index a682e1b..282e07a 100644 --- a/Sources/Shared/PasskeyManager.swift +++ b/Sources/Shared/PasskeyManager.swift @@ -136,67 +136,16 @@ public class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, let attestationObject = rawAttestationObject.base64URLEncodedString() let clientDataJson = credentialRegistration.rawClientDataJSON.base64URLEncodedString() let credentialId = credentialRegistration.credentialID.base64URLEncodedString() - - let attestation = Attestation( - credentialId: credentialId, clientDataJson: clientDataJson, attestationObject: attestationObject) + credentialId: credentialId, clientDataJson: clientDataJson, + attestationObject: attestationObject) let registrationResult = PasskeyRegistrationResult( challenge: challenge, attestation: attestation) notifyRegistrationCompleted(result: registrationResult) return - // credentialRegistration.rawAttestationObject?.base64URLEncodedString() - // - // - // guard - // let clientDataJSON = try? JSONDecoder().decode( - // ClientDataJSON.self, from: credentialRegistration.rawClientDataJSON) - // else { - // notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON) - // return - // } - // - // guard let rawAttestationData = credentialRegistration.rawAttestationObject else { - // notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation) - // return - // } - - // guard - // let attestation = try? JSONDecoder().decode( - // ClientDataJSON.self, from: rawAttestationData) - // else { - // notifyRegistrationFailed(error: PasskeyRegistrationError.invalidClientDataJSON) - // return - // } - - // do { - // guard let jsonData = try JSONSerialization.data(withJSONObject: rawAttestationData, options: []) - // else { - // notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation) - // return - // } - // - // guard let jsonString = String(data: jsonData, encoding: .utf8) else { - // notifyRegistrationFailed(error: PasskeyRegistrationError.invalidAttestation) - // return - // } - // } - // catch { - // - // } - - // let attestation = - // String(data: attestationData, encoding: .utf8) ?? "Invalid attestation encoding" - // - // let challenge = clientDataJSON.challenge - - // let registrationResult = PasskeyRegistrationResult( - // challenge: challenge, attestation: attestation) - // - // notifyRegistrationCompleted(result: registrationResult) - case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: logger.log("A passkey was used to sign in: \(credentialAssertion)") notifyPasskeyAssertionCompleted(result: credentialAssertion) diff --git a/Tests/TurnkeySDKTests/TurnkeySDKTests.swift b/Tests/TurnkeySDKTests/TurnkeySDKTests.swift index fa44a20..c32f6dc 100644 --- a/Tests/TurnkeySDKTests/TurnkeySDKTests.swift +++ b/Tests/TurnkeySDKTests/TurnkeySDKTests.swift @@ -185,18 +185,17 @@ final class TurnkeySDKTests: XCTestCase { let email = "taylor+swift-sdk-test@turnkey.io" let targetPublicKey = "04d3f967632eb6a317059a164b7b71704c22fb2b0f20e6f27f62fdadeea14da558318a88bb9bb06c5886397666b4f1a1e3b92337c3ebebb4d570d4c735bc46fe83" - // Data(hexString: apiPrivateKey!) + let apiKeyName = "email-auth-key" let expirationSeconds = "3600" - let output = try await client.emailAuth( organizationId: organizationId!, email: email, targetPublicKey: targetPublicKey, apiKeyName: apiKeyName, expirationSeconds: expirationSeconds, - emailCustomization: Components.Schemas.EmailCustomizationParams() + emailCustomization: nil ) // Assert the response @@ -204,9 +203,9 @@ final class TurnkeySDKTests: XCTestCase { case .ok(let response): switch response.body { case .json(let emailAuthResponse): - // Assert the expected properties in the emailAuthResponse - XCTAssertNotNil(emailAuthResponse.activityId) - // XCTAssertEqual(emailAuthResponse.status, "Success") + + // Assert the expected properties in the emailAuthResponse + XCTAssertNotNil(emailAuthResponse.activity.id) } case .undocumented(let statusCode, let undocumentedPayload): // Handle the undocumented response diff --git a/docs/email-auth.md b/docs/email-auth.md new file mode 100644 index 0000000..d2226f9 --- /dev/null +++ b/docs/email-auth.md @@ -0,0 +1,125 @@ +# Email Authentication + +This guide provides a walkthrough for implementing email authentication in a Swift application using the [TurnkeyClient](../Sources/TurnkeySDK/TurnkeyClient.generated.swift) and [AuthKeyManager](../Sources/Shared/AuthKeyManager.swift). This process involves generating key pairs, handling encrypted bundles, and verifying user identity. + +For a more detailed explanation of the email authentication process, please refer to the [Turnkey API documentation](https://docs.turnkey.com/features/email-auth). + +## Prerequisites + +- A proxy server set up to handle authentication requests. +- Organization ID and API key name from your Turnkey account. + +## Step 1: Initialize the TurnkeyClient + +Create an instance of TurnkeyClient using a proxy URL to handle the authentication. +As a convenience, we've provided a [ProxyMiddleware](../Sources/Shared/ProxyMiddleware.swift) class that can be used to set up a proxy server to handle the authentication request. +We are using a proxy URL because an authenticated user is required to initiate the email authentication request. + +Note: The proxy server must be set up to handle the authentication request and return the exact payload received from the Turnkey API. If the response doesn't match exactly you'll see an undocumented response error in the logs. + +```swift +let proxyURL = "http://localhost:3000/api/email-auth" +let client = TurnkeyClient(proxyURL: proxyURL) +``` + +You may also forgo the use of the provided proxy middleware and make the request yourself. + +## Step 2: Generate Ephemeral Key Pair + +Use AuthKeyManager to generate a new ephemeral key pair for the email authentication flow. +This key pair is not persisted and is used temporarily during the authentication process. +Note: The 'domain' is used for scoping the key storage specific to an app and is optional for persisting the key. + +```swift +let authKeyManager = AuthKeyManager(domain: "your_domain") +let publicKey = try authKeyManager.createKeyPair() +``` + +## Step 3: Define Authentication Parameters + +```swift +let organizationId = "your_organization_id" +let email = "user@example.com" +let targetPublicKey = publicKey.toString(representation: .raw) +let expirationSeconds = "3600" +let emailCustomization = Components.Schemas.EmailCustomizationParams() // Customize as needed +``` + +## Step 4: Send Email Authentication Request + +With the TurnkeyClient initialized and the ephemeral key pair generated, you can now send an email authentication request. This involves using the emailAuth method of the TurnkeyClient, passing the necessary parameters. + +```swift +let emailAuthResult = try await client.emailAuth( + organizationId: organizationId, + email: email, + targetPublicKey: targetPublicKey, + apiKeyName: "your_api_key_name", + expirationSeconds: expirationSeconds, + emailCustomization: emailCustomization +) +``` + +After sending the email authentication request, it's important to handle the response appropriately.If the authentication is successful, you should save the user's sub-organizationId from the response for future use. You'll need this organizationId later to verify the user's keys. + +```swift +switch emailAuthResult { +case .ok(let response): + // The user's sub-organizationId: + let organizationId = response.activity.organizationId + // Proceed with user session creation or update +case .undocumented(let statusCode, let undocumentedPayload): + // Handle error, possibly retry or log +} +``` + +## Step 6: Verify Encrypted Bundle + +After your user receives the encrypted bundle from Turnkey, via email, you need to decrypt this bundle to retrieve the necessary keys for further authentication steps. Use the [`decryptBundle`](../Sources/Shared/AuthKeyManager.swift?plain=1#L160) method from the `AuthKeyManager` to handle this. + +```swift +let (privateKey, publicKey) = try authManager.decryptBundle(encryptedBundle) +``` + +This method will decrypt the encrypted bundle and provide you with the private and public keys needed for the session. +At this point in the authentication process, you have two options: + +1. Prompt the user for passkey authentication (using the `PasskeyManager`) and add a passkey as an authenticator. +2. Save the API private key in the keychain and use that for subsequent authentication requests. + +Note: Since the decrypted API key is similar to a session key, it should be handled with the same level of security as authentication tokens. + +## Step 7: Initializing the TurnkeyClient and Verify the user + +After successfully decrypting the encrypted bundle and retrieving the private and public API keys, you can initialize a TurnkeyClient instance using these keys for further authenticated requests: + +```swift +// ... + +let apiPublicKey = try publicKey.toString(representation: .compressed) +let apiPrivateKey = try privateKey.toString(representation: .raw) + +// Initialize a new TurnkeyClient instance with the provided privateKey and publicKey +let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) +``` + +### Verifying User Credentials with getWhoami + +After initializing the TurnkeyClient with the decrypted API keys, it is recommended to verify the validity of these credentials. This can be done using the `getWhoami` method, which checks the active status of the credentials against the Turnkey API. + +```swift +do { + let whoamiResponse = try await turnkeyClient.getWhoami(organizationId: organizationId /* from emailAuthResult */) + + switch whoamiResponse { + case .ok(let response): + print("Credential verification successful: \(whoamiResponse)") + case .undocumented(let statusCode, let undocumentedPayload): + print("Error during credential verification: \(error)") + } +} catch { + print("Error during credential verification: \(error)") +} + + +``` diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.pbxproj b/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.pbxproj index 3bd6c95..fb8e1f8 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.pbxproj +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.pbxproj @@ -21,6 +21,11 @@ 1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */; }; 1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */; }; 1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */; }; + 1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */ = {isa = PBXBuildFile; productRef = 1EABCB292BE468B00037DD52 /* web3swift */; }; + 1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2B2BE491790037DD52 /* UserManager.swift */; }; + 1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB2E2BE498A50037DD52 /* UserModel.swift */; }; + 1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */; }; + 1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -59,6 +64,10 @@ 1E4FCBCF2BCDCAB80042A4B2 /* UserHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHomeViewController.swift; sourceTree = ""; }; 1E4FCBD12BCDCAD00042A4B2 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailAuthViewController.swift; sourceTree = ""; }; + 1EABCB2B2BE491790037DD52 /* UserManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManager.swift; sourceTree = ""; }; + 1EABCB2E2BE498A50037DD52 /* UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModel.swift; sourceTree = ""; }; + 1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionManager.swift; path = ../../../../../../../../Documents/SessionManager.swift; sourceTree = ""; }; + 1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTransactionViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,6 +76,7 @@ buildActionMask = 2147483647; files = ( 1E4CD78B2BE2CF180097E2CB /* TurnkeySDK in Frameworks */, + 1EABCB2A2BE468B00037DD52 /* web3swift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -110,7 +120,9 @@ 1E27801F2BCA349300AE790C /* TurnkeyiOSExample */ = { isa = PBXGroup; children = ( + 1EABCB2D2BE498930037DD52 /* Models */, 1E37D5972BD2F3A400962F0D /* Info.plist */, + 1EABCB2E2BE498A50037DD52 /* UserModel.swift */, 1E37D5962BD2EBD200962F0D /* TurnkeyiOSExample.entitlements */, 1E2780242BCA349400AE790C /* Assets.xcassets */, 1E2780262BCA349400AE790C /* Preview Content */, @@ -122,6 +134,9 @@ 1E37D5992BD2FFDD00962F0D /* LaunchScreen.storyboard */, 1E37D59D2BD3004D00962F0D /* Main.storyboard */, 1E81BE832BDC2755006A9A0A /* EmailAuthViewController.swift */, + 1EABCB2B2BE491790037DD52 /* UserManager.swift */, + 1EABCB6A2BE4B0000037DD52 /* SessionManager.swift */, + 1EABCB6E2BE56B4F0037DD52 /* SendTransactionViewController.swift */, ); path = TurnkeyiOSExample; sourceTree = ""; @@ -151,6 +166,13 @@ path = TurnkeyiOSExampleUITests; sourceTree = ""; }; + 1EABCB2D2BE498930037DD52 /* Models */ = { + isa = PBXGroup; + children = ( + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -169,6 +191,7 @@ name = TurnkeyiOSExample; packageProductDependencies = ( 1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */, + 1EABCB292BE468B00037DD52 /* web3swift */, ); productName = TurnkeyiOSExample; productReference = 1E27801D2BCA349300AE790C /* TurnkeyiOSExample.app */; @@ -244,6 +267,7 @@ mainGroup = 1E2780142BCA349300AE790C; packageReferences = ( 1E4351F82BCDDBAD00BF67F2 /* XCLocalSwiftPackageReference "../.." */, + 1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */, ); productRefGroup = 1E27801E2BCA349300AE790C /* Products */; projectDirPath = ""; @@ -291,8 +315,12 @@ files = ( 1E81BE842BDC2755006A9A0A /* EmailAuthViewController.swift in Sources */, 1E4FCBD02BCDCAB80042A4B2 /* UserHomeViewController.swift in Sources */, + 1EABCB6B2BE4B0000037DD52 /* SessionManager.swift in Sources */, + 1EABCB2F2BE498A50037DD52 /* UserModel.swift in Sources */, + 1EABCB2C2BE491790037DD52 /* UserManager.swift in Sources */, 1E37D5932BD2D77C00962F0D /* AccountManager.swift in Sources */, 1E4FCBCA2BCB71820042A4B2 /* AppDelegate.swift in Sources */, + 1EABCB6F2BE56B4F0037DD52 /* SendTransactionViewController.swift in Sources */, 1E4FCBD22BCDCAD00042A4B2 /* SignInViewController.swift in Sources */, 1E4FCBCC2BCDCA6C0042A4B2 /* SceneDelegate.swift in Sources */, ); @@ -632,11 +660,27 @@ }; /* End XCLocalSwiftPackageReference section */ +/* Begin XCRemoteSwiftPackageReference section */ + 1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/web3swift-team/web3swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.2.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 1E4CD78A2BE2CF180097E2CB /* TurnkeySDK */ = { isa = XCSwiftPackageProductDependency; productName = TurnkeySDK; }; + 1EABCB292BE468B00037DD52 /* web3swift */ = { + isa = XCSwiftPackageProductDependency; + package = 1EABCB282BE468B00037DD52 /* XCRemoteSwiftPackageReference "web3swift" */; + productName = web3swift; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 1E2780152BCA349300AE790C /* Project object */; diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bf09b30..407c4f3 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "5.3.0" } }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073", + "version" : "1.5.1" + } + }, { "identity" : "openapikit", "kind" : "remoteSourceControl", @@ -126,6 +135,15 @@ "version" : "4.3.0" } }, + { + "identity" : "web3swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/web3swift-team/web3swift.git", + "state" : { + "revision" : "60a56e3b9c4cd0568d84acde27af2f25d2a8ff4d", + "version" : "3.2.1" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift b/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift index 1ade6f6..88959fc 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample/AccountManager.swift @@ -7,384 +7,474 @@ import AuthenticationServices import Foundation +import os import Shared +import SwiftData import TurnkeySDK -import os extension NSNotification.Name { - static let UserSignedIn = Notification.Name("UserSignedInNotification") - static let ModalSignInSheetCanceled = Notification.Name("ModalSignInSheetCanceledNotification") - static let PasskeyRegistrationCompleted = Notification.Name( - "PasskeyRegistrationCompletedNotification") - static let PasskeyRegistrationFailed = Notification.Name("PasskeyRegistrationFailedNotification") - static let PasskeyRegistrationCanceled = Notification.Name( - "PasskeyRegistrationCanceledNotification") - static let InitEmailAuth = Notification.Name("InitEmailAuthNotification") + static let UserSignedIn = Notification.Name("UserSignedInNotification") + static let ModalSignInSheetCanceled = Notification.Name("ModalSignInSheetCanceledNotification") + static let PasskeyRegistrationCompleted = Notification.Name( + "PasskeyRegistrationCompletedNotification") + static let PasskeyRegistrationFailed = Notification.Name("PasskeyRegistrationFailedNotification") + static let PasskeyRegistrationCanceled = Notification.Name( + "PasskeyRegistrationCanceledNotification") + static let InitEmailAuth = Notification.Name("InitEmailAuthNotification") } class AccountManager: NSObject, ASAuthorizationControllerPresentationContextProviding, - ASAuthorizationControllerDelegate -{ - let domain = "turnkey-nextjs-demo-weld.vercel.app" - let parentOrgId = "70189536-9086-4810-a9f0-990d4e7cd622" - var authenticationAnchor: ASPresentationAnchor? - var isPerformingModalRequest = false - private var passkeyRegistration: PasskeyManager? - private let authKeyManager: AuthKeyManager - - override init() { - - self.authKeyManager = AuthKeyManager(domain: domain) - super.init() - // Add observers for passkey registration notifications - NotificationCenter.default.addObserver( - self, - selector: #selector(handlePasskeyRegistrationCompleted), - name: .PasskeyRegistrationCompleted, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handlePasskeyRegistrationFailed), - name: .PasskeyRegistrationFailed, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handlePasskeyRegistrationCanceled), - name: .PasskeyRegistrationCanceled, - object: nil - ) - } - - deinit { - // Remove observers when the AccountManager instance is deallocated - NotificationCenter.default.removeObserver(self) - } - // func signIn(anchor: ASPresentationAnchor, preferImmediatelyAvailableCredentials: Bool) { - func signIn(email: String, anchor: ASPresentationAnchor) async { - let turnkeyClient = TurnkeyClient(rpId: domain, presentationAnchor: anchor) - - // let organizationId = "acd0bc97-2af5-475b-bc34-0fa7ca3bdc75" - - do { - // Call the GetWhoami method on the TurnkeyClient instance - let output = try await turnkeyClient.getWhoami(organizationId: parentOrgId) - - // Assert the response - switch output { - case .ok(let response): - switch response.body { - case .json(let whoamiResponse): - print(whoamiResponse) - } - case .undocumented(let statusCode, let undocumentedPayload): - // Handle the undocumented response - if let body = undocumentedPayload.body { - // Convert the HTTPBody to a string - let bodyString = try await String(collecting: body, upTo: .max) - print("Undocumented response body: \(bodyString)") - } - print("Undocumented response: \(statusCode)") - } - } catch { - print("Error occurred: \(error)") + ASAuthorizationControllerDelegate { + let domain = "turnkey-nextjs-demo-weld.vercel.app" + let parentOrgId = "70189536-9086-4810-a9f0-990d4e7cd622" + var authenticationAnchor: ASPresentationAnchor? + var isPerformingModalRequest = false + private var passkeyRegistration: PasskeyManager? + private let authKeyManager: AuthKeyManager + private var currentEmail: String? + private var modelContext: ModelContext { + return AppDelegate.userModelContext } - isPerformingModalRequest = true - } - - func signInEmailAuth(email: String, anchor: ASPresentationAnchor) async { - - // For email auth we need to proxy the request to a backend that can stamp it - let proxyURL = "http://localhost:3000/api/email-auth" - // We create a proxied instance of the Turnkey Client that can proxy requests to the backend - let turnkeyClient = TurnkeyClient(proxyURL: proxyURL) - - do { - let publicKey = try authKeyManager.createKeyPair() - - var targetPublicKey = Data([0x04]) - let rawRepresentation = publicKey.rawRepresentation - targetPublicKey.append(rawRepresentation) - - let output = try await turnkeyClient.emailAuth( - organizationId: parentOrgId, - email: email, - targetPublicKey: targetPublicKey.map { String(format: "%02x", $0) }.joined(), - apiKeyName: "test-api-key-swift-sdk", - expirationSeconds: "3600", - emailCustomization: Components.Schemas.EmailCustomizationParams() - ) - - // Assert the response - switch output { - case .ok(let response): - switch response.body { - case .json(let emailAuthResponse): - print(emailAuthResponse) - DispatchQueue.main.async { - self.initEmailAuth() - } + override init() { + authKeyManager = AuthKeyManager(domain: domain) + super.init() + // Add observers for passkey registration notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePasskeyRegistrationCompleted), + name: .PasskeyRegistrationCompleted, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePasskeyRegistrationFailed), + name: .PasskeyRegistrationFailed, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePasskeyRegistrationCanceled), + name: .PasskeyRegistrationCanceled, + object: nil + ) + } + + deinit { + // Remove observers when the AccountManager instance is deallocated + NotificationCenter.default.removeObserver(self) + } + + // func signIn(anchor: ASPresentationAnchor, preferImmediatelyAvailableCredentials: Bool) { + func signIn(email: String, anchor: ASPresentationAnchor) async { + let turnkeyClient = TurnkeyClient(rpId: domain, presentationAnchor: anchor) + guard let user = getUser(email: email) else { + + return } - case .undocumented(let statusCode, let undocumentedPayload): - // Handle the undocumented response - if let body = undocumentedPayload.body { - let bodyString = try await String(collecting: body, upTo: .max) - print("Undocumented response body: \(bodyString)") + + guard let organizationId = user.subOrgId else { + print("no suborg id found on device") + return } - print("Undocumented response: \(statusCode)") - } - } catch { - print("Error occurred: \(error)") + + do { + // Call the GetWhoami method on the TurnkeyClient instance + let output = try await turnkeyClient.getWhoami(organizationId: organizationId) + + // Assert the response + switch output { + case let .ok(response): + switch response.body { + case let .json(whoamiResponse): + print(whoamiResponse) + } + case let .undocumented(statusCode, undocumentedPayload): + // Handle the undocumented response + if let body = undocumentedPayload.body { + // Convert the HTTPBody to a string + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") + } + } catch { + print("Error occurred: \(error)") + } + + isPerformingModalRequest = true } - } - - func verifyEncryptedBundle(bundle: String) async { - do { - let (privateKey, publicKey) = try authKeyManager.decryptBundle(bundle) - - let apiPublicKey = try publicKey.toString(representation: .compressed) - let apiPrivateKey = try privateKey.toString(representation: .raw) - - print("apiPrivateKey: \(apiPrivateKey) - apiPublicKey:\(apiPublicKey)") - // Initialize a new TurnkeyClient instance with the provided privateKey and publicKey - let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) - let response = try await turnkeyClient.getWhoami(organizationId: parentOrgId) - - // Assert the response - switch response { - case .ok(let response): - switch response.body { - case .json(let emailAuthResponse): - print(emailAuthResponse) + + func signInEmailAuth(email: String, anchor: ASPresentationAnchor) async { + // For email auth we need to proxy the request to a backend that can stamp it + let proxyURL = "http://localhost:3000/api/email-auth" + // We create a proxied instance of the Turnkey Client that can proxy requests to the backend + let turnkeyClient = TurnkeyClient(proxyURL: proxyURL) + + do { + let publicKey = try authKeyManager.createKeyPair() + + var targetPublicKey = Data([0x04]) + let rawRepresentation = publicKey.rawRepresentation + targetPublicKey.append(rawRepresentation) + + let output = try await turnkeyClient.emailAuth( + organizationId: parentOrgId, + email: email, + targetPublicKey: targetPublicKey.map { String(format: "%02x", $0) }.joined(), + apiKeyName: "test-api-key-swift-sdk", + expirationSeconds: "3600", + emailCustomization: Components.Schemas.EmailCustomizationParams() + ) + + // Assert the response + switch output { + case let .ok(response): + switch response.body { + case let .json(emailAuthResponse): + print(emailAuthResponse.activity.organizationId) + DispatchQueue.main.async { + self.initEmailAuth() + } + } + case let .undocumented(statusCode, undocumentedPayload): + // Handle the undocumented response + if let body = undocumentedPayload.body { + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") + } + } catch { + print("Error occurred: \(error)") } - case .undocumented(let statusCode, let undocumentedPayload): - // Handle the undocumented response - if let body = undocumentedPayload.body { - let bodyString = try await String(collecting: body, upTo: .max) - print("Undocumented response body: \(bodyString)") + } + + func verifyEncryptedBundle(bundle: String) async { + do { + let (privateKey, publicKey) = try authKeyManager.decryptBundle(bundle) + + let apiPublicKey = try publicKey.toString(representation: .compressed) + let apiPrivateKey = try privateKey.toString(representation: .raw) + + print("apiPrivateKey: \(apiPrivateKey) - apiPublicKey:\(apiPublicKey)") + // Initialize a new TurnkeyClient instance with the provided privateKey and publicKey + let turnkeyClient = TurnkeyClient(apiPrivateKey: apiPrivateKey, apiPublicKey: apiPublicKey) + let response = try await turnkeyClient.getWhoami(organizationId: parentOrgId) + + // Assert the response + switch response { + case let .ok(response): + switch response.body { + case let .json(emailAuthResponse): + print(emailAuthResponse) + } + case let .undocumented(statusCode, undocumentedPayload): + // Handle the undocumented response + if let body = undocumentedPayload.body { + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") + } + } catch { + print("Error occurred: \(error)") } - print("Undocumented response: \(statusCode)") - } - } catch { - print("Error occurred: \(error)") } - } - - func beginAutoFillAssistedPasskeySignIn(anchor: ASPresentationAnchor) { - self.authenticationAnchor = anchor - - let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( - relyingPartyIdentifier: domain) - - // Fetch the challenge from the server. The challenge needs to be unique for each request. - let challenge = Data() - let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest( - challenge: challenge) - - // AutoFill-assisted requests only support ASAuthorizationPlatformPublicKeyCredentialAssertionRequest. - let authController = ASAuthorizationController(authorizationRequests: [assertionRequest]) - authController.delegate = self - authController.presentationContextProvider = self - authController.performAutoFillAssistedRequests() - } - - func signUp(email: String, anchor: ASPresentationAnchor) { - self.authenticationAnchor = anchor - - passkeyRegistration = PasskeyManager(rpId: domain, presentationAnchor: anchor) - passkeyRegistration?.registerPasskey(email: email) - - isPerformingModalRequest = true - } - - func sendCreateSubOrgRequest(passkeyRegistrationResult: PasskeyRegistrationResult) async throws { - // For email auth we need to proxy the request to a backend that can stamp it - let proxyURL = "http://localhost:3001/api/sign-up" - - // Create an instance of TurnkeyClient - let client = TurnkeyClient(proxyURL: proxyURL) - - - let attestation: Components.Schemas.Attestation = .init(credentialId: passkeyRegistrationResult.attestation.credentialId, clientDataJson: passkeyRegistrationResult.attestation.clientDataJson, attestationObject: passkeyRegistrationResult.attestation.attestationObject, transports: [.AUTHENTICATOR_TRANSPORT_BLE]) - - // Define the test input - let subOrganizationName = "Test Sub Organization" - let rootUsers: [Components.Schemas.RootUserParams] = [ - .init( - userName: "user1", - userEmail: "user1@example.com", - apiKeys: [], - authenticators: [ - .init(authenticatorName: "Tuide - Simulator", challenge: passkeyRegistrationResult.challenge, attestation: attestation) + + func beginAutoFillAssistedPasskeySignIn(anchor: ASPresentationAnchor) { + authenticationAnchor = anchor + + let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: domain) + + // Fetch the challenge from the server. The challenge needs to be unique for each request. + let challenge = Data() + let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest( + challenge: challenge) + + // AutoFill-assisted requests only support ASAuthorizationPlatformPublicKeyCredentialAssertionRequest. + let authController = ASAuthorizationController(authorizationRequests: [assertionRequest]) + authController.delegate = self + authController.presentationContextProvider = self + authController.performAutoFillAssistedRequests() + } + + func signUp(email: String, anchor: ASPresentationAnchor) { + authenticationAnchor = anchor + SessionManager.shared.setCurrentUser(user: User(email: email)) + + passkeyRegistration = PasskeyManager(rpId: domain, presentationAnchor: anchor) + passkeyRegistration?.registerPasskey(email: email) + print("\(email) signup") + isPerformingModalRequest = true + } + + func sendCreateSubOrgRequest(passkeyRegistrationResult: PasskeyRegistrationResult, email: String) async throws -> Components.Schemas.CreateSubOrganizationResultV4? { +// guard let email else { +// throw NSError(domain: "AccountManagerError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Email not set"]) +// } + + // For email auth we need to proxy the request to a backend that can stamp it + let proxyURL = "http://localhost:3000/api/sign-up" + + // Create an instance of TurnkeyClient + let client = TurnkeyClient(proxyURL: proxyURL) + + let attestation: Components.Schemas.Attestation = .init(credentialId: passkeyRegistrationResult.attestation.credentialId, clientDataJson: passkeyRegistrationResult.attestation.clientDataJson, attestationObject: passkeyRegistrationResult.attestation.attestationObject, transports: [.AUTHENTICATOR_TRANSPORT_BLE]) + + // Define the test input + let subOrganizationName = "Test Sub Organization" + let rootUsers: [Components.Schemas.RootUserParams] = [ + .init( + userName: "user1", + userEmail: email, + apiKeys: [], + authenticators: [ + .init(authenticatorName: "Tuide - Simulator", challenge: passkeyRegistrationResult.challenge, attestation: attestation), + ] + ), ] - ) - ] - let rootQuorumThreshold: Int32 = 1 - let wallet: Components.Schemas.WalletParams = .init( - walletName: "Test Wallet", - accounts: [ - .init( - curve: .CURVE_SECP256K1, - pathFormat: .PATH_FORMAT_BIP32, - path: "m/44'/60'/0'/0/0", - addressFormat: .ADDRESS_FORMAT_ETHEREUM + let rootQuorumThreshold: Int32 = 1 + let wallet: Components.Schemas.WalletParams = .init( + walletName: "Test Wallet", + accounts: [ + .init( + curve: .CURVE_SECP256K1, + pathFormat: .PATH_FORMAT_BIP32, + path: "m/44'/60'/0'/0/0", + addressFormat: .ADDRESS_FORMAT_ETHEREUM + ), + ] + ) + let disableEmailRecovery = false + let disableEmailAuth = false + + // Call the createSubOrganization method on the TurnkeyClient instance + let output = try await client.createSubOrganization( + organizationId: parentOrgId, + subOrganizationName: subOrganizationName, + rootUsers: rootUsers, + rootQuorumThreshold: rootQuorumThreshold, + wallet: wallet, + disableEmailRecovery: disableEmailRecovery, + disableEmailAuth: disableEmailAuth ) - ] - ) - let disableEmailRecovery = false - let disableEmailAuth = false - - // Call the createSubOrganization method on the TurnkeyClient instance - let output = try await client.createSubOrganization( - organizationId: parentOrgId, - subOrganizationName: subOrganizationName, - rootUsers: rootUsers, - rootQuorumThreshold: rootQuorumThreshold, - wallet: wallet, - disableEmailRecovery: disableEmailRecovery, - disableEmailAuth: disableEmailAuth - ) - - // Assert the response - switch output { - case .ok(let response): - switch response.body { - case .json(let activityResponse): - print(activityResponse) - // Print the activity as JSON - // let encoder = JSONEncoder() - // encoder.outputFormatting = .prettyPrinted - // let jsonData = try encoder.encode(activityResponse.activity.result) - // if let jsonString = String(data: jsonData, encoding: .utf8) { - // print(jsonString) - // } - - } - case .undocumented(let statusCode, let undocumentedPayload): - // Handle the undocumented response - if let body = undocumentedPayload.body { - // Convert the HTTPBody to a string - let bodyString = try await String(collecting: body, upTo: .max) - print("Undocumented response body: \(bodyString)") - } - print("Undocumented response: \(statusCode)") + + // Assert the response + switch output { + case let .ok(response): + switch response.body { + case let .json(activityResponse): + let result = activityResponse.activity.result.createSubOrganizationResultV4 + return result + // Print the activity as JSON + // let encoder = JSONEncoder() + // encoder.outputFormatting = .prettyPrinted + // let jsonData = try encoder.encode(activityResponse.activity.result) + // if let jsonString = String(data: jsonData, encoding: .utf8) { + // print(jsonString) + // } + } + case let .undocumented(statusCode, undocumentedPayload): + // Handle the undocumented response + if let body = undocumentedPayload.body { + // Convert the HTTPBody to a string + let bodyString = try await String(collecting: body, upTo: .max) + print("Undocumented response body: \(bodyString)") + } + print("Undocumented response: \(statusCode)") + } + return nil } - } - @objc private func handlePasskeyRegistrationCompleted(_ notification: Notification) { - guard let result = notification.userInfo?["result"] as? PasskeyRegistrationResult else { - return + @objc private func handlePasskeyRegistrationCompleted(_ notification: Notification) { + guard let result = notification.userInfo?["result"] as? PasskeyRegistrationResult else { + return + } + + Task { + let user = SessionManager.shared.getCurrentUser() + guard let email = user?.email else { + print("no email") + return + } + let result = try await sendCreateSubOrgRequest(passkeyRegistrationResult: result, email: email) + + user?.walletAddress = result?.wallet?.addresses[0] + user?.subOrgId = result?.subOrganizationId + if(user != nil) { + modelContext.insert(user!) + try modelContext.save() + } + + DispatchQueue.main.async { + self.didFinishSignIn() + } + } } - print("handlePasskeyRegistrationCompleted \(result)") - Task { - try await sendCreateSubOrgRequest(passkeyRegistrationResult: result) - didFinishSignIn() + @objc private func handlePasskeyRegistrationFailed(_ notification: Notification) { + guard let error = notification.userInfo?["error"] as? PasskeyRegistrationError else { + return + } + + // Handle passkey registration failure + // ... + + isPerformingModalRequest = false } - - } - @objc private func handlePasskeyRegistrationFailed(_ notification: Notification) { - guard let error = notification.userInfo?["error"] as? PasskeyRegistrationError else { - return + @objc private func handlePasskeyRegistrationCanceled(_ notification: Notification) { + // Handle passkey registration cancellation + // ... + + didCancelModalSheet() } - // Handle passkey registration failure - // ... - - isPerformingModalRequest = false - } - - @objc private func handlePasskeyRegistrationCanceled(_ notification: Notification) { - // Handle passkey registration cancellation - // ... - - didCancelModalSheet() - } - - func authorizationController( - controller: ASAuthorizationController, - didCompleteWithAuthorization authorization: ASAuthorization - ) { - let logger = Logger() - switch authorization.credential { - case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: - logger.log("A new passkey was registered: \(credentialRegistration)") - // Verify the attestationObject and clientDataJSON with your service. - // The attestationObject contains the user's new public key to store and use for subsequent sign-ins. - // let attestationObject = credentialRegistration.rawAttestationObject - // let clientDataJSON = credentialRegistration.rawClientDataJSON - - // After the server verifies the registration and creates the user account, sign in the user with the new account. - didFinishSignIn() - case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: - logger.log("A passkey was used to sign in: \(credentialAssertion)") - - // Verify the below signature and clientDataJSON with your service for the given userID. - // let signature = credentialAssertion.signature - // let clientDataJSON = credentialAssertion.rawClientDataJSON - // let userID = credentialAssertion.userID - - // After the server verifies the assertion, sign in the user. - didFinishSignIn() - case let passwordCredential as ASPasswordCredential: - logger.log("A password was provided: \(passwordCredential)") - // Verify the userName and password with your service. - // let userName = passwordCredential.user - // let password = passwordCredential.password - - // After the server verifies the userName and password, sign in the user. - didFinishSignIn() - default: - fatalError("Received unknown authorization type.") + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + let logger = Logger() + switch authorization.credential { + case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: + logger.log("A new passkey was registered: \(credentialRegistration)") + // Verify the attestationObject and clientDataJSON with your service. + // The attestationObject contains the user's new public key to store and use for subsequent sign-ins. + // let attestationObject = credentialRegistration.rawAttestationObject + // let clientDataJSON = credentialRegistration.rawClientDataJSON + + // After the server verifies the registration and creates the user account, sign in the user with the new account. + didFinishSignIn() + case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: + logger.log("A passkey was used to sign in: \(credentialAssertion)") + + // Verify the below signature and clientDataJSON with your service for the given userID. + // let signature = credentialAssertion.signature + // let clientDataJSON = credentialAssertion.rawClientDataJSON + // let userID = credentialAssertion.userID + + // After the server verifies the assertion, sign in the user. + didFinishSignIn() + case let passwordCredential as ASPasswordCredential: + logger.log("A password was provided: \(passwordCredential)") + // Verify the userName and password with your service. + // let userName = passwordCredential.user + // let password = passwordCredential.password + + // After the server verifies the userName and password, sign in the user. + didFinishSignIn() + default: + fatalError("Received unknown authorization type.") + } + + isPerformingModalRequest = false } - isPerformingModalRequest = false - } - - func authorizationController( - controller: ASAuthorizationController, didCompleteWithError error: Error - ) { - let logger = Logger() - guard let authorizationError = error as? ASAuthorizationError else { - isPerformingModalRequest = false - logger.error("Unexpected authorization error: \(error.localizedDescription)") - return + func authorizationController( + controller: ASAuthorizationController, didCompleteWithError error: Error + ) { + let logger = Logger() + guard let authorizationError = error as? ASAuthorizationError else { + isPerformingModalRequest = false + logger.error("Unexpected authorization error: \(error.localizedDescription)") + return + } + + if authorizationError.code == .canceled { + // Either the system doesn't find any credentials and the request ends silently, or the user cancels the request. + // This is a good time to show a traditional login form, or ask the user to create an account. + logger.log("Request canceled. isPerformingModalReqest: \(self.isPerformingModalRequest)") + + if isPerformingModalRequest { + logger.log("didCancelModalSheet.") + didCancelModalSheet() + } + } else { + // Another ASAuthorization error. + // Note: The userInfo dictionary contains useful information. + logger.error("Error: \((error as NSError).userInfo)") + } + + isPerformingModalRequest = false } - if authorizationError.code == .canceled { - // Either the system doesn't find any credentials and the request ends silently, or the user cancels the request. - // This is a good time to show a traditional login form, or ask the user to create an account. - logger.log("Request canceled. isPerformingModalReqest: \(self.isPerformingModalRequest)") + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return authenticationAnchor! + } - if isPerformingModalRequest { - logger.log("didCancelModalSheet.") - didCancelModalSheet() - } - } else { - // Another ASAuthorization error. - // Note: The userInfo dictionary contains useful information. - logger.error("Error: \((error as NSError).userInfo)") + func didFinishSignIn() { + NotificationCenter.default.post(name: .UserSignedIn, object: nil) } - isPerformingModalRequest = false - } + func didCancelModalSheet() { + NotificationCenter.default.post(name: .ModalSignInSheetCanceled, object: nil) + } + + func initEmailAuth() { + NotificationCenter.default.post(name: .InitEmailAuth, object: nil) + } + + func getUser(email: String) -> User? { + let fetchDescriptor = FetchDescriptor(predicate: nil) // No predicate means fetch all users + do { + let users = try modelContext.fetch(fetchDescriptor) + // Now 'users' contains all user instances + for user in users { + print(user) // or handle each user as needed + } + print("No USERS ATALL") + } catch { + print("Failed to fetch users: \(error)") + } + + let userPredicate = #Predicate { + $0.email == email + } - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return authenticationAnchor! - } + // Assuming you have a way to identify the specific User, e.g., by a stored userID or currentEmail + let request = FetchDescriptor(predicate: userPredicate) + + do { + let users = try modelContext.fetch(request) + guard let user = users.first else { + print("no user found for email \(email)") + return nil + } + return user + } catch { + print("error getting user \(error)") + return nil + } + } - func didFinishSignIn() { - NotificationCenter.default.post(name: .UserSignedIn, object: nil) - } + func updateUser(email: String, walletAddress: String?, subOrgId: String?, userName: String?) { + let context = modelContext - func didCancelModalSheet() { - NotificationCenter.default.post(name: .ModalSignInSheetCanceled, object: nil) - } + let userPredicate = #Predicate { + $0.email == email + } - func initEmailAuth() { - NotificationCenter.default.post(name: .InitEmailAuth, object: nil) - } + // Assuming you have a way to identify the specific User, e.g., by a stored userID or currentEmail + let request = FetchDescriptor(predicate: userPredicate) + do { + let users = try context.fetch(request) + if let user = users.first { + user.email = email + + try context.save() + SessionManager.shared.setCurrentUser(user: user) + print("Email updated successfully") + } else { + // Handle case where user is not found, possibly create a new user + let newUser = User(email: email, userName: userName, subOrgId: subOrgId, walletAddress: walletAddress) + context.insert(newUser) + try context.save() + print("New user created with email") + } + } catch { + print("Failed to update or create user: \(error)") + } + } } diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample/AppDelegate.swift b/example/TurnkeyiOSExample/TurnkeyiOSExample/AppDelegate.swift index 1e6cdbb..b73e1ad 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample/AppDelegate.swift +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample/AppDelegate.swift @@ -5,39 +5,68 @@ // Created by Taylor Dawson on 4/13/24. // +import SwiftData import TurnkeySDK import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - let accountManager = AccountManager() + let accountManager = AccountManager() + var modelContainer: ModelContainer? - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - // The override point for customization after app launch. - return true - } + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Initialize the ModelContainer + do { + modelContainer = try ModelContainer(for: User.self) + + } catch { + fatalError("Failed to initialize the model container: \(error)") + } - // MARK: UISceneSession Lifecycle + return true + } + + // Accessor for ModelContext + static var userModelContext: ModelContext { + // Ensure we are on the main thread when accessing the context + if Thread.isMainThread { + guard let context = (UIApplication.shared.delegate as? AppDelegate)?.modelContainer?.mainContext else { + fatalError("ModelContainer is not initialized") + } + return context + } else { + var context: ModelContext? + DispatchQueue.main.sync { + context = (UIApplication.shared.delegate as? AppDelegate)?.modelContainer?.mainContext + } + guard let mainContext = context else { + fatalError("ModelContainer is not initialized") + } + return mainContext + } + } - func application( - _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - // The system calls this method when creating a new scene. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration( - name: "Default Configuration", sessionRole: connectingSceneSession.role) - } + // MARK: UISceneSession Lifecycle - func application( - _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set - ) { - // The system calls this method when the user discards a scene session. - // If the system discards any sessions while the app isn't running, - // it calls this shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that are specific to the discarded scenes, because they don't return. - } + func application( + _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + // The system calls this method when creating a new scene. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration( + name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application( + _ application: UIApplication, didDiscardSceneSessions sceneSessions: Set + ) { + // The system calls this method when the user discards a scene session. + // If the system discards any sessions while the app isn't running, + // it calls this shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that are specific to the discarded scenes, because they don't return. + } } diff --git a/example/TurnkeyiOSExample/TurnkeyiOSExample/Main.storyboard b/example/TurnkeyiOSExample/TurnkeyiOSExample/Main.storyboard index 8c736ca..de48aa4 100644 --- a/example/TurnkeyiOSExample/TurnkeyiOSExample/Main.storyboard +++ b/example/TurnkeyiOSExample/TurnkeyiOSExample/Main.storyboard @@ -11,67 +11,88 @@ - + - - + + + + + + + + + + + + + + + + + + + @@ -88,7 +109,7 @@ - + @@ -97,13 +118,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -130,8 +424,8 @@ - - + + @@ -150,7 +444,6 @@