Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
// Created by Austin Feight on 11/24/25.
//

import CrossmintAuth
import CrossmintClient

let crossmintApiKey = "ck_staging_YOUR_API_KEY"
// swiftlint:disable:next force_try
let crossmintAuthManager = try! CrossmintAuthManager(apiKey: crossmintApiKey)
@MainActor var authManager: CrossmintAuthManager { CrossmintSDK.shared.authManager }
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ struct JWTSignInView: View {
}
isSigningIn = true
Task {
await crossmintAuthManager.setJWT(token)
await CrossmintSDK.shared.setJWT(token)
let authStatus = AuthenticationStatus.authenticated(email: email, jwt: token, secret: "")
withAnimation(AnimationConstants.easeInOut()) {
authenticationStatus = authStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct OTPSignInView: View {
isSigningIn = true
Task {
do {
try await crossmintAuthManager.sendEmailOtp(email: email)
try await authManager.sendEmailOtp(email: email)
isSigningIn = false
showOTPVerification = true
} catch let authError as AuthManagerError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import SwiftUI
import CrossmintClient

struct VerificationView: View {
private let sdk: CrossmintSDK = .shared

@Binding var authenticationStatus: AuthenticationStatus?

@State private var verificationCode: String = ""
Expand All @@ -12,10 +10,6 @@ struct VerificationView: View {
@State private var alertMessage: String = ""
@State private var opacity: Double = 0

private var authManager: AuthManager {
sdk.authManager
}

let email: String

var body: some View {
Expand Down Expand Up @@ -82,7 +76,7 @@ struct VerificationView: View {
isVerifying = true
Task {
do {
let status = try await crossmintAuthManager.confirmEmailOtp(email: email, code: verificationCode)
let status = try await authManager.confirmEmailOtp(email: email, code: verificationCode)

isVerifying = false

Expand All @@ -91,9 +85,8 @@ struct VerificationView: View {
opacity = 0
}

DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.duration) {
authenticationStatus = authStatus
}
try? await Task.sleep(for: .seconds(AnimationConstants.duration))
authenticationStatus = authStatus
}
} catch {
isVerifying = false
Expand All @@ -106,7 +99,7 @@ struct VerificationView: View {
private func resendCode() {
Task {
do {
try await crossmintAuthManager.sendEmailOtp(email: email)
try await authManager.sendEmailOtp(email: email)
showAlert(with: "A new verification code has been sent to your email.")
} catch {
showAlert(with: "Error sending new code: \(error.localizedDescription)")
Expand All @@ -121,10 +114,9 @@ struct VerificationView: View {
}

Task {
_ = await crossmintAuthManager.reset()
DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.duration) {
authenticationStatus = .nonAuthenticated
}
_ = await authManager.reset()
try? await Task.sleep(for: .seconds(AnimationConstants.duration))
authenticationStatus = .nonAuthenticated
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ struct PlaygroundView: View {
}

private func signOut() async {
try? await crossmintAuthManager.logout()
CrossmintSDK.shared.logout()
await CrossmintSDK.shared.logout()
authenticationStatus = .nonAuthenticated
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import CrossmintClient

@main
struct SmartWalletsDemoApp: App {
init() {
CrossmintSDK.configure(apiKey: crossmintApiKey, logLevel: .info)
}

var body: some Scene {
WindowGroup {
SplashScreen()
.crossmintNonCustodialSigner(
CrossmintSDK.shared(apiKey: crossmintApiKey, authManager: crossmintAuthManager, logLevel: .info)
)
.crossmintNonCustodialSigner()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ struct SplashScreen: View {
@State private var authenticationStatus: AuthenticationStatus?
@State private var transitionOpacity: Double = 0
@State private var error: Error?
private var authManager: AuthManager {
CrossmintSDK.shared.authManager
}

@ViewBuilder
private var splashContent: some View {
Expand Down Expand Up @@ -130,7 +127,7 @@ struct SplashScreen: View {
guard authenticationStatus == nil else { return }
isLoading = true
do {
authenticationStatus = try await crossmintAuthManager.authenticationStatus
authenticationStatus = try await authManager.authenticationStatus
} catch {
if case .signInRequired = error {
self.error = .invalidCredentialsStored
Expand Down
5 changes: 3 additions & 2 deletions Sources/CrossmintClient/ClientSDK.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import CrossmintAuth
import CrossmintService
import Wallet

protocol ClientSDK {
func crossmintWallets() -> CrossmintWallets
var authManager: AuthManager { get }
var isProductionEnvironment: Bool { get }
var authManager: CrossmintAuthManager { get }
var crossmintService: CrossmintService { get }
}
2 changes: 1 addition & 1 deletion Sources/CrossmintClient/CrossmintClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ actor CrossmintClient {
self.apiKey = apiKey
}

static func sdk(key: String, authManager: AuthManager? = nil) throws -> ClientSDK {
static func sdk(key: String, authManager: CrossmintAuthManager? = nil) throws -> ClientSDK {
lock.lock()
defer { lock.unlock() }

Expand Down
6 changes: 3 additions & 3 deletions Sources/CrossmintClient/CrossmintClientSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ final class CrossmintClientSDK: ClientSDK, Sendable {
private let apiKey: ApiKey
private let secureStorage: SecureStorage
private let secureWalletStorage: SecureWalletStorage
private let crossmintService: CrossmintService
let authManager: any CrossmintAuth.AuthManager
let crossmintService: CrossmintService
let authManager: CrossmintAuthManager

init(apiKey: ApiKey, authManager: AuthManager? = nil) {
init(apiKey: ApiKey, authManager: CrossmintAuthManager? = nil) {
self.apiKey = apiKey

guard let bundleId = Bundle.main.bundleIdentifier else {
Expand Down
95 changes: 49 additions & 46 deletions Sources/CrossmintClient/SwiftUI/CrossmintSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,34 @@ import Web

@MainActor
final public class CrossmintSDK: ObservableObject {
private static var _shared: CrossmintSDK?
@MainActor private static var _shared: CrossmintSDK?

public static var shared: CrossmintSDK {
guard let shared = _shared else {
let newInstance = CrossmintSDK()
_shared = newInstance
return newInstance
@MainActor public static var shared: CrossmintSDK {
guard let instance = _shared else {
fatalError(
"CrossmintSDK is not configured. " +
"Call CrossmintSDK.configure(apiKey:) before accessing CrossmintSDK.shared."
)
}
return shared
return instance
}

public static func shared(
apiKey: String,
authManager: AuthManager? = nil,
logLevel: LogLevel = .error
) -> CrossmintSDK {
if let existing = _shared {
return existing
/// Configures the SDK with the given API key. Must be called before accessing `CrossmintSDK.shared`.
/// Subsequent calls are ignored — the SDK can only be configured once per process.
@MainActor public static func configure(apiKey: String, logLevel: LogLevel = .error) {
guard _shared == nil else {
Logger.sdk.warn("CrossmintSDK.configure() called after SDK is already configured — ignoring")
return
}

Logger.level = logLevel
let newInstance = CrossmintSDK(apiKey: apiKey, authManager: authManager)
_shared = newInstance
return newInstance
_shared = CrossmintSDK(apiKey: apiKey)
}

private let sdk: ClientSDK

public let crossmintWallets: CrossmintWallets
public let authManager: AuthManager
public let isProductionEnvironment: Bool
public let authManager: CrossmintAuthManager
public let crossmintService: CrossmintService

let crossmintTEE: CrossmintTEE

Expand All @@ -56,46 +53,52 @@ final public class CrossmintSDK: ObservableObject {
crossmintTEE.cancelOTP()
}

private convenience init() {
#if DEBUG
if let apiKey = ProcessInfo.processInfo.environment["CROSSMINT_API_KEY"] {
Logger.client.info("Using API key from the environment variable.")
self.init(apiKey: apiKey)
return
}
#endif
Logger.client.error("Crossmint SDK requires an API key")
fatalError(
"Crossmint SDK requires an API key. " +
"Please call CrossmintSDK.shared(apiKey:) before accessing CrossmintSDK.shared"
)
public var isProductionEnvironment: Bool {
crossmintService.isProductionEnvironment
}

private init(apiKey: String, authManager: AuthManager? = nil) {
/// Sets a JWT for authentication. Use this when authenticating with an externally obtained token
/// rather than through the built-in OTP flow.
///
/// - Note: Unlike the TypeScript SDK's synchronous `setJwt`, this is `async` because it
/// updates actor-isolated state on `CrossmintAuthManager`.
public func setJWT(_ jwt: String) async {
await authManager.setJWT(jwt)
}

private init(apiKey: String) {
sdkInstances += 1
if sdkInstances > 1 {
Logger.sdk.error("Multiple SDK instances created, behaviour is undefined")
}

let innerSdk: ClientSDK
do {
sdk = try CrossmintClient.sdk(key: apiKey, authManager: authManager)
let authManager = sdk.authManager
self.crossmintWallets = sdk.crossmintWallets()
self.authManager = authManager
self.isProductionEnvironment = sdk.isProductionEnvironment
self.crossmintTEE = CrossmintTEE.start(
auth: authManager,
webProxy: DefaultWebViewCommunicationProxy(),
apiKey: apiKey,
isProductionEnvironment: isProductionEnvironment
)
innerSdk = try CrossmintClient.sdk(key: apiKey)
} catch {
Logger.client.error("Invalid Crossmint API key provided: \(error)")
fatalError("Invalid Crossmint API key provided. Please verify your API key is a valid client key.")
}

let authManager = innerSdk.authManager
sdk = innerSdk
crossmintWallets = innerSdk.crossmintWallets()
self.authManager = authManager
crossmintService = innerSdk.crossmintService
crossmintTEE = CrossmintTEE.start(
auth: authManager,
webProxy: DefaultWebViewCommunicationProxy(),
apiKey: apiKey,
isProductionEnvironment: innerSdk.crossmintService.isProductionEnvironment
)
}

public func logout() {
public func logout() async {
do {
_ = try await authManager.logout()
} catch {
Logger.sdk.warn("Logout request failed: \(error) — clearing local state anyway")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what can happen here if we're not logged out but clear local state? What's the difference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the intent was to avoid locking users out if the logout endpoint request fails. But on the current implementation, if that request throws, we'd also never clear the user's JWT and refresh tokens from keychain, which could lead to the user getting automatically logged in on the next app launch, if the client doesn't have any safeguards to that

the only part that can throw is the logout network call, so we could clear the credentials before making the request instead, so even if that fails, the client would still lose access to the JWT and the user wouldn't end up accidentally being signed in again, regardless of the request result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah all good as-is, didn't realize we have a logout network request

}
crossmintTEE.resetState()
}

Expand Down
10 changes: 3 additions & 7 deletions Sources/CrossmintClient/SwiftUI/View+NonCustodialSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,13 @@ final class InstanceTracker: ObservableObject, Sendable {
}

extension View {
public func crossmintNonCustodialSigner(_ sdk: CrossmintSDK) -> some View {
self.modifier(CrossmintNonCustodialSignerViewModifier(sdk: sdk))
public func crossmintNonCustodialSigner() -> some View {
self.modifier(CrossmintNonCustodialSignerViewModifier())
}
}

private struct CrossmintNonCustodialSignerViewModifier: ViewModifier {
private let crossmintTEE: CrossmintTEE

init(sdk: CrossmintSDK) {
crossmintTEE = sdk.crossmintTEE
}
private let crossmintTEE: CrossmintTEE = CrossmintSDK.shared.crossmintTEE

func body(content: Content) -> some View {
ZStack {
Expand Down
Loading