> ## 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.

# Advanced integration

> Production-grade integration with NFC, lifecycle events, and graceful degradation.

A production-style integration demonstrating custom configuration, lifecycle management, error handling, capability negotiation, and graceful degradation for environment-sensitive features like NFC.

## What this example demonstrates

* Typed capability slots for document capture with `CameraDetector` permission state
* Custom capability registration for NFC
* Runtime capability detection with pre-launch validation
* `CaptureResult`-based response encoding (no manual JSONValue dictionaries)
* Lifecycle management (background/foreground events)
* Delegate-based message observation and routing
* Graceful degradation for unsupported capabilities

## Complete source

The full app below is a single SwiftUI target that demonstrates pre-launch capability validation, typed slots for document capture, custom NFC capability registration, lifecycle event forwarding, and delegate-based message observation. Read through the section markers (`// MARK: -`) to navigate; each block is the production-style version of a pattern shown elsewhere in the docs.

```swift expandable theme={null}
import Combine
import CoreNFC
import GBGBridge
import SwiftUI

// MARK: - App Entry Point

@main
struct AdvancedApp: App {
    var body: some Scene {
        WindowGroup {
            JourneyLauncherView()
        }
    }
}

// MARK: - Journey Configuration

/// Centralized configuration for the identity verification journey.
struct JourneyConfig {
    let url: URL
    let requiredCapabilities: [String]
    let hostVersion: String

    static let passportVerification = JourneyConfig(
        url: URL(string: "https://journey.example.com/passport")!,
        requiredCapabilities: ["camera.document", "nfc.read"],
        hostVersion: "1.0.0"
    )

    static let documentOnly = JourneyConfig(
        url: URL(string: "https://journey.example.com/document")!,
        requiredCapabilities: ["camera.document"],
        hostVersion: "1.0.0"
    )
}

// MARK: - Journey Launcher (Pre-launch Validation)

struct JourneyLauncherView: View {
    @State private var selectedJourney: JourneyConfig = .passportVerification
    @State private var showJourney = false
    @State private var showCapabilityWarning = false

    private let deviceCapabilities = DeviceCapabilities.detect()

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                Text("Identity Verification")
                    .font(.title2)
                    .fontWeight(.semibold)

                // Journey selection
                VStack(spacing: 12) {
                    JourneyOptionButton(
                        title: "Passport Verification",
                        subtitle: "Document + NFC chip read",
                        requirements: ["camera.document", "nfc.read"],
                        deviceCapabilities: deviceCapabilities
                    ) {
                        selectedJourney = .passportVerification
                        attemptLaunch()
                    }

                    JourneyOptionButton(
                        title: "Document Verification",
                        subtitle: "Document capture only",
                        requirements: ["camera.document"],
                        deviceCapabilities: deviceCapabilities
                    ) {
                        selectedJourney = .documentOnly
                        attemptLaunch()
                    }
                }
                .padding(.horizontal)

                // Device capability summary
                DeviceCapabilitySummary(capabilities: deviceCapabilities)

                Spacer()
            }
            .padding(.top, 32)
            .navigationTitle("Verification")
            .navigationBarTitleDisplayMode(.inline)
            .fullScreenCover(isPresented: $showJourney) {
                JourneyContainerView(config: selectedJourney)
            }
            .alert("Capability Warning", isPresented: $showCapabilityWarning) {
                Button("Continue Anyway") {
                    showJourney = true
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                let missing = missingCapabilities(for: selectedJourney)
                Text("This journey requires \(missing.joined(separator: ", ")) which \(missing.count == 1 ? "is" : "are") not available on this device. The journey may not complete successfully.")
            }
        }
    }

    private func attemptLaunch() {
        let missing = missingCapabilities(for: selectedJourney)
        if missing.isEmpty {
            showJourney = true
        } else {
            showCapabilityWarning = true
        }
    }

    private func missingCapabilities(for journey: JourneyConfig) -> [String] {
        journey.requiredCapabilities.filter { capability in
            deviceCapabilities.isSupported(capability) != true
        }
    }
}

// MARK: - Device Capabilities Detection

struct DeviceCapabilities {
    let nfcAvailable: Bool
    let cameraResult: CameraDetector.Result

    static func detect() -> DeviceCapabilities {
        DeviceCapabilities(
            nfcAvailable: NFCTagReaderSession.readingAvailable,
            cameraResult: CameraDetector.check()
        )
    }

    func isSupported(_ capability: String) -> Bool {
        switch capability {
        case "camera.document": return cameraResult.hardwareAvailable
        case "nfc.read": return nfcAvailable
        default: return false
        }
    }
}

// MARK: - Journey Container (Full Integration)

struct JourneyContainerView: View {
    let config: JourneyConfig

    @StateObject private var host: BridgeHost
    @StateObject private var coordinator = JourneyCoordinator()
    @Environment(\.dismiss) private var dismiss
    @State private var journeyState: JourneyState = .loading
    @State private var showDocumentCamera = false

    init(config: JourneyConfig) {
        self.config = config
        _host = StateObject(wrappedValue: BridgeHost(hostVersion: config.hostVersion))
    }

    var body: some View {
        NavigationStack {
            ZStack {
                // Journey WebView
                BridgeWebView(url: config.url, host: host)
                    .opacity(journeyState == .active ? 1 : 0)

                // Loading overlay
                if journeyState == .loading {
                    ProgressView("Loading verification...")
                }

                // Error overlay
                if case .error(let message) = journeyState {
                    JourneyErrorView(message: message) {
                        journeyState = .loading
                        // WebView will reload via URL change mechanism
                    }
                }

                // Completion overlay
                if case .completed(let result) = journeyState {
                    JourneyCompleteView(result: result) {
                        dismiss()
                    }
                }
            }
            .navigationTitle("Verification")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        host.send(event: "journey.cancel", data: [
                            "reason": .string("user_dismissed")
                        ])
                        dismiss()
                    }
                }
            }
            .onAppear { setupBridge() }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
                host.send(event: "host.background", data: [:])
            }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
                host.send(event: "host.foreground", data: [:])
            }
        }
    }

    private func setupBridge() {
        // Set delegate
        host.delegate = coordinator
        coordinator.onStateChange = { [weak coordinator] newState in
            Task { @MainActor in
                journeyState = newState
            }
        }

        let capabilities = DeviceCapabilities.detect()

        // Set up typed slot for document capture
        if capabilities.cameraResult.hardwareAvailable {
            host.documentCapture.handler = { [weak host] request in
                guard let host else { return .cancelled(reason: "Host deallocated") }
                return await host.documentCapture.awaitCompletion()
            }
            host.documentCapture.permissionState = capabilities.cameraResult.permissionState
        }

        // Register NFC as a custom capability
        if capabilities.nfcAvailable {
            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
                }

                // In a real app, start NFC reader session and read the chip
                responder.respond(
                    status: .success,
                    data: [
                        "chipRead": .bool(true),
                        "dataGroups": .array([.string("DG1"), .string("DG2")])
                    ],
                    error: nil
                )
            }
        }

        // Mark as active after a brief delay (allows WebView to initialize)
        Task {
            try? await Task.sleep(nanoseconds: 500_000_000)
            await MainActor.run {
                if journeyState == .loading {
                    journeyState = .active
                }
            }
        }
    }
}

// MARK: - Journey State

enum JourneyState: Equatable {
    case loading
    case active
    case completed(JourneyResult)
    case error(String)

    static func == (lhs: JourneyState, rhs: JourneyState) -> Bool {
        switch (lhs, rhs) {
        case (.loading, .loading), (.active, .active): return true
        case (.completed(let a), .completed(let b)): return a.status == b.status
        case (.error(let a), .error(let b)): return a == b
        default: return false
        }
    }
}

struct JourneyResult {
    let status: String
    let referenceId: String?
}

// MARK: - Journey Coordinator (Delegate)

@MainActor
final class JourneyCoordinator: ObservableObject, BridgeHostDelegate {
    var onStateChange: ((JourneyState) -> Void)?

    func bridgeHost(_ host: BridgeHost, didReceive message: BridgeMessage) {
        // Monitor for journey lifecycle events
        switch message.payload.action {
        case "journey.started":
            onStateChange?(.active)

        case "journey.complete":
            let status = extractString(from: message.payload.data, key: "status") ?? "unknown"
            let refId = extractString(from: message.payload.data, key: "referenceId")
            onStateChange?(.completed(JourneyResult(status: status, referenceId: refId)))

        case "journey.error":
            let errorMsg = extractString(from: message.payload.data, key: "message") ?? "An error occurred"
            onStateChange?(.error(errorMsg))

        default:
            break
        }
    }

    func bridgeHost(_ host: BridgeHost, unhandledRequest message: BridgeMessage) {
        // Respond to unknown requests with unsupported
        host.respond(
            to: message.correlationId,
            status: .unsupported,
            error: BridgeErrorPayload(
                code: "UNSUPPORTED_ACTION",
                message: "Action '\(message.payload.action)' is not supported by this host",
                recoverable: false
            )
        )
    }

    private func extractString(from data: [String: JSONValue]?, key: String) -> String? {
        guard let value = data?[key], case .string(let str) = value else { return nil }
        return str
    }
}

// MARK: - Supporting Views

struct JourneyOptionButton: View {
    let title: String
    let subtitle: String
    let requirements: [String]
    let deviceCapabilities: DeviceCapabilities
    let action: () -> Void

    private var allRequirementsMet: Bool {
        requirements.allSatisfy { deviceCapabilities.isSupported($0) }
    }

    var body: some View {
        Button(action: action) {
            HStack {
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.headline)
                        .foregroundColor(.primary)
                    Text(subtitle)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                Spacer()
                if allRequirementsMet {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundColor(.green)
                } else {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .foregroundColor(.orange)
                }
            }
            .padding()
            .background(Color(.secondarySystemGroupedBackground))
            .cornerRadius(12)
        }
    }
}

struct DeviceCapabilitySummary: View {
    let capabilities: DeviceCapabilities

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Device Capabilities")
                .font(.caption)
                .fontWeight(.semibold)
                .foregroundColor(.secondary)

            HStack(spacing: 16) {
                CapabilityBadge(name: "Camera", available: capabilities.cameraAvailable)
                CapabilityBadge(name: "NFC", available: capabilities.nfcAvailable)
            }
        }
        .padding(.horizontal)
    }
}

struct CapabilityBadge: View {
    let name: String
    let available: Bool

    var body: some View {
        HStack(spacing: 4) {
            Image(systemName: available ? "checkmark.circle.fill" : "xmark.circle.fill")
                .foregroundColor(available ? .green : .red)
                .font(.caption)
            Text(name)
                .font(.caption)
                .foregroundColor(.primary)
        }
        .padding(.horizontal, 10)
        .padding(.vertical, 4)
        .background(
            Capsule()
                .fill(available ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
        )
    }
}

struct JourneyErrorView: View {
    let message: String
    let onRetry: () -> Void

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "exclamationmark.triangle")
                .font(.system(size: 48))
                .foregroundColor(.orange)
            Text("Verification Error")
                .font(.title3)
                .fontWeight(.semibold)
            Text(message)
                .multilineTextAlignment(.center)
                .foregroundColor(.secondary)
            Button("Retry") {
                onRetry()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

struct JourneyCompleteView: View {
    let result: JourneyResult
    let onDismiss: () -> Void

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: result.status == "success" ? "checkmark.circle.fill" : "xmark.circle.fill")
                .font(.system(size: 64))
                .foregroundColor(result.status == "success" ? .green : .red)
            Text(result.status == "success" ? "Verification Complete" : "Verification Failed")
                .font(.title3)
                .fontWeight(.semibold)
            if let refId = result.referenceId {
                Text("Reference: \(refId)")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            Button("Done") {
                onDismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

// Note: CameraDetector (from GBGBridge) handles AVFoundation imports internally
```

