Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e5c804d
Add OTPFlow and WalletSigner types
tomas-martins-crossmint May 20, 2026
fbdb10e
Refactor CrossmintTEE to use OTPFlow callback instead of @Published i…
tomas-martins-crossmint May 20, 2026
2312792
Add onAuthRequired to EmailSigner protocol and implementations
tomas-martins-crossmint May 20, 2026
1502348
Add getOrCreate to CrossmintWallets protocol with WalletSigner-based …
tomas-martins-crossmint May 20, 2026
17a5f8f
Remove isOTPRequired/provideOTP/cancelOTP from CrossmintSDK, re-expor…
tomas-martins-crossmint May 20, 2026
66c653a
Update demo app to use OTPFlow-based sheet and getOrCreate
tomas-martins-crossmint May 20, 2026
a2c4294
Add CrossmintTEEOTPFlowTests and GetOrCreateTests, update existing TE…
tomas-martins-crossmint May 20, 2026
06e958e
Fix review issues: extract makeOTPFlow, fix OTPSheet call sites, clea…
tomas-martins-crossmint May 21, 2026
dcff51f
Replace getOrCreate with split getWallet/createWallet, add doc commen…
tomas-martins-crossmint May 26, 2026
7e77e00
Fix OTPFlow review issues: UUID id, @MainActor closures, internal ini…
tomas-martins-crossmint May 26, 2026
fb42218
Add doc comments to OTPFlow
tomas-martins-crossmint May 26, 2026
de08208
Replace OTPFlow.Signer enum with plain email: String
tomas-martins-crossmint May 26, 2026
5846be3
Tighten OTPFlow doc comments
tomas-martins-crossmint May 26, 2026
2b11a38
Add Xcode file headers to new files
tomas-martins-crossmint May 26, 2026
bddb05b
Remove noise: strip headers from services files, revert WalletOptions…
tomas-martins-crossmint May 26, 2026
7aed9fd
Align tests with project conventions: extract helpers/mocks, fix func…
tomas-martins-crossmint May 26, 2026
6c9a615
Narrow PR to TEE/OTP mechanism, move onAuthRequired to wallet method …
tomas-martins-crossmint May 26, 2026
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 @@ -7,17 +7,7 @@ import CrossmintClient
import SwiftUI

extension View {
func otpSheet() -> some View {
modifier(OTPSheetModifier())
}
}

