iOS SDK

The AgentKey iOS SDK provides native iOS integration for seamless authentication flows with a modern, Plaid-like provider selection interface. It handles authentication, token management, and connection status for iOS applications.

Installation

Add the iOS SDK to your Xcode project using Swift Package Manager:

  1. In Xcode, go to File > Add Package Dependencies
  2. Enter the repository URL: https://github.com/yourusername/agentkey-ios-sdk
  3. Select the version and add to your target

Manual Installation

  1. Download the AgentKey.framework from the releases page
  2. Drag and drop it into your Xcode project
  3. Make sure to add it to your target’s “Frameworks, Libraries, and Embedded Content”

Quick Start

Basic Setup

import AgentKey

class ViewController: UIViewController {
    private var agentKey: AKAgentKey?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupAgentKey()
    }
    
    private func setupAgentKey() {
        let configuration = AKConfiguration(
            linkToken: "link_token_from_your_backend",
            clientID: "your_client_id",
            clientSecret: "your_client_secret"
        )
        
        agentKey = AKAgentKey(configuration: configuration)
        agentKey?.delegate = self
    }
    
    @IBAction func connectAccountTapped(_ sender: UIButton) {
        agentKey?.present(from: self)
    }
}

// MARK: - AgentKey Delegate
extension ViewController: AKAgentKeyDelegate {
    func agentKey(_ agentKey: AKAgentKey, didCompleteWith result: AKConnectionResult) {
        switch result {
        case .success(let publicToken, let metadata):
            print("✅ Connected! Public Token: \(publicToken)")
            // Send publicToken to your backend for exchange
            
        case .failure(let error):
            print("❌ Connection failed: \(error.localizedDescription)")
            
        case .cancelled(let metadata):
            print("ℹ️ User cancelled connection")
        }
    }
    
    func agentKey(_ agentKey: AKAgentKey, didUpdateStatus status: String) {
        print("Status: \(status)")
    }
    
    func agentKeyDidRequestDismissal(_ agentKey: AKAgentKey) {
        // Handle any cleanup if needed
    }
}

Complete Integration with SwiftUI

import SwiftUI
import AgentKey

struct ContentView: View {
    @State private var publicToken: String?
    @State private var connectionStatus: String = ""
    @State private var isLoading: Bool = false
    @State private var agentKey: AKAgentKey?
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Connect Your Account")
                .font(.title)
                .fontWeight(.bold)
            
            if let token = publicToken {
                VStack {
                    Text("✅ Connected Successfully!")
                        .foregroundColor(.green)
                        .font(.headline)
                    
                    Text("Public Token:")
                        .font(.caption)
                        .foregroundColor(.secondary)
                    
                    Text(String(token.prefix(40)) + "...")
                        .font(.monospaced(.caption)())
                        .padding()
                        .background(Color.gray.opacity(0.1))
                        .cornerRadius(8)
                }
                .padding()
                .background(Color.green.opacity(0.1))
                .cornerRadius(12)
            }
            
            Button(action: {
                presentAgentKey()
            }) {
                HStack {
                    if isLoading {
                        ProgressView()
                            .scaleEffect(0.8)
                    }
                    Text(publicToken != nil ? "Connect Another Account" : "Connect Account")
                }
                .foregroundColor(.white)
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.blue)
                .cornerRadius(10)
            }
            .disabled(isLoading)
            
            if !connectionStatus.isEmpty {
                Text(connectionStatus)
                    .font(.caption)
                    .foregroundColor(.secondary)
                    .padding()
            }
        }
        .padding()
        .onAppear {
            setupAgentKey()
        }
    }
    
    private func setupAgentKey() {
        let configuration = AKConfiguration(
            linkToken: "your_link_token_here",
            clientID: "your_client_id",
            clientSecret: "your_client_secret",
            environment: .sandbox
        )
        
        agentKey = AKAgentKey(configuration: configuration)
        agentKey?.delegate = AgentKeyCoordinator(
            onSuccess: { publicToken, metadata in
                self.publicToken = publicToken
                self.isLoading = false
                self.connectionStatus = "Account connected successfully!"
            },
            onFailure: { error in
                self.isLoading = false
                self.connectionStatus = "Connection failed: \(error.localizedDescription)"
            },
            onStatusUpdate: { status in
                self.connectionStatus = status
            }
        )
    }
    
    private func presentAgentKey() {
        guard let agentKey = agentKey else { return }
        isLoading = true
        
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let rootViewController = windowScene.windows.first?.rootViewController {
            agentKey.present(from: rootViewController)
        }
    }
}

