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.

Overview

This guide demonstrates how to implement authentication flows using NEAR Connect. The connectAndSignMessage method combines wallet connection and message signing in a single user approval, perfect for sign-in flows.
This code is extracted from the working SignInAndSignMessageDemoView in the NEAR Connect iOS example app.

Complete Authentication Example

import SwiftUI
import NEARConnect

struct SignInAndSignMessageDemoView: View {
    @EnvironmentObject var walletManager: NEARWalletManager
    @Environment(\.dismiss) private var dismiss
    var onLog: ((_ action: String, _ params: String, _ output: String, _ isError: Bool) -> Void)?

    @State private var message = "Sign in to NEAR Connect Demo"
    @State private var recipient = "near-connect-demo.near"
    @State private var isProcessing = false
    @State private var showError = false
    @State private var errorMessage = ""

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Text("Connect a wallet and sign a message in a single step. The wallet presents one approval screen for both sign-in and message signing.")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }

                Section(header: Text("Message Parameters")) {
                    VStack(alignment: .leading, spacing: 4) {
                        Text("Message")
                            .font(.caption)
                            .foregroundColor(.secondary)
                        TextEditor(text: $message)
                            .font(.system(.body, design: .monospaced))
                            .frame(height: 60)
                    }

                    TextField("Recipient", text: $recipient)
                        .textInputAutocapitalization(.never)
                        .autocorrectionDisabled()
                }

                if walletManager.isSignedIn {
                    Section {
                        HStack {
                            Text("Connected as")
                                .foregroundColor(.secondary)
                            Spacer()
                            Text(walletManager.currentAccount?.accountId ?? "")
                                .font(.caption)
                        }

                        Button(action: {
                            walletManager.disconnect()
                        }) {
                            Text("Disconnect first to test connect flow")
                                .foregroundColor(.red)
                        }
                    }
                }

                Section {
                    Button(action: connectAndSign) {
                        HStack {
                            Spacer()
                            if isProcessing {
                                ProgressView()
                                    .padding(.trailing, 8)
                            }
                            Text(isProcessing ? "Connecting..." : "Connect & Sign Message")
                            Spacer()
                        }
                    }
                    .disabled(isProcessing || message.isEmpty || recipient.isEmpty || walletManager.isSignedIn)
                }

                Section {
                    Text("A random nonce is generated automatically to prevent replay attacks.")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle("Connect & Sign")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Done") { dismiss() }
                        .disabled(isProcessing)
                }
            }
            .alert("Error", isPresented: $showError) {
                Button("OK", role: .cancel) { }
            } message: {
                Text(errorMessage)
            }
        }
    }

    private func connectAndSign() {
        let params = "message: \(message), recipient: \(recipient)"
        Task {
            isProcessing = true
            defer { isProcessing = false }

            do {
                let signInResult = try await walletManager.connectAndSignMessage(
                    message: message,
                    recipient: recipient
                )
                var output = "account: \(signInResult.account.accountId), wallet: \(signInResult.account.walletId)"
                if let pk = signInResult.account.publicKey {
                    output += ", publicKey: \(pk)"
                }
                if let sig = signInResult.signedMessage {
                    output += "\nsignedMessage: \(sig)"
                }
                onLog?("connectAndSignMessage", params, output, false)
            } catch {
                onLog?("connectAndSignMessage", params, error.localizedDescription, true)
                errorMessage = error.localizedDescription
                showError = true
            }
        }
    }
}

Step-by-Step Breakdown

1
Define Message Parameters
2
@State private var message = "Sign in to NEAR Connect Demo"
@State private var recipient = "near-connect-demo.near"
3
Set up the message to sign and the recipient (typically your app’s NEAR account).
4
Use a descriptive message that clearly explains what the user is signing. This improves security and user trust.
5
Call connectAndSignMessage
6
let signInResult = try await walletManager.connectAndSignMessage(
    message: message,
    recipient: recipient
)
7
This single method handles both wallet connection and message signing with one user approval.
8
Extract Account Information
9
let account = signInResult.account
print("Account ID: \(account.accountId)")
print("Wallet ID: \(account.walletId)")
if let publicKey = account.publicKey {
    print("Public Key: \(publicKey)")
}
10
Access the connected account details from the result.
11
Verify the Signed Message
12
if let signedMessage = signInResult.signedMessage {
    print("Signed Message: \(signedMessage)")
    // Send to your backend for verification
}
13
The signed message can be verified server-side to authenticate the user.

API Reference

func connectAndSignMessage(
    message: String,
    recipient: String,
    nonce: [UInt8]? = nil
) async throws -> SignInResult
Connects a wallet and signs a message in one flow.Parameters:
  • message: The message to sign (typically a sign-in prompt)
  • recipient: Your app’s NEAR account ID (message recipient)
  • nonce: Optional nonce bytes (auto-generated if nil)
Returns:
  • SignInResult containing account info and signed message
Throws:
  • Error if connection fails or user cancels
A random nonce is automatically generated to prevent replay attacks. You can provide a custom nonce if needed.

Authentication Patterns

Basic Sign-In

func signIn() async throws {
    let result = try await walletManager.connectAndSignMessage(
        message: "Sign in to MyApp",
        recipient: "myapp.near"
    )
    
    // User is now connected and authenticated
    print("Signed in as: \(result.account.accountId)")
}

Sign-In with Backend Verification