private struct OTPSheetModifier: ViewModifier {
@State private var showOTPView = false

func body(content: Content) -> some View {
content
.sheet(isPresented: $showOTPView) { OTPValidatorView() }
.onReceive(CrossmintSDK.shared.isOTPRequired) { showOTPView = $0 }
func otpSheet(flow: Binding<OTPFlow?>) -> some View {
sheet(item: flow) { OTPValidatorView(flow: $0) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import SwiftUI
import CrossmintClient

struct OTPValidatorView: View {
private let sdk: CrossmintSDK = .shared
let flow: OTPFlow

@State private var verificationCode: String = ""
@State private var showAlert: Bool = false
@State private var alertMessage: String = ""
@State private var verificationCode = ""
@State private var isVerifying = false
@State private var verificationSucceeded = false
@State private var errorMessage: String?
@State private var opacity: Double = 0

var body: some View {
Expand All @@ -15,9 +16,7 @@ struct OTPValidatorView: View {
Text("OTP Verification")
.font(.title3)
.fontWeight(.bold)

Spacer()

Button(action: dismiss) {
Image(systemName: "xmark")
.font(.system(size: 16))
Expand All @@ -29,6 +28,10 @@ struct OTPValidatorView: View {
}
.padding(.top, 20)

Text("Code sent to \(flow.email)")
.font(.caption)
.foregroundColor(.secondary)

CustomTextField(
placeholder: "Verification code",
text: $verificationCode,
Expand All @@ -38,45 +41,58 @@ struct OTPValidatorView: View {
.autocapitalization(.none)
.disableAutocorrection(true)

if let errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
}

PrimaryButton(
text: "Verify",
action: verifyCode,
isDisabled: verificationCode.isEmpty
text: verificationSucceeded ? "Verified — signing…" : (isVerifying ? "Verifying…" : "Verify"),
action: verify,
isDisabled: verificationCode.isEmpty || isVerifying || verificationSucceeded
)

Button("Resend code") { resend() }
.font(.caption)
.foregroundColor(.secondary)
.disabled(verificationSucceeded)

Spacer()
}
.padding(.horizontal, 24)
.background(Color(.systemBackground))
.opacity(opacity)
.onAppear {
withAnimation(AnimationConstants.easeIn()) {
opacity = 1
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Alert"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
withAnimation(AnimationConstants.easeIn()) { opacity = 1 }
}
}

private func verifyCode() {
guard !verificationCode.isEmpty else { return }
sdk.submit(otp: verificationCode)
private func verify() {
isVerifying = true
errorMessage = nil
Task {
do {
try await flow.verifyOTP(verificationCode)
verificationSucceeded = true
} catch {
isVerifying = false
errorMessage = error.localizedDescription
}
}
}

private func dismiss() {
withAnimation(AnimationConstants.easeOut()) {
opacity = 0
}
sdk.cancelTransaction()
flow.cancel()
}

private func showAlert(with message: String) {
alertMessage = message
showAlert = true
private func resend() {
Task {
do {
try await flow.sendOTP()
} catch {
errorMessage = error.localizedDescription
}
}
}
}

#Preview {
OTPValidatorView()
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class AppState {
var isLoadingBalance: Bool = false
var isCreatingWallet: Bool = false
var walletErrorMessage: String?
var pendingOTPFlow: OTPFlow?

// Per-chain state
private var walletCache: [SupportedChain: Wallet] = [:]
Expand Down Expand Up @@ -107,6 +108,31 @@ final class AppState {
isCreatingWallet = false
}

func loadOrCreateWallet(email: String) async {
let chain = selectedChain
guard !loadingChains.contains(chain) else { return }

notFoundChains.remove(chain)
balance = nil
loadingChains.insert(chain)
walletErrorMessage = nil

do {
let wallet = try await getOrCreateWallet(chain: chain, email: email)
walletCache[chain] = wallet
if chain == selectedChain {
await fetchBalance()
}
} catch {
if chain == selectedChain {
walletErrorMessage = error.userMessage
}
}

pendingOTPFlow = nil
loadingChains.remove(chain)
}

/// Switches chain without re-fetching if the wallet is already cached.
func switchChain(_ chain: SupportedChain, email: String) async {
guard chain != selectedChain else { return }
Expand Down Expand Up @@ -244,4 +270,52 @@ final class AppState {
)
}
}

private func getOrCreateWallet(chain: SupportedChain, email: String) async throws -> Wallet {
let options = WalletOptions(deviceSigner: true)
let onAuthRequired: @MainActor (OTPFlow) async -> Void = { [weak self] flow in
self?.pendingOTPFlow = flow
}
switch chain {
case .evm:
if let wallet = try await sdk.crossmintWallets.getWallet(
chain: EVMChain.baseSepolia,
recovery: EVMSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
) { return wallet }
return try await sdk.crossmintWallets.createWallet(
chain: EVMChain.baseSepolia,
recovery: EVMSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
)
case .solana:
if let wallet = try await sdk.crossmintWallets.getWallet(
chain: SolanaChain.solana,
recovery: SolanaSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
) { return wallet }
return try await sdk.crossmintWallets.createWallet(
chain: SolanaChain.solana,
recovery: SolanaSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
)
case .stellar:
if let wallet = try await sdk.crossmintWallets.getWallet(
chain: StellarChain.stellar,
recovery: StellarSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
) { return wallet }
return try await sdk.crossmintWallets.createWallet(
chain: StellarChain.stellar,
recovery: StellarSigners.email(email),
options: options,
onAuthRequired: onAuthRequired
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct PlaygroundView: View {
}
}
.environment(appState)
.otpSheet(flow: Bindable(appState).pendingOTPFlow)
.sheet(item: $presentedSheet) { sheet in
switch sheet {
case .signers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ struct AddSignerSheet: View {
}
}
.interactiveDismissDisabled(isAdding)
.otpSheet()
.otpSheet(flow: Bindable(appState).pendingOTPFlow)
}

private var passkeyHost: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ struct SignersView: View {
AddSignerSheet()
.environment(appState)
}
.otpSheet()
.otpSheet(flow: Bindable(appState).pendingOTPFlow)
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ struct SigningView: View {
}
}
.interactiveDismissDisabled(isSigning)
.otpSheet()
.otpSheet(flow: Bindable(appState).pendingOTPFlow)
}

private func sign() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ struct TransferView: View {
}
}
.interactiveDismissDisabled(isSending)
.otpSheet()
.otpSheet(flow: Bindable(appState).pendingOTPFlow)
}

private func send() async {
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ let package = Package(
name: "WalletTests",
dependencies: [
"Wallet",
"SecureStorage",
"TestsUtils"
],
resources: [
Expand Down
13 changes: 1 addition & 12 deletions Sources/CrossmintClient/SwiftUI/CrossmintSDK.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import CrossmintAuth
import Combine
@_exported import CrossmintCommonTypes
@_exported import CrossmintService
import Logger
import SwiftUI
import Utils
@_exported import Wallet
import Web
@_exported import Web

@MainActor private var sdkInstances = 0

Expand Down Expand Up @@ -46,16 +45,6 @@ final public class CrossmintSDK: ObservableObject {

let crossmintTEE: CrossmintTEE

public var isOTPRequired: Published<Bool>.Publisher {
crossmintTEE.$isOTPRequired
}
public func submit(otp: String) {
crossmintTEE.provideOTP(otp)
}
public func cancelTransaction() {
crossmintTEE.cancelOTP()
}

public var isProductionEnvironment: Bool {
crossmintService.isProductionEnvironment
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Logger/LogEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ public enum LogEvents {
/// Waiting for user OTP input
public static let otpWait = "signer.otp.wait"

/// onAuthRequired callback fired — caller notified of pending OTP
public static let otpCallbackFired = "signer.otp.callbackFired"

/// OTP received from user
public static let otpReceived = "signer.otp.received"

Expand Down
Loading
Loading