> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Capability Handling

> Typed slots, custom capabilities, and handler patterns.

This guide explains how GBGBridge handles capabilities — the native features that a host application can provide to web journeys. It covers typed capability slots (recommended), custom capability registration, the legacy configuration approach, capability negotiation, permission state, and graceful degradation patterns.

## What is a Capability?

A **capability** is a native device feature that the host app can provide to the web journey. Examples include:

* `camera.document`: Document photography
* `camera.selfie`: Facial capture for liveness checks
* `nfc.read`: Reading NFC chips on identity documents

Capabilities are identified by string keys using dot-separated namespaces.

## Three Ways to Declare Capabilities

GBGBridge provides three approaches, listed from most to least recommended:

| Approach                | Best For                                      | Init Method                                         |
| ----------------------- | --------------------------------------------- | --------------------------------------------------- |
| **Typed slots**         | Document/selfie capture                       | `init(hostVersion:)`                                |
| **Custom capability**   | NFC, biometrics, or any non-camera capability | `init(hostVersion:)` + `registerCustomCapability()` |
| **Configuration-based** | Full manual control                           | `init(configuration:)` + `register(handler:)`       |

All three can be used together. Typed slots take precedence over custom capabilities with the same ID.

## Typed Capability Slots (Recommended)

Typed slots are the recommended way to declare capture capabilities. Setting a `handler` on a slot simultaneously declares support and provides the implementation.

### Basic Setup

```swift theme={null}
let host = BridgeHost(hostVersion: "1.0.0")

// Declare document capture support
host.documentCapture.handler = { [weak host] request in
    guard let host else { return .cancelled(reason: "Host deallocated") }
    return await host.documentCapture.awaitCompletion()
}

// Declare selfie capture support
host.selfieCapture.handler = { [weak host] request in
    guard let host else { return .cancelled(reason: "Host deallocated") }
    return await host.selfieCapture.awaitCompletion()
}
```

### Available Slots

| Property               | Capability ID     | Action ID                 |
| ---------------------- | ----------------- | ------------------------- |
| `host.documentCapture` | `camera.document` | `camera.document.capture` |
| `host.selfieCapture`   | `camera.selfie`   | `camera.selfie.capture`   |

### Handler-as-Declaration

With typed slots, there is no separate "declaration" step. A slot's `isSupported` property is computed as `handler != nil && isEnabled`. When the web journey sends a `capability.query` request, only slots with handlers appear as supported.

```swift theme={null}
// Not supported — no handler set
host.documentCapture.isSupported  // false

// Supported — handler is set
host.documentCapture.handler = { request in ... }
host.documentCapture.isSupported  // true

// Temporarily disabled — handler set but isEnabled is false
host.documentCapture.isEnabled = false
host.documentCapture.isSupported  // false
```

### Returning Results

Typed slot handlers return `CaptureResult` values. The SDK encodes them into the bridge protocol format automatically — no manual `JSONValue` dictionary construction needed.

```swift theme={null}
// Document capture success
return .document(DocumentCaptureResult(
    imageData: imageData,
    width: 1920,
    height: 1080,
    mimeType: "image/jpeg"
))

// Selfie capture success
return .selfie(SelfieCaptureResult(
    previewImageData: previewData,
    width: 640,
    height: 480,
    encryptedBlob: encryptedData,
    unencryptedBlob: unencryptedData
))

// User cancelled
return .cancelled(reason: "User dismissed camera")

// Failure
return .failed(
    code: "CAMERA_DENIED",
    message: "Camera permission was denied",
    recoverable: true
)
```

### SwiftUI Integration with awaitCompletion()

The `awaitCompletion()` / `complete()` pattern bridges async handlers with SwiftUI's declarative presentation:

```swift expandable theme={null}
struct JourneyView: View {
    @StateObject private var host = BridgeHost(hostVersion: "1.0.0")
    @State private var showDocumentCamera = false

    var body: some View {
        BridgeWebView(url: journeyURL, host: host)
            .onChange(of: host.documentCapture.activeRequest?.correlationId) { _ in
                showDocumentCamera = host.documentCapture.activeRequest != nil
            }
            .fullScreenCover(isPresented: $showDocumentCamera, onDismiss: {
                // If dismissed without completing, cancel the request
                if host.documentCapture.activeRequest != nil {
                    host.documentCapture.complete(.cancelled(reason: "Dismissed"))
                }
            }) {
                DocumentCameraView { imageData, width, height in
                    host.documentCapture.complete(.document(
                        DocumentCaptureResult(
                            imageData: imageData,
                            width: width,
                            height: height
                        )
                    ))
                }
            }
            .onAppear { setupHandlers() }
    }

    private func setupHandlers() {
        host.documentCapture.handler = { [weak host] request in
            guard let host else { return .cancelled(reason: "Host deallocated") }
            return await host.documentCapture.awaitCompletion()
        }
    }
}
```

