Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/frol/near-connect-ios/llms.txt

Use this file to discover all available pages before exploring further.

NEAR Connect iOS is built with a clean separation of concerns, following SwiftUI best practices while maintaining a persistent WebView bridge for wallet communication.

Component Overview

NEARConnect Package
├── NEARWalletManager          # Core manager (@MainActor, ObservableObject)
│   ├── bridgeWebView          # Persistent WKWebView instance
│   ├── coordinator            # WebViewCoordinator (delegates)
│   ├── Published state        # currentAccount, isBusy, showWalletUI
│   └── Continuations          # Async/await bridges

├── WebViewCoordinator         # WKWebView delegate handler
│   ├── WKScriptMessageHandler # JS → Swift messages
│   ├── WKNavigationDelegate   # URL handling
│   ├── WKUIDelegate           # Popup windows
│   └── popupWebViews[]        # Managed popup windows

├── BridgeWebViewContainer     # UIViewRepresentable wrapper
│   └── Wraps WKWebView for SwiftUI

├── WalletBridgeSheet          # Ready-to-use UI component
│   ├── Shows BridgeWebViewContainer
│   ├── Loading overlay
│   └── Close button

├── NEARAccount               # Account model (Codable, Sendable)
├── NEARError                 # Error types
├── NEARConnectEvent          # Internal bridge events
└── Resources/
    └── near-connect-bridge.html  # HTML + JavaScript bridge

Core Components

NEARWalletManager

The NEARWalletManager is the central coordinator marked with @MainActor to ensure all UI updates happen on the main thread.

Published Properties

@MainActor
public class NEARWalletManager: ObservableObject {
    // State that triggers UI updates
    @Published public var currentAccount: NEARAccount?
    @Published public var isBusy = false
    @Published public var lastError: String?
    @Published public private(set) var isBridgeReady = false
    @Published public var showWalletUI = false
    
    // Network configuration
    @Published public var network: Network = .mainnet
    
    // Computed properties
    public var isSignedIn: Bool { currentAccount != nil }
}
The @Published properties automatically trigger SwiftUI view updates when changed, providing reactive UI behavior without manual state management.

WebView Ownership

The manager owns the persistent WebView and its coordinator:
public private(set) var bridgeWebView: WKWebView!
private var coordinator: WebViewCoordinator!
This ownership model ensures:
  • The WebView lives as long as the manager
  • JavaScript state persists across UI presentations
  • Wallet sessions survive view hierarchy changes
  • Memory is properly managed when the manager is deallocated

Continuation Management

Async operations use checked continuations stored as optional properties:
private var signInContinuation: CheckedContinuation<NEARAccount, any Error>?
private var transactionContinuation: CheckedContinuation<TransactionResult, any Error>?
private var messageContinuation: CheckedContinuation<MessageSignResult, any Error>?
private var delegateActionContinuation: CheckedContinuation<DelegateActionResult, any Error>?
private var signInAndSignMessageContinuation: CheckedContinuation<SignInWithMessageResult, any Error>?
Each continuation represents a pending async operation. When a JavaScript event arrives, the corresponding continuation is resumed and set to nil.
Only one operation of each type can be in flight at a time. The isBusy flag provides additional protection against concurrent operations.

Persistence Layer

private let userDefaults: UserDefaults
private let accountStorageKey = "near_connected_account"

private func saveAccount(_ account: NEARAccount) {
    if let data = try? JSONEncoder().encode(account) {
        userDefaults.set(data, forKey: accountStorageKey)
    }
}

private func loadStoredAccount() {
    guard let data = userDefaults.data(forKey: accountStorageKey),
          let account = try? JSONDecoder().decode(NEARAccount.self, from: data) else {
        return
    }
    currentAccount = account
}
The account is loaded during initialization and saved whenever the user connects a wallet.

WebViewCoordinator

The WebViewCoordinator implements all WKWebView delegate protocols, acting as the bridge between WebKit and the Swift manager.

Responsibilities