## Architecture overview

```mermaid theme={null}
graph TB
    subgraph "Launch Phase"
        DC[DeviceCapabilities.detect] --> JL[JourneyLauncherView]
        JL -->|validate requirements| CHECK{All capabilities<br/>available?}
        CHECK -->|Yes| LAUNCH[Launch Journey]
        CHECK -->|No| WARN[Show Warning Alert]
        WARN -->|Continue| LAUNCH
        WARN -->|Cancel| STOP[Stay on launcher]
    end

    subgraph "Journey Phase"
        LAUNCH --> JC[JourneyContainerView]
        JC --> HOST[BridgeHost]
        JC --> COORD[JourneyCoordinator]
        HOST --> WV[BridgeWebView]
        HOST --> DC2[documentCapture<br/>Typed Slot]
        HOST --> NFC[nfc.read<br/>Custom Capability]
        COORD -->|state changes| JC
    end

    subgraph "Lifecycle"
        BG[App Background] -->|event| HOST
        FG[App Foreground] -->|event| HOST
        CANCEL[User Cancel] -->|event + dismiss| HOST
    end
```

### Key patterns demonstrated

#### Pre-launch capability validation

Before starting the journey, the launcher checks whether all required capabilities are available:

```swift theme={null}
let missing = journey.requiredCapabilities.filter { capability in
    deviceCapabilities.isSupported(capability) != true
}

if missing.isEmpty {
    showJourney = true
} else {
    showCapabilityWarning = true  // Let user choose to continue or cancel
}
```

