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 uses a persistent WebView bridge to wrap the JavaScript @hot-labs/near-connect library, giving your native iOS app access to the full NEAR wallet ecosystem without reimplementing wallet-specific protocols.

Architecture Overview

┌──────────────────────────────────────────────┐
│  iOS App (SwiftUI)                           │
│                                              │
│  NEARWalletManager                           │
│    ├── bridgeWebView (persistent WKWebView)  │
│    │     └── near-connect-bridge.html        │
│    │           └── @hot-labs/near-connect JS │
│    ├── connect() / disconnect()              │
│    ├── sendNEAR(to:amountYocto:)             │
│    ├── callFunction(contractId:methodName:)  │
│    └── signMessage(message:recipient:)       │
│                                              │
│  Communication:                              │
│    Swift → JS:  evaluateJavaScript()         │
│    JS → Swift:  WKScriptMessageHandler       │
└──────────────────────────────────────────────┘

The WebView Bridge Pattern

Persistent WebView Instance

The core of NEAR Connect iOS is a persistent WKWebView that lives for the entire lifetime of the NEARWalletManager. This design is critical for several reasons:
  • Session persistence: Wallet connections and JavaScript state survive across sheet presentations
  • Performance: Avoids reloading the bridge HTML and near-connect library on every operation
  • State continuity: Pending transactions and wallet flows can complete even if the UI is temporarily hidden
The WebView is created once during manager initialization:
private func setupBridgeWebView() {
    coordinator = WebViewCoordinator(manager: self)
    
    let config = WKWebViewConfiguration()
    config.allowsInlineMediaPlayback = true
    config.preferences.javaScriptCanOpenWindowsAutomatically = true
    
    let contentController = WKUserContentController()
    contentController.add(coordinator, name: "nearConnect")
    config.userContentController = contentController
    
    bridgeWebView = WKWebView(frame: CGRect(x: 0, y: 0, width: 400, height: 800), 
                               configuration: config)
}
The WebView dimensions are arbitrary since it will be resized when added to a container view. The important part is that it exists and has loaded the bridge page before any wallet operations begin.

Loading the Bridge Page

On initialization, the WebView loads a minimal HTML page from the bundle that imports the near-connect ES module from CDN:
private func loadBridgePage() {
    guard let htmlURL = Bundle.module.url(forResource: "near-connect-bridge", 
                                          withExtension: "html") else {
        lastError = "Bridge HTML not found in bundle"
        return
    }
    
    if let htmlContent = try? String(contentsOf: htmlURL, encoding: .utf8) {
        let modifiedHTML = htmlContent.replacingOccurrences(
            of: "window.location.search",
            with: "'?network=\(network.rawValue)'"
        )
        bridgeWebView.loadHTMLString(modifiedHTML, 
                                      baseURL: URL(string: "https://near-connect-bridge.local/")!)
    }
}
The HTML is modified at runtime to inject the network parameter (mainnet or testnet) before loading.

JavaScript-Swift Communication

Swift → JavaScript: evaluateJavaScript()

When your app needs to trigger a wallet operation, Swift calls JavaScript functions using evaluateJavaScript():
extension WKWebView {
    func callNEARConnect(_ functionCall: String) {
        evaluateJavaScript(functionCall) { _, error in
            if let error {
                print("[NEARConnect] JS error: \(error.localizedDescription)")
            }
        }
    }
}
Examples of Swift → JS calls:
Swift MethodJavaScript Function
connect()window.nearConnect()
disconnect()window.nearDisconnect()
sendNEAR()window.nearSignAndSendTransaction(receiverId, actions)
signMessage()window.nearSignMessage(message, recipient, nonce)
callFunction()window.nearSignAndSendTransaction(contractId, actions)

JavaScript → Swift: WKScriptMessageHandler

When the JavaScript bridge has updates (wallet connected, transaction complete, errors), it sends messages back to Swift using the window.webkit.messageHandlers.nearConnect.postMessage() API. The WebViewCoordinator implements WKScriptMessageHandler to receive these messages:
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(
            accountId: body["accountId"] as? String ?? "",
            publicKey: body["publicKey"] as? String,
            walletId: body["walletId"] as? String ?? "unknown"
        )
    case "transactionResult":
        event = .transactionResult(
            hash: body["transactionHash"] as? String ?? "unknown",
            rawResult: body["result"] as? String
        )
    // ... more event types
    }
    
    self.manager?.handleEvent(event)
}
All message handling is automatically dispatched to the main actor using MainActor.assumeIsolated to ensure thread safety when updating UI state.

Wallet Operation Flow

Here’s a complete example of how a wallet connection flows through the system:

1. User Initiates Connection

// User taps "Connect Wallet" button
walletManager.connect()

2. Manager Shows UI

public func connect() {
    pendingConnect = true
    showWalletUI = true  // Triggers fullScreenCover presentation
}

3. Sheet Appears and Triggers Selector

When WalletBridgeSheet appears and the bridge is ready, it calls:
private func triggerConnectIfNeeded() {
    guard walletManager.isBridgeReady, !didTrigger else { return }
    
    if walletManager.pendingConnect {
        didTrigger = true
        walletManager.pendingConnect = false
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            walletManager.triggerWalletSelector()
        }
    }
}
Which executes:
public func triggerWalletSelector() {
    bridgeWebView.callNEARConnect("window.nearConnect()")
}

4. JavaScript Shows Wallet Selector

The near-connect library renders its wallet selector UI in the WebView DOM.

5. User Selects Wallet

  • Web wallets (MyNearWallet, Meteor): Open in a popup WKWebView via window.open()
  • Native app wallets (HOT/Telegram): Deep link to the native app via custom URL scheme

6. Wallet Completes Sign-In

The wallet redirects back to the bridge page with authentication data. The JavaScript bridge extracts the account info and posts a message:
window.webkit.messageHandlers.nearConnect.postMessage({
    type: 'signIn',
    accountId: 'user.near',
    publicKey: 'ed25519:...',
    walletId: 'my-near-wallet'
});

7. Swift Receives Event

The coordinator receives the message and creates a NEARConnectEvent.signedIn event, which the manager handles:
func handleEvent(_ event: NEARConnectEvent) {
    switch event {
    case .signedIn(let accountId, let publicKey, let walletId):
        let account = NEARAccount(
            accountId: accountId,
            publicKey: publicKey,
            walletId: walletId
        )
        currentAccount = account
        saveAccount(account)  // Persist to UserDefaults
        signInContinuation?.resume(returning: account)
        signInContinuation = nil
        isBusy = false
        closePopups()
        showWalletUI = false  // Dismiss sheet
    }
}

8. UI Updates

The sheet dismisses automatically when showWalletUI becomes false, and your app’s UI updates to show the connected account.

Async/Await with Continuations

All wallet operations use Swift’s modern async/await API. Under the hood, they work with checked continuations that bridge the callback-based WebView communication:
public func sendNEAR(to receiverId: String, amountYocto: String) async throws -> TransactionResult {
    guard isSignedIn else { throw NEARError.notSignedIn }
    guard !isBusy else { throw NEARError.operationInProgress }
    
    isBusy = true
    showWalletUI = true
    
    return try await withCheckedThrowingContinuation { continuation in
        transactionContinuation = continuation
        bridgeWebView.callNEARConnect(
            "window.nearSignAndSendTransaction('\(receiverId)', '[...]')"
        )
    }
}
When the transaction completes, the handleEvent method resumes the continuation:
case .transactionResult(let hash, let rawResult):
    let result = TransactionResult(
        transactionHashes: [hash],
        rawResult: rawResult
    )
    transactionContinuation?.resume(returning: result)
    transactionContinuation = nil
    isBusy = false
    showWalletUI = false
Continuations are automatically cancelled if the user dismisses the sheet mid-flow, preventing hung async operations.

Session Persistence

Connected accounts are stored in UserDefaults so users don’t need to reconnect every time they launch your app:
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 stored account is loaded during manager initialization, and currentAccount is automatically populated if a previous session exists.
Only the account ID, public key, and wallet ID are stored locally. Private keys always remain in the wallet—your app never has access to them.
Web-based wallets often need to open authentication pages in separate windows. NEAR Connect iOS handles these via the WKUIDelegate:
func webView(
    _ webView: WKWebView,
    createWebViewWith configuration: WKWebViewConfiguration,
    for navigationAction: WKNavigationAction,
    windowFeatures: WKWindowFeatures
) -> WKWebView? {
    let popup = WKWebView(frame: webView.bounds, configuration: configuration)
    popup.navigationDelegate = self
    popup.uiDelegate = self
    
    manager.bridgeWebView.addSubview(popup)
    popupWebViews.append(popup)
    return popup
}
Popup WebViews are:
  • Created on demand when JavaScript calls window.open()
  • Added as subviews of the bridge WebView
  • Tracked in an array for cleanup
  • Automatically removed when the wallet flow completes
Deep links (like Telegram’s tg:// or NEAR Mobile’s near://) are intercepted and opened in the system:
private func shouldOpenExternally(_ url: URL) -> Bool {
    let scheme = url.scheme?.lowercased() ?? ""
    
    // Non-HTTP schemes open externally (custom deep links)
    if scheme != "http" && scheme != "https" {
        return true
    }
    
    // Known app-link domains
    let externalDomains = ["t.me", "telegram.me"]
    if let host = url.host?.lowercased(),
       externalDomains.contains(where: { host == $0 || host.hasSuffix(".\($0)") }) {
        return true
    }
    
    return false
}

Network Configuration

The manager supports both mainnet and testnet via the network property:
public enum Network: String, Sendable {
    case mainnet
    case testnet
}
The network is injected into the bridge HTML at load time and affects:
  • Which RPC endpoints are used for queries
  • Which wallet environments are shown in the selector
  • Where transactions are broadcast
Changing the network after initialization requires reloading the bridge page, which will disconnect any active wallet session.