1. Script Message Handling (WKScriptMessageHandler) Receives messages from JavaScript and converts them to typed Swift events:
func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    guard message.name == "nearConnect",
          let body = message.body as? [String: Any],
          let type = body["type"] as? String else {
        return
    }
    
    let event: NEARConnectEvent
    switch type {
    case "ready":
        event = .ready
    case "signIn":
        event = .signedIn(...)
    case "transactionResult":
        event = .transactionResult(...)
    // ... handle all event types
    }
    
    self.manager?.handleEvent(event)
}
2. Navigation Handling (WKNavigationDelegate) Intercepts navigation requests to handle deep links:
func webView(
    _ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
    guard let url = navigationAction.request.url else {
        decisionHandler(.allow)
        return
    }
    
    let isPopup = popupWebViews.contains(where: { $0 === webView })
    if isPopup, shouldOpenExternally(url) {
        decisionHandler(.cancel)
        UIApplication.shared.open(url)  // Open in system/native app
        webView.removeFromSuperview()
        popupWebViews.removeAll { $0 === webView }
        return
    }
    
    decisionHandler(.allow)
}
3. Popup Window Management (WKUIDelegate) Creates and manages popup WebViews for wallet authentication:
func webView(
    _ webView: WKWebView,
    createWebViewWith configuration: WKWebViewConfiguration,
    for navigationAction: WKNavigationAction,
    windowFeatures: WKWindowFeatures
) -> WKWebView? {
    let url = navigationAction.request.url
    
    if let url, shouldOpenExternally(url) {
        UIApplication.shared.open(url)
        return nil
    }
    
    // Enable cookies for Cloudflare-protected wallets
    configuration.websiteDataStore = .default()
    
    let popup = WKWebView(frame: webView.bounds, configuration: configuration)
    popup.navigationDelegate = self
    popup.uiDelegate = self
    
    manager.bridgeWebView.addSubview(popup)
    popupWebViews.append(popup)
    return popup
}
Popup windows are added as subviews of the bridge WebView, not directly to the view hierarchy. This ensures they move with the bridge when it’s presented/dismissed.
Popups are tracked and cleaned up automatically:
var popupWebViews: [WKWebView] = []

func closeAllPopups() {
    for popup in popupWebViews {
        popup.removeFromSuperview()
    }
    popupWebViews.removeAll()
}

func webViewDidClose(_ webView: WKWebView) {
    webView.removeFromSuperview()
    popupWebViews.removeAll { $0 === webView }
}

BridgeWebViewContainer

A UIViewRepresentable that wraps the persistent WebView for use in SwiftUI:
public struct BridgeWebViewContainer: UIViewRepresentable {
    public let webView: WKWebView
    
    public func makeUIView(context: Context) -> UIView {
        let container = UIView()
        container.backgroundColor = .systemBackground
        webView.frame = container.bounds
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        container.addSubview(webView)
        return container
    }
    
    public func updateUIView(_ uiView: UIView, context: Context) {
        if webView.superview !== uiView {
            webView.frame = uiView.bounds
            webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            uiView.addSubview(webView)
        }
    }
}
The updateUIView check ensures the WebView is only moved when necessary, preventing unnecessary view hierarchy changes.

WalletBridgeSheet

A ready-to-use full-screen sheet that combines the bridge WebView with a loading overlay and close button:
public struct WalletBridgeSheet: View {
    @EnvironmentObject var walletManager: NEARWalletManager
    @State private var didTrigger = false
    
    public var body: some View {
        ZStack {
            // Bridge WebView (always present)
            BridgeWebViewContainer(webView: walletManager.bridgeWebView)
                .ignoresSafeArea()
            
            // Loading overlay (shown while bridge initializes)
            if !walletManager.isBridgeReady {
                VStack(spacing: 16) {
                    ProgressView()
                    Text("Loading wallet connector...")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color(uiColor: .systemBackground))
                .ignoresSafeArea()
            }
            
            // Close button (always visible)
            VStack {
                HStack {
                    Spacer()
                    Button(action: cancelFlow) {
                        Image(systemName: "xmark.circle.fill")
                            .font(.title)
                            .symbolRenderingMode(.palette)
                            .foregroundStyle(.white, .gray.opacity(0.6))
                    }
                    .padding(.top, 8)
                    .padding(.trailing, 16)
                }
                Spacer()
            }
        }
        .onChange(of: walletManager.isBridgeReady) { _ in
            triggerConnectIfNeeded()
        }
        .onAppear {
            triggerConnectIfNeeded()
        }
        .onDisappear {
            walletManager.cleanUpOnDismiss()
        }
    }
}

Automatic Flow Triggering

The sheet automatically triggers pending wallet operations when it appears and the bridge is ready:
private func triggerConnectIfNeeded() {
    guard walletManager.isBridgeReady, !didTrigger else { return }
    
    if let params = walletManager.pendingSignMessageParams {
        // Connect with message signing
        didTrigger = true
        walletManager.pendingSignMessageParams = nil
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            walletManager.triggerConnectWithSignMessage(
                message: params.message,
                recipient: params.recipient,
                nonce: params.nonce
            )
        }
    } else if walletManager.pendingConnect {
        // Standard connect
        didTrigger = true
        walletManager.pendingConnect = false
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            walletManager.triggerWalletSelector()
        }
    }
}
The 0.3 second delay ensures the WebView has fully rendered before triggering JavaScript, preventing timing issues with the near-connect library initialization.

WebView Lifecycle

Initialization Phase

Presentation Phase

Transaction Phase

Cleanup Phase

Coordination Pattern

The architecture uses a coordinator pattern where the NEARWalletManager orchestrates communication between:

1. SwiftUI Views

  • Observe @Published properties
  • Bind showWalletUI to sheet presentation
  • Call async methods like connect(), sendNEAR(), etc.

2. WebView Layer

  • Execute JavaScript functions via evaluateJavaScript
  • Receive events via WKScriptMessageHandler
  • Manage popup windows via WKUIDelegate

3. Persistence Layer

  • Store account to UserDefaults on sign-in
  • Load account from UserDefaults on initialization
  • Clear account on disconnect

4. Network Layer

  • RPC calls for account queries
  • Network selection affects bridge initialization
  • Separate RPC endpoints for mainnet/testnet

Event Flow Example: Sending NEAR

┌─────────────┐         ┌──────────────────┐         ┌─────────────┐
│  SwiftUI    │         │ NEARWalletManager│         │  WebView    │
└──────┬──────┘         └────────┬─────────┘         └──────┬──────┘
       │                         │                          │
       │ sendNEAR(to:amount:)    │                          │
       │────────────────────────>│                          │
       │                         │                          │
       │                         │ Store continuation       │
       │                         │ showWalletUI = true      │
       │                         │                          │
       │<────────────────────────│                          │
       │ Sheet presents          │                          │
       │                         │                          │
       │                         │ evaluateJavaScript       │
       │                         │ (nearSignAndSendTx)      │
       │                         │─────────────────────────>│
       │                         │                          │
       │                         │                          │ JS shows
       │                         │                          │ approval UI
       │                         │                          │
       │                         │                          │ User approves
       │                         │                          │
       │                         │ postMessage(txResult)    │
       │                         │<─────────────────────────│
       │                         │                          │
       │                         │ Resume continuation      │
       │                         │ showWalletUI = false     │
       │                         │                          │
       │ Sheet dismisses         │                          │
       │<────────────────────────│                          │
       │                         │                          │
       │ Return TransactionResult│                          │
       │<────────────────────────│                          │
       │                         │                          │

Thread Safety

@MainActor Isolation

The NEARWalletManager is marked with @MainActor to ensure all property access and method calls happen on the main thread:
@MainActor
public class NEARWalletManager: ObservableObject {
    // All methods and properties are main-actor-isolated
}

WebKit Callbacks

WebKit delegate methods are nonisolated by default but use MainActor.assumeIsolated to safely update manager state:
nonisolated func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
) {
    MainActor.assumeIsolated {
        // Safe to access @MainActor properties here
        self.manager?.handleEvent(event)
    }
}
This pattern ensures all UI updates happen on the main thread without requiring explicit DispatchQueue.main.async calls throughout the codebase.

Memory Management

The architecture uses Swift’s automatic reference counting (ARC) with careful attention to reference cycles:
  • WebViewCoordinator holds a weak reference to the manager
  • The manager holds strong references to the WebView and coordinator
  • Popup WebViews are removed from the hierarchy and deallocated when closed
  • The manager is typically owned by the app’s root view via @StateObject
class WebViewCoordinator: NSObject {
    private weak var manager: NEARWalletManager?  // Weak to prevent cycle
    
    init(manager: NEARWalletManager) {
        self.manager = manager
    }
}

Build docs developers (and LLMs) love