The flow is:

1. Web journey sends `camera.document.capture` request.
2. Handler runs, sets `activeRequest`, calls `awaitCompletion()` — suspends.
3. SwiftUI detects `activeRequest` change, presents camera `fullScreenCover`.
4. Camera completes, view calls `host.documentCapture.complete(.document(...))`.
5. Handler resumes with the result, SDK encodes and sends the response.
6. `activeRequest` resets to `nil`, cover dismisses.

### Busy Rejection

If a request arrives while another is already active on the same slot, the SDK automatically responds with an error:

```json theme={null}
{
  "status": "error",
  "error": {
    "code": "BUSY",
    "message": "A camera.document capture request is already in progress",
    "recoverable": true
  }
}
```

### Permission State

Each typed slot has a `permissionState` property. Populate it using `CameraDetector` so the web journey can check permissions before attempting capture:

```swift theme={null}
let camera = CameraDetector.check()
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
```

This information appears in the `capability.query` response:

```json theme={null}
{
  "camera.document": {
    "supported": true,
    "version": "1.0",
    "permissionState": "granted"
  }
}
```

### Enable/Disable at Runtime

Toggle `isEnabled` to temporarily disable a slot without removing the handler:

```swift theme={null}
// Disable selfie capture for this journey
host.selfieCapture.isEnabled = false

// Re-enable later
host.selfieCapture.isEnabled = true
```

When disabled, the slot reports `isSupported = false` in capability queries.

## Custom Capability Registration

For capabilities that don't have a typed slot, for example, NFC and biometrics, use `registerCustomCapability()`:

```swift expandable theme={null}
let host = BridgeHost(hostVersion: "1.0.0")

host.registerCustomCapability("nfc.read", version: "1.0") { request, responder in
    guard NFCTagReaderSession.readingAvailable else {
        responder.respond(
            status: .unsupported,
            data: nil,
            error: BridgeErrorPayload(
                code: "NFC_NOT_AVAILABLE",
                message: "NFC reading is not available on this device",
                recoverable: false
            )
        )
        return
    }

    do {
        let chipData = try await readNFCChip()
        responder.respond(
            status: .success,
            data: [
                "mrz": .string(chipData.mrz),
                "photo": .string(chipData.photo.base64EncodedString())
            ],
            error: nil
        )
    } catch {
        responder.respond(
            status: .error,
            data: nil,
            error: BridgeErrorPayload(
                code: "NFC_ERROR",
                message: error.localizedDescription,
                recoverable: true
            )
        )
    }
}
```

Custom capabilities automatically appear in `capability.query` responses as supported. If a typed slot exists for the same ID, the typed slot takes precedence.

## Configuration-Based Approach (Legacy)

The configuration-based approach gives you full manual control. Use it when you need explicit capability dictionaries with constraints.

```swift theme={null}
let configuration = BridgeConfiguration(
    hostVersion: "1.0.0",
    capabilities: [
        "camera.document": BridgeCapabilityInfo(
            supported: true,
            version: "1.0",
            constraints: [
                "maxResolution": .number(4096),
                "formats": .array([.string("jpeg"), .string("png")])
            ],
            permissionState: "granted"
        ),
        "nfc.read": BridgeCapabilityInfo(
            supported: true,
            version: "1.0"
        )
    ]
)

let host = BridgeHost(configuration: configuration)
host.register(handler: DocumentCaptureHandler())
host.register(handler: NFCReadHandler())
```

With this approach, you implement `BridgeCapabilityHandler` for each action:

```swift theme={null}
public final class NFCReadHandler: BridgeCapabilityHandler, @unchecked Sendable {
    public let action = "nfc.read"

    public func handle(request: BridgeMessage, responder: BridgeResponder) async {
        // Handle the request and call responder.respond(...)
    }
}
```

### Handler Lifecycle

1. **Registration** — Call `host.register(handler:)` before the web journey sends requests.
2. **Request routing** — When a matching request arrives, `handle(request:responder:)` is called in an async task.
3. **Response** — Call `responder.respond(...)` exactly once with the result.
4. **Unregistration** — Call `host.unregister(action:)` to remove a handler.

## Capability Negotiation

### How It Works

The web journey sends a `capability.query` request. GBGBridge's built-in `CapabilityQueryHandler` responds automatically.

When using `init(hostVersion:)`, the response is built dynamically from typed slots and custom capabilities. When using `init(configuration:)`, it uses the static `BridgeConfiguration` dictionary.