func signInWithVerification() async throws {
    // Step 1: Connect and sign
    let result = try await walletManager.connectAndSignMessage(
        message: "Sign in to MyApp at \(Date())",
        recipient: "myapp.near"
    )
    
    guard let signature = result.signedMessage else {
        throw AuthError.signatureMissing
    }
    
    // Step 2: Send to backend for verification
    let authToken = try await verifySignature(
        accountId: result.account.accountId,
        publicKey: result.account.publicKey ?? "",
        signature: signature
    )
    
    // Step 3: Store auth token
    UserDefaults.standard.set(authToken, forKey: "authToken")
}

private func verifySignature(
    accountId: String,
    publicKey: String,
    signature: String
) async throws -> String {
    // Call your backend API
    let url = URL(string: "https://api.myapp.com/auth/verify")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let body = [
        "accountId": accountId,
        "publicKey": publicKey,
        "signature": signature
    ]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    
    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONDecoder().decode(AuthResponse.self, from: data)
    
    return response.token
}

Time-Limited Authentication

func signInWithExpiry() async throws {
    let timestamp = Date().timeIntervalSince1970
    let expiryTimestamp = timestamp + (60 * 60) // 1 hour
    
    let message = """
    Sign in to MyApp
    Timestamp: \(timestamp)
    Expires: \(expiryTimestamp)
    """
    
    let result = try await walletManager.connectAndSignMessage(
        message: message,
        recipient: "myapp.near"
    )
    
    // Store both signature and expiry
    UserDefaults.standard.set(result.signedMessage, forKey: "signature")
    UserDefaults.standard.set(expiryTimestamp, forKey: "signatureExpiry")
}

Custom Nonce for Extra Security

func signInWithCustomNonce() async throws {
    // Generate a nonce (typically from your backend)
    let nonce = generateNonce()
    
    let result = try await walletManager.connectAndSignMessage(
        message: "Sign in to MyApp",
        recipient: "myapp.near",
        nonce: nonce
    )
    
    // Verify with backend using the same nonce
    try await verifyWithNonce(
        signature: result.signedMessage ?? "",
        nonce: nonce
    )
}

private func generateNonce() -> [UInt8] {
    var nonce = [UInt8](repeating: 0, count: 32)
    _ = SecRandomCopyBytes(kSecRandomDefault, nonce.count, &nonce)
    return nonce
}

User Flow

1

User taps Connect & Sign Message

The app calls connectAndSignMessage with a descriptive message.
2

Wallet UI appears

The WalletBridgeSheet shows available NEAR wallets.
3

User selects wallet

User chooses which wallet to connect (e.g., MyNearWallet, Meteor, etc.).
4

User approves in wallet

The wallet shows a single approval screen for both connection and message signing.
5

Wallet signs message

The wallet cryptographically signs the message with the user’s private key.
6

App receives result

The app receives account details and the signed message for verification.

Security Best Practices

Follow these security practices when implementing authentication:

Include Timestamp in Message

let message = """
Sign in to MyApp
Timestamp: \(Date().timeIntervalSince1970)
"""
Prevents old signatures from being reused.

Verify Signatures Server-Side

// NEVER trust client-side verification alone
// Always send the signature to your backend
let isValid = try await backend.verifySignature(
    accountId: result.account.accountId,
    signature: result.signedMessage ?? "",
    publicKey: result.account.publicKey ?? ""
)

Use Descriptive Messages

let message = """
Sign in to MyApp (myapp.near)
Action: Authentication
Timestamp: \(Date())
This signature will be used to verify your identity.
"""
Clear messages help users understand what they’re signing.

Store Auth State Securely

import Security

func storeAuthToken(_ token: String) {
    // Use Keychain for sensitive data
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "authToken",
        kSecValueData as String: token.data(using: .utf8)!
    ]
    SecItemAdd(query as CFDictionary, nil)
}

Common Use Cases

Wallet-Based Login

Replace traditional username/password with NEAR wallet authentication:
func login() async throws {
    let result = try await walletManager.connectAndSignMessage(
        message: "Sign in to access your account",
        recipient: "myapp.near"
    )
    
    // User is authenticated by their NEAR account
    await loadUserProfile(accountId: result.account.accountId)
}

Gated Content Access

Verify ownership before showing premium content:
func unlockPremiumContent() async throws {
    let result = try await walletManager.connectAndSignMessage(
        message: "Verify ownership to access premium content",
        recipient: "content.near"
    )
    
    let hasAccess = try await backend.checkAccess(
        accountId: result.account.accountId,
        signature: result.signedMessage ?? ""
    )
    
    if hasAccess {
        showPremiumContent()
    }
}

Multi-Factor Authentication

Add NEAR wallet as a second authentication factor:
func secondFactorAuth(username: String) async throws {
    let result = try await walletManager.connectAndSignMessage(
        message: "Second factor authentication for \(username)",
        recipient: "auth.near"
    )
    
    try await backend.verify2FA(
        username: username,
        accountId: result.account.accountId,
        signature: result.signedMessage ?? ""
    )
}
The connectAndSignMessage method is perfect for authentication because it combines connection and signing in a single user action, reducing friction in the sign-in flow.

Next Steps

Token Transfer

Send NEAR tokens after authentication

Smart Contracts

Call contracts with authenticated user

Error Handling

Handle authentication errors

Security

Best practices for secure authentication

Build docs developers (and LLMs) love