This prevents users from starting a journey that will fail partway through due to missing hardware.

#### Lifecycle events

The app sends events when entering background and returning to foreground. The web journey can use these to pause/resume timers, save state, or handle session timeouts.

```swift theme={null}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
    host.send(event: "host.background", data: [:])
}
```

#### Permission state with CameraDetector

`CameraDetector.check()` detects hardware and permission status. The result is assigned to the typed slot's `permissionState`, which is automatically included in capability query responses:

```swift theme={null}
let capabilities = DeviceCapabilities.detect()
host.documentCapture.permissionState = capabilities.cameraResult.permissionState
```

The web journey can now check `permissionState` before attempting capture and prompt the user to grant access if needed.

### Typed Slots vs Custom Capabilities

This example demonstrates both patterns side by side:

* **Document capture** uses a typed slot (`host.documentCapture`) — the SDK handles result encoding and busy rejection automatically.
* **NFC** uses `registerCustomCapability()` — the handler receives a `BridgeResponder` and builds the response manually.

### Graceful Degradation

NFC is detected at runtime. If not available, the launcher shows a warning, and the custom capability handler responds with `.unsupported` if somehow called.

## Running this example

To run the example end-to-end, follow these setup steps:

1. Add GBGBridge via SPM.
2. Add to Info.plist:
   ```xml theme={null}
   <key>NSCameraUsageDescription</key>
   <string>Camera is used for document capture during identity verification.</string>
   <key>NSNFCReaderUsageDescription</key>
   <string>NFC is used to read the chip in your identity document.</string>
   ```
3. For NFC: Add the **Near Field Communication Tag Reading** capability.
4. Update `journeyURL` to your web journey endpoint.
5. Build and run.

## Next steps

* [Capability Handling Guide](/docs/go-v2/developer-integration/sdks/ios/capability-handling) — Deep dive on capability patterns
* [Security Guide](/docs/go-v2/developer-integration/sdks/ios/security) — Production security checklist
* [API Reference](/docs/go-v2/developer-integration/sdks/ios/api-reference) — Complete type reference