```mermaid theme={null}
sequenceDiagram
    participant Web as Web Journey
    participant Host as BridgeHost
    participant QH as CapabilityQueryHandler

    Web->>Host: request: capability.query
    Host->>QH: route (built-in handler)
    QH->>Host: respond with capabilities + permissionState
    Host->>Web: response: { environment, hostVersion, capabilities }
    Note over Web: Adapt journey flow<br/>based on capabilities<br/>and permissions
```

### Query Response

The web journey receives:

```json theme={null}
{
  "environment": "ios",
  "hostVersion": "1.0.0",
  "capabilities": {
    "camera.document": { "supported": true, "version": "1.0", "permissionState": "granted" },
    "camera.selfie": { "supported": true, "version": "1.0", "permissionState": "notDetermined" },
    "nfc.read": { "supported": true, "version": "1.0" }
  }
}
```

The `permissionState` field appears when the capability provides permission metadata (typed slots with `permissionState` set, or `BridgeCapabilityInfo` with `permissionState` non-nil).

## Environment-Specific Behavior

### The Problem

Not all environments support the same capabilities:

| Capability         | iOS Native      | Web (iframe) | Android Native          |
| ------------------ | --------------- | ------------ | ----------------------- |
| Camera capture     | Yes             | Limited      | Yes                     |
| NFC chip read      | Yes (iPhone 7+) | No           | Yes (varies)            |
| Face ID / Touch ID | Yes             | No           | Fingerprint/Face Unlock |

When a web journey includes an NFC step but the host doesn't support NFC, the journey needs to know *before* reaching that step.

### Detecting Environment from the Web Journey

The `capability.query` response includes an `environment` field (`"ios"`, `"android"`, or `"web"` for iframe hosts). The web journey uses both the environment and the capability flags to make routing decisions.

### Runtime Hardware Detection on iOS

Use `CameraDetector` for camera hardware and permission detection:

```swift theme={null}
let camera = CameraDetector.check()
// camera.hardwareAvailable — whether camera hardware exists
// camera.permissionState — .granted, .denied, .notDetermined, .restricted
```

For NFC, check at initialization time:

```swift theme={null}
import CoreNFC

let nfcSupported = NFCTagReaderSession.readingAvailable

if nfcSupported {
    host.registerCustomCapability("nfc.read", version: "1.0") { request, responder in
        // Handle NFC
    }
}
```

## Graceful Degradation Patterns

When a capability isn't available, your integration has two main routes: fall back to a web-based equivalent, or check upfront and prevent the user from starting a journey that won't complete. The patterns below show both.

### Pattern 1: Fall Back to Web or Skip

The web journey checks capabilities and adapts its flow. If a web-based fallback exists for the capability, the journey uses it. If there is no web equivalent, the step is skipped entirely.

```
Journey: Document Capture -> NFC Read -> Face Match -> Result

If camera.document is unsupported (web fallback exists):
Journey: Document Capture (web) -> NFC Read -> Face Match -> Result

If NFC is unsupported (no web fallback):
Journey: Document Capture -> Face Match -> Result (NFC skipped)
```

The host app doesn't need to do anything special — the web journey handles fallback and routing decisions based on the capability query response.

### Pattern 2: Check Permissions Before Starting

With permission state in the capability query, the web journey can detect permission issues and prompt the user:

```
If camera.document.permissionState == "denied":
    Show "Please enable camera access in Settings" before starting capture
```

### Pattern 3: Respond with Unsupported Status

If the web journey sends a request for a capability the host doesn't support, typed slots automatically respond with `.unsupported` when no handler is set. For custom capabilities, respond explicitly:

```swift theme={null}
responder.respond(
    status: .unsupported,
    data: nil,
    error: BridgeErrorPayload(
        code: "CAPABILITY_UNAVAILABLE",
        message: "NFC is not available on this device",
        recoverable: false
    )
)
```

## Dynamic Capability Updates

With typed slots, capability state is inherently dynamic:

* Set or clear `handler` to change support status.
* Toggle `isEnabled` to temporarily disable a slot.
* Update `permissionState` when permissions change (e.g., after returning from Settings).

The web journey should re-query capabilities after significant state changes (e.g., after the app returns from background) to pick up any changes.

## Next Steps

* [Security Guide](/docs/go-v2/developer-integration/sdks/ios/security) — Transport security and content policies
* [Messaging Guide](/docs/go-v2/developer-integration/sdks/ios/messaging) — Request/response patterns
* [Troubleshooting Guide](/docs/go-v2/developer-integration/sdks/ios/troubleshooting) — Diagnosing capability-related issues
