Skip to main content

Overview

The Dynamic Swift SDK provides multiple authentication methods that are secure, user-friendly, and easy to implement. Users can sign in using their email address, phone number, passkeys, social media accounts, or external JWT tokens.
For testing purposes, we recommend starting with Email OTP authentication as it doesn’t require real phone numbers and is easier to set up.

Prerequisites

Before you begin, make sure you have:
  • Dynamic SDK initialized (see Installation Guide)
  • Valid environment ID configured
  • Authentication methods enabled in your Dynamic dashboard

Built-in Authentication UI

The easiest way to add authentication is using the built-in UI:
import DynamicSDKSwift

let sdk = DynamicSDK.instance()

// Show the authentication UI
sdk.ui.showAuth()
This displays a complete authentication flow with all enabled providers.

Email OTP Authentication

Email OTP (One-Time Password) authentication allows users to sign in using their email address.

How It Works

  1. User enters their email address
  2. SDK sends an OTP to their email
  3. User enters the OTP code
  4. SDK verifies the code and authenticates the user

Send OTP to Email

import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func sendEmailOTP(email: String) async {
    do {
        try await sdk.auth.email.sendOTP(email: email)
        print("OTP sent to \(email)")
        // Show OTP input UI
    } catch {
        print("Failed to send OTP: \(error)")
    }
}

Verify Email OTP

func verifyEmailOTP(code: String) async {
    do {
        try await sdk.auth.email.verifyOTP(token: code)
        print("Email verified successfully!")
        // User is now authenticated
    } catch {
        print("Invalid OTP: \(error)")
    }
}

Resend Email OTP

func resendEmailOTP() async {
    do {
        try await sdk.auth.email.resendOTP()
        print("OTP resent")
    } catch {
        print("Failed to resend OTP: \(error)")
    }
}

SMS OTP Authentication

SMS OTP authentication allows users to sign in using their phone number.

Send OTP via SMS

import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func sendSmsOTP(phoneNumber: String) async {
    do {
        let phoneData = PhoneData(
            dialCode: "+1",      // Country dial code
            iso2: "US",          // ISO country code
            phone: phoneNumber   // Phone number without country code
        )
        
        try await sdk.auth.sms.sendOTP(phoneData: phoneData)
        print("SMS OTP sent")
        // Show OTP input UI
    } catch {
        print("Failed to send SMS OTP: \(error)")
    }
}

Verify SMS OTP

func verifySmsOTP(code: String) async {
    do {
        try await sdk.auth.sms.verifyOTP(token: code)
        print("Phone verified successfully!")
        // User is now authenticated
    } catch {
        print("Invalid OTP: \(error)")
    }
}

Resend SMS OTP

func resendSmsOTP() async {
    do {
        try await sdk.auth.sms.resendOTP()
        print("SMS OTP resent")
    } catch {
        print("Failed to resend OTP: \(error)")
    }
}

Passkey Authentication

Passkeys provide a passwordless authentication experience using biometrics or device credentials.

Sign In with Passkey

import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func signInWithPasskey() async {
    do {
        _ = try await sdk.auth.passkey.signIn()
        print("Signed in with passkey!")
        // User is now authenticated
    } catch {
        print("Passkey sign-in failed: \(error)")
    }
}

Register a New Passkey

After authentication, users can register additional passkeys:
func registerPasskey() async {
    do {
        try await sdk.passkeys.registerPasskey()
        print("Passkey registered successfully!")
    } catch {
        print("Failed to register passkey: \(error)")
    }
}

External JWT Authentication

For apps with existing authentication systems, you can authenticate users with an external JWT:
import DynamicSDKSwift

let sdk = DynamicSDK.instance()

func signInWithExternalJwt(jwt: String) async {
    do {
        try await sdk.auth.externalAuth.signInWithExternalJwt(
            props: SignInWithExternalJwtParams(jwt: jwt)
        )
        print("Signed in with external JWT!")
    } catch {
        print("External JWT sign-in failed: \(error)")
    }
}

Authentication State Management

Check Authentication Status

import DynamicSDKSwift

let sdk = DynamicSDK.instance()

// Check current user
if let user = sdk.auth.authenticatedUser {
    print("User is authenticated: \(user.userId)")
    print("Email: \(user.email ?? "Not provided")")
} else {
    print("User is not authenticated")
}

// Get current auth token
if let token = sdk.auth.token {
    print("Auth token: \(token)")
}

Listen for Authentication Changes

Use Combine publishers for reactive state management:
import DynamicSDKSwift
import Combine

class AuthViewModel: ObservableObject {
    @Published var user: UserProfile?
    @Published var token: String?
    
    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()
    
    func startListening() {
        // Initial values
        user = sdk.auth.authenticatedUser
        token = sdk.auth.token
        
        // Listen for user changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newUser in
                self?.user = newUser
                if newUser == nil {
                    // User logged out - navigate to login
                }
            }
            .store(in: &cancellables)
        