// Helper class to bridge delegate pattern with SwiftUI
class AgentKeyCoordinator: NSObject, AKAgentKeyDelegate {
    let onSuccess: (String, AKConnectionMetadata) -> Void
    let onFailure: (AKError) -> Void
    let onStatusUpdate: (String) -> Void
    
    init(onSuccess: @escaping (String, AKConnectionMetadata) -> Void,
         onFailure: @escaping (AKError) -> Void,
         onStatusUpdate: @escaping (String) -> Void) {
        self.onSuccess = onSuccess
        self.onFailure = onFailure
        self.onStatusUpdate = onStatusUpdate
    }
    
    func agentKey(_ agentKey: AKAgentKey, didCompleteWith result: AKConnectionResult) {
        switch result {
        case .success(let publicToken, let metadata):
            onSuccess(publicToken, metadata)
        case .failure(let error):
            onFailure(error)
        case .cancelled:
            onFailure(AKError.authenticationFailed("User cancelled"))
        }
    }
    
    func agentKey(_ agentKey: AKAgentKey, didUpdateStatus status: String) {
        onStatusUpdate(status)
    }
    
    func agentKeyDidRequestDismissal(_ agentKey: AKAgentKey) {
        // Handle dismissal if needed
    }
}

API Reference

AKAgentKey Class

The main class for handling AgentKey authentication flows.

Initialization

public init(configuration: AKConfiguration)

Methods

// Present the authentication flow
public func present(from presentingViewController: UIViewController)

// Factory method for quick setup
public static func create(
    linkToken: String,
    clientID: String,
    clientSecret: String,
    environment: AKEnvironment = .sandbox,
    backendURL: URL? = nil
) -> AKAgentKey

AKConfiguration

Configuration object for initializing AgentKey.

public struct AKConfiguration {
    public let linkToken: String        // Link token from your backend
    public let clientID: String         // Your client ID
    public let clientSecret: String     // Your client secret
    public let backendURL: URL?         // Custom backend URL (optional)
    public let environment: AKEnvironment // Environment setting
    
    public init(
        linkToken: String,
        clientID: String,
        clientSecret: String,
        backendURL: URL? = nil,
        environment: AKEnvironment = .sandbox
    )
}

AKAgentKeyDelegate Protocol

Delegate methods for handling authentication events.

public protocol AKAgentKeyDelegate: AnyObject {
    func agentKey(_ agentKey: AKAgentKey, didCompleteWith result: AKConnectionResult)
    func agentKey(_ agentKey: AKAgentKey, didUpdateStatus status: String)
    func agentKeyDidRequestDismissal(_ agentKey: AKAgentKey)
}

Types

// Environment options
public enum AKEnvironment: String {
    case development = "development"
    case sandbox = "sandbox" 
    case production = "production"
}

// Connection result
public enum AKConnectionResult {
    case success(String, AKConnectionMetadata)  // publicToken, metadata
    case failure(AKError)                       // error details
    case cancelled(AKConnectionMetadata?)       // user cancelled
}

// Connection metadata
public struct AKConnectionMetadata {
    public let sessionID: String?
    public let providerName: String?
    public let timestamp: Date?
    public let requestID: String?
    public let accounts: [AKAccount]
    public let connectedProviders: [String]
}

// Account information
public struct AKAccount {
    public let id: String
    public let name: String
    public let type: String
    public let providerName: String
    public let isActive: Bool
}

// Error types
public enum AKError: Error {
    case invalidToken(String)
    case tokenExpired
    case authenticationFailed(String)
    case networkError(Error)
    case initializationFailed(String)
    case sessionTimeout
    case invalidBackendURL
    case invalidResponse
    case noData
    case serverError(String)
    case unknownError(String)
}

Features

Provider Selection Interface

The iOS SDK includes a modern, Plaid-like provider selection screen featuring:

  • Clean Card Layout: Provider logos organized in a clean grid
  • Search Functionality: Search through providers by name or category
  • Organized Categories:
    • Utilities (PG&E, ConEd, LA DWP, SoCalGas)
    • Streaming (Netflix, Hulu, Disney+, Amazon Prime)
    • Internet & Cable (Comcast Xfinity, Spectrum)
    • Phone & Mobile (Verizon, AT&T)

Create link tokens from your backend:

AKAgentKey.createLinkToken(
    AKLinkTokenRequest(clientUserID: "user_123"),
    clientID: "your_client_id",
    clientSecret: "your_client_secret"
) { result in
    switch result {
    case .success(let response):
        // Use response.linkToken to initialize AgentKey
        let config = AKConfiguration(
            linkToken: response.linkToken,
            clientID: "your_client_id", 
            clientSecret: "your_client_secret"
        )
        let agentKey = AKAgentKey(configuration: config)
        
    case .failure(let error):
        print("Failed to create link token: \(error)")
    }
}

Authentication Flow

The iOS SDK follows this authentication pattern:

  1. Get Link Token: Fetch a link token from your backend
  2. Configure SDK: Initialize AKAgentKey with the link token
  3. Present UI: User selects provider and authenticates
  4. Receive Public Token: Your delegate receives a public token
  5. Send to Backend: Exchange the public token on your backend

Error Handling

Handle different types of errors in your delegate:

func agentKey(_ agentKey: AKAgentKey, didCompleteWith result: AKConnectionResult) {
    switch result {
    case .success(let publicToken, let metadata):
        // Handle successful connection
        handleSuccessfulConnection(publicToken: publicToken, metadata: metadata)
        
    case .failure(let error):
        switch error {
        case .invalidToken(let message):
            showAlert(title: "Invalid Token", message: message)
            
        case .tokenExpired:
            showAlert(title: "Token Expired", message: "Please try again")
            
        case .authenticationFailed(let message):
            showAlert(title: "Authentication Failed", message: message)
            
        case .networkError(let underlyingError):
            showAlert(title: "Network Error", message: underlyingError.localizedDescription)
            
        case .sessionTimeout:
            showAlert(title: "Session Timeout", message: "Please try again")
            
        default:
            showAlert(title: "Error", message: error.localizedDescription)
        }
        
    case .cancelled(let metadata):
        print("User cancelled authentication")
        // Optionally handle cancellation
    }
}

Best Practices

Always fetch link tokens from your secure backend:

// ✅ Good - Fetch from your secure backend
func fetchLinkToken() async throws -> String {
    let url = URL(string: "https://your-backend.com/api/link-token")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let requestBody = ["clientUserId": "user_123"]
    request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
    
    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONSerialization.jsonObject(with: data) as! [String: Any]
    
    return response["linkToken"] as! String
}

Token Exchange

Send public tokens to your backend immediately:

func agentKey(_ agentKey: AKAgentKey, didCompleteWith result: AKConnectionResult) {
    switch result {
    case .success(let publicToken, _):
        // ✅ Good - Immediately send to backend
        Task {
            await exchangePublicToken(publicToken)
        }
    default:
        break
    }
}

func exchangePublicToken(_ publicToken: String) async {
    // Send to your backend for secure exchange
    let url = URL(string: "https://your-backend.com/api/exchange-token")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let requestBody = ["publicToken": publicToken]
    request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody)
    
    do {
        let (_, _) = try await URLSession.shared.data(for: request)
        print("Token exchanged successfully")
    } catch {
        print("Token exchange failed: \(error)")
    }
}

User Experience

Provide clear feedback and loading states:

class ConnectionViewController: UIViewController {
    @IBOutlet weak var connectButton: UIButton!
    @IBOutlet weak var statusLabel: UILabel!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    func agentKey(_ agentKey: AKAgentKey, didUpdateStatus status: String) {
        DispatchQueue.main.async {
            self.statusLabel.text = status
        }
    }
    
    @IBAction func connectAccountTapped(_ sender: UIButton) {
        connectButton.isEnabled = false
        activityIndicator.startAnimating()
        statusLabel.text = "Initializing connection..."
        
        agentKey?.present(from: self)
    }
    
    func handleConnectionComplete() {
        DispatchQueue.main.async {
            self.connectButton.isEnabled = true
            self.activityIndicator.stopAnimating()
            self.statusLabel.text = "Ready to connect"
        }
    }
}

Environment Configuration

The SDK automatically infers the environment from your client secret prefix:

  • ak_dev_* → Development (http://localhost:4000)
  • ak_sandbox_* → Sandbox (https://sandbox-api.agentkey.com)
  • ak_prod_* → Production (https://api.agentkey.com)

You can also explicitly set the environment:

let configuration = AKConfiguration(
    linkToken: linkToken,
    clientID: clientID,
    clientSecret: clientSecret,
    environment: .production  // Explicit environment
)

Next Steps

  • Backend Integration - Set up your backend to create link tokens and exchange public tokens
  • Examples - See complete iOS integration examples
  • API Reference - Backend API documentation