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 Method | JavaScript 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.
Popup Window Handling
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.