        // Listen for token changes
        sdk.auth.tokenChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newToken in
                self?.token = newToken
            }
            .store(in: &cancellables)
    }
}

Logout

func logout() async {
    do {
        try await sdk.auth.logout()
        print("Logged out successfully")
        // Navigation to login is triggered by authenticatedUserChanges
    } catch {
        print("Logout failed: \(error)")
    }
}

Complete Authentication Flow Example

Here’s a complete example using SwiftUI:
import SwiftUI
import DynamicSDKSwift
import Combine

struct LoginView: View {
    @StateObject private var viewModel = LoginViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            // Email OTP Section
            TextField("Email", text: $viewModel.email)
                .textFieldStyle(.roundedBorder)
                .autocapitalization(.none)
                .keyboardType(.emailAddress)
            
            Button("Send Email OTP") {
                viewModel.sendEmailOTP()
            }
            .disabled(viewModel.email.isEmpty || viewModel.isLoading)
            
            // OTP Input (shown after sending)
            if viewModel.showOtpInput {
                TextField("Enter OTP", text: $viewModel.otpCode)
                    .textFieldStyle(.roundedBorder)
                    .keyboardType(.numberPad)
                
                Button("Verify OTP") {
                    viewModel.verifyEmailOTP()
                }
                .disabled(viewModel.otpCode.isEmpty || viewModel.isLoading)
            }
            
            Divider()
            
            // Social Login Buttons
            Button("Continue with Google") {
                viewModel.signInWithGoogle()
            }
            
            Button("Continue with Apple") {
                viewModel.signInWithApple()
            }
            
            Button("Sign in with Passkey") {
                viewModel.signInWithPasskey()
            }
            
            // Built-in Auth UI
            Divider()
            
            Button("Open Auth Flow") {
                viewModel.openAuthFlow()
            }
            
            // Error Message
            if let error = viewModel.errorMessage {
                Text(error)
                    .foregroundColor(.red)
                    .font(.caption)
            }
        }
        .padding()
    }
}

@MainActor
class LoginViewModel: ObservableObject {
    @Published var email = ""
    @Published var otpCode = ""
    @Published var showOtpInput = false
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let sdk = DynamicSDK.instance()
    
    func openAuthFlow() {
        sdk.ui.showAuth()
    }
    
    func sendEmailOTP() {
        guard !email.isEmpty else { return }
        
        isLoading = true
        errorMessage = nil
        
        Task {
            do {
                try await sdk.auth.email.sendOTP(email: email)
                showOtpInput = true
            } catch {
                errorMessage = "Failed to send OTP: \(error.localizedDescription)"
            }
            isLoading = false
        }
    }
    
    func verifyEmailOTP() {
        guard !otpCode.isEmpty else { return }
        
        isLoading = true
        errorMessage = nil
        
        Task {
            do {
                try await sdk.auth.email.verifyOTP(token: otpCode)
                // User is now authenticated - navigation handled by auth state listener
            } catch {
                errorMessage = "Invalid OTP: \(error.localizedDescription)"
            }
            isLoading = false
        }
    }
    
    func signInWithGoogle() {
        errorMessage = nil
        Task {
            do {
                try await sdk.auth.social.connect(provider: .google)
            } catch {
                errorMessage = "Google sign-in failed: \(error.localizedDescription)"
            }
        }
    }
    
    func signInWithApple() {
        errorMessage = nil
        Task {
            do {
                try await sdk.auth.social.connect(provider: .apple)
            } catch {
                errorMessage = "Apple sign-in failed: \(error.localizedDescription)"
            }
        }
    }
    
    func signInWithPasskey() {
        errorMessage = nil
        Task {
            do {
                _ = try await sdk.auth.passkey.signIn()
            } catch {
                errorMessage = "Passkey sign-in failed: \(error.localizedDescription)"
            }
        }
    }
}

Best Practices

Error Handling

Always handle errors gracefully and provide meaningful feedback to users:
do {
    try await sdk.auth.email.verifyOTP(token: code)
} catch {
    // Provide user-friendly error messages
    let userMessage = getErrorMessage(for: error)
    showAlert(message: userMessage)
}

func getErrorMessage(for error: Error) -> String {
    // Customize based on error type
    return "Authentication failed. Please try again."
}

User Experience

  • Show loading states during authentication
  • Provide clear error messages
  • Allow users to resend OTP codes
  • Implement proper validation for email and phone numbers

Security

  • Never store OTP codes in persistent storage
  • Clear sensitive data when users log out
  • Use secure network connections (HTTPS)
  • Implement proper session management

What’s Next

Now that you have authentication working, you can:
  1. Social Authentication - Add more social login options
  2. Wallet Operations - Work with user wallets
  3. Networks - Configure blockchain networks
The authentication flow is designed to be secure and user-friendly. The SDK handles all the complex security details, including OTP generation, verification, and session management, so you can focus on building great user experiences.