> ## 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 capability validation, lifecycle events, and graceful degradation.

A production-style integration demonstrating pre-launch capability validation, lifecycle event forwarding, error state handling, completion routing, and graceful degradation for environment-sensitive features.

## What this example demonstrates

* Pre-launch capability detection with explicit user warning when requirements aren't met
* Journey state machine (loading → active → completed / failed / error)
* Lifecycle event forwarding (`visibilitychange` → `host.background` / `host.foreground`)
* Capability handler registration via `useBridgeCapability` with busy guards
* Custom capability registration for non-camera actions
* Delegate-based event routing (journey lifecycle → React state)
* Graceful degradation when capabilities are unavailable

## Complete source

The full source below is a single React file demonstrating pre-launch capability validation, journey state routing, lifecycle event forwarding, and busy-guarded capture handlers. Section markers are omitted — read straight through, or use the architecture diagram after the code to navigate.

```tsx expandable theme={null}
import {
  BridgeHostProvider,
  useBridgeHost,
  useBridgeCapability,
  useBridgeHostEvent,
} from "@gbg/go-bridge-web-react";
import { BridgeActions } from "@gbg/go-bridge-web";
import type { BridgeHostDelegate, BridgeResponder } from "@gbg/go-bridge-web";
import { useEffect, useMemo, useRef, useState } from "react";

interface JourneyConfig {
  url: string;
  requiredCapabilities: string[];
  hostVersion: string;
}

const JOURNEYS: Record<string, JourneyConfig> = {
  documentOnly: {
    url: "https://journey.example.com/document",
    requiredCapabilities: ["camera.document"],
    hostVersion: "1.0.0",
  },
  passportPlus: {
    url: "https://journey.example.com/passport",
    requiredCapabilities: ["camera.document", "device.info"],
    hostVersion: "1.0.0",
  },
};

interface DeviceCapabilities {
  cameraAvailable: boolean;
  cameraPermissionState: string;
}

async function detectCapabilities(): Promise<DeviceCapabilities> {
  const cameraAvailable = !!navigator.mediaDevices?.getUserMedia;
  let cameraPermissionState = "prompt";
  if (navigator.permissions) {
    try {
      const status = await navigator.permissions.query({ name: "camera" as PermissionName });
      cameraPermissionState = status.state;
    } catch {
      // Permission API doesn't support 'camera' on older browsers — leave as prompt
    }
  }
  return { cameraAvailable, cameraPermissionState };
}

export default function App() {
  const [activeJourney, setActiveJourney] = useState<keyof typeof JOURNEYS | null>(null);
  const [capabilities, setCapabilities] = useState<DeviceCapabilities | null>(null);

  useEffect(() => {
    detectCapabilities().then(setCapabilities);
  }, []);

  if (!capabilities) return <div>Detecting capabilities…</div>;

  if (activeJourney) {
    return (
      <JourneyContainer
        config={JOURNEYS[activeJourney]}
        capabilities={capabilities}
        onExit={() => setActiveJourney(null)}
      />
    );
  }

  return (
    <Launcher
      capabilities={capabilities}
      onLaunch={(key) => setActiveJourney(key)}
    />
  );
}

function Launcher({
  capabilities,
  onLaunch,
}: {
  capabilities: DeviceCapabilities;
  onLaunch: (key: keyof typeof JOURNEYS) => void;
}) {
  const [warning, setWarning] = useState<{
    journey: keyof typeof JOURNEYS;
    missing: string[];
  } | null>(null);

  function isSupported(capability: string): boolean {
    if (capability === "camera.document") return capabilities.cameraAvailable;
    return true; // device.info is always available
  }

  function attemptLaunch(key: keyof typeof JOURNEYS) {
    const missing = JOURNEYS[key].requiredCapabilities.filter((c) => !isSupported(c));
    if (missing.length === 0) {
      onLaunch(key);
    } else {
      setWarning({ journey: key, missing });
    }
  }

  return (
    <div style={{ padding: 24, maxWidth: 600, margin: "0 auto" }}>
      <h1>Identity Verification</h1>

      <JourneyOption
        title="Document Verification"
        subtitle="Document capture only"
        requirements={JOURNEYS.documentOnly.requiredCapabilities}
        capabilities={capabilities}
        onClick={() => attemptLaunch("documentOnly")}
      />

      <JourneyOption
        title="Passport Plus"
        subtitle="Document + device info"
        requirements={JOURNEYS.passportPlus.requiredCapabilities}
        capabilities={capabilities}
        onClick={() => attemptLaunch("passportPlus")}
      />

      <DeviceCapabilitySummary capabilities={capabilities} />

      {warning && (
        <div role="dialog" style={dialogStyle}>
          <h3>Capability Warning</h3>
          <p>
            This journey requires <strong>{warning.missing.join(", ")}</strong>{" "}
            which {warning.missing.length === 1 ? "is" : "are"} not available on
            this device. The journey may not complete successfully.
          </p>
          <button onClick={() => { onLaunch(warning.journey); setWarning(null); }}>
            Continue Anyway
          </button>
          <button onClick={() => setWarning(null)}>Cancel</button>
        </div>
      )}
    </div>
  );
}

type JourneyState =
  | { kind: "loading" }
  | { kind: "active" }
  | { kind: "completed"; data: Record<string, unknown> }
  | { kind: "failed"; reason: string }
  | { kind: "error"; message: string };

function JourneyContainer({
  config,
  capabilities,
  onExit,
}: {
  config: JourneyConfig;
  capabilities: DeviceCapabilities;
  onExit: () => void;
}) {
  const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);
  const [state, setState] = useState<JourneyState>({ kind: "loading" });

  const delegate = useMemo<BridgeHostDelegate>(
    () => ({
      onError(_host, error) {
        console.error("[Journey bridge error]", error);
        setState({ kind: "error", message: error.message });
      },
    }),
    [],
  );

  return (
    <div style={{ position: "relative", height: "100vh" }}>
      <BridgeHostProvider
        iframe={iframe}
        allowedOrigins={[new URL(config.url).origin]}
        hostVersion={config.hostVersion}
        delegate={delegate}
        capabilities={{
          "camera.document": {
            supported: capabilities.cameraAvailable,
            version: "1.0",
            permissionState: capabilities.cameraPermissionState,
          },
        }}
      >
        <DocumentCaptureHandler />
        <DeviceInfoCapability />
        <LifecycleForwarder />
        <JourneyStateRouter onState={setState} />

        <iframe
          ref={setIframe}
          src={config.url}
          allow="camera"
          style={{
            width: "100%",
            height: "100%",
            border: 0,
            opacity: state.kind === "active" ? 1 : 0,
          }}
        />
      </BridgeHostProvider>

      {state.kind === "loading" && <Overlay>Loading verification…</Overlay>}
      {state.kind === "completed" && (
        <CompletionOverlay
          message="Verification complete"
          tone="success"
          onDismiss={onExit}
        />
      )}
      {state.kind === "failed" && (
        <CompletionOverlay
          message={`Verification failed: ${state.reason}`}
          tone="error"
          onDismiss={onExit}
        />
      )}
      {state.kind === "error" && (
        <CompletionOverlay
          message={`Bridge error: ${state.message}`}
          tone="error"
          onDismiss={onExit}
        />
      )}

      <button onClick={onExit} style={cancelStyle}>
        Cancel
      </button>
    </div>
  );
}

function JourneyStateRouter({
  onState,
}: {
  onState: (state: JourneyState) => void;
}) {
  useBridgeHostEvent(BridgeActions.JOURNEY_STARTED, () => {
    onState({ kind: "active" });
  });
  useBridgeHostEvent(BridgeActions.JOURNEY_COMPLETED, (data) => {
    onState({ kind: "completed", data });
  });
  useBridgeHostEvent(BridgeActions.JOURNEY_FAILED, (data) => {
    onState({ kind: "failed", reason: String(data.reason ?? "unknown") });
  });
  useBridgeHostEvent(BridgeActions.JOURNEY_ABANDONED, () => {
    onState({ kind: "failed", reason: "abandoned" });
  });
  return null;
}

function LifecycleForwarder() {
  const host = useBridgeHost();

  useEffect(() => {
    if (!host) return;
    function onVisibilityChange() {
      if (document.visibilityState === "hidden") {
        host?.sendEvent("host.background", {});
      } else {
        host?.sendEvent("host.foreground", {});
      }
    }
    document.addEventListener("visibilitychange", onVisibilityChange);
    return () => document.removeEventListener("visibilitychange", onVisibilityChange);
  }, [host]);

  return null;
}

function DocumentCaptureHandler() {
  const responderRef = useRef<BridgeResponder | null>(null);
  const [active, setActive] = useState(false);

  useBridgeCapability("camera.document.capture", {
    handle: async (_request, responder) => {
      if (responderRef.current) {
        responder.error({
          code: "BUSY",
          message: "A capture is already active",
          recoverable: true,
        });
        return;
      }
      responderRef.current = responder;
      setActive(true);
    },
  });

  useEffect(() => {
    return () => {
      responderRef.current?.cancelled("Component unmounted");
      responderRef.current = null;
    };
  }, []);

  function complete(imageBase64: string) {
    responderRef.current?.success({
      imageBase64,
      mimeType: "image/jpeg",
    });
    responderRef.current = null;
    setActive(false);
  }

  function cancel() {
    responderRef.current?.cancelled("User dismissed");
    responderRef.current = null;
    setActive(false);
  }

  if (!active) return null;

  return (
    <div style={captureOverlayStyle}>
      <h2 style={{ color: "white" }}>Capture document</h2>
      <input
        type="file"
        accept="image/*"
        capture="environment"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (!file) return;
          const reader = new FileReader();
          reader.onload = () => {
            const dataUrl = reader.result as string;
            complete(dataUrl.split(",")[1]);
          };
          reader.readAsDataURL(file);
        }}
      />
      <button onClick={cancel} style={{ marginTop: 12 }}>
        Cancel
      </button>
    </div>
  );
}

function DeviceInfoCapability() {
  const host = useBridgeHost();
  useEffect(() => {
    if (!host) return;
    host.registerCustomCapability("device.info", "1.0", {
      handle: (_request, responder) => {
        responder.success({
          userAgent: navigator.userAgent,
          platform: navigator.platform,
          language: navigator.language,
          viewport: { width: window.innerWidth, height: window.innerHeight },
        });
      },
    });
  }, [host]);
  return null;
}

function JourneyOption({ title, subtitle, requirements, capabilities, onClick }: any) {
  const allMet = requirements.every((c: string) =>
    c === "camera.document" ? capabilities.cameraAvailable : true,
  );
  return (
    <button onClick={onClick} style={journeyOptionStyle}>
      <div style={{ textAlign: "left" }}>
        <div style={{ fontWeight: 600 }}>{title}</div>
        <div style={{ fontSize: 12, color: "#666" }}>{subtitle}</div>
      </div>
      <span style={{ color: allMet ? "green" : "orange" }}>
        {allMet ? "✓" : "⚠"}
      </span>
    </button>
  );
}

function DeviceCapabilitySummary({ capabilities }: { capabilities: DeviceCapabilities }) {
  return (
    <div style={{ marginTop: 24, fontSize: 14, color: "#666" }}>
      <div>
        Camera: {capabilities.cameraAvailable ? "✓ available" : "✗ unavailable"} ·{" "}
        permission: {capabilities.cameraPermissionState}
      </div>
    </div>
  );
}

function Overlay({ children }: { children: React.ReactNode }) {
  return <div style={overlayStyle}>{children}</div>;
}

function CompletionOverlay({
  message,
  tone,
  onDismiss,
}: {
  message: string;
  tone: "success" | "error";
  onDismiss: () => void;
}) {
  return (
    <div style={overlayStyle}>
      <h2 style={{ color: tone === "success" ? "#4CAF50" : "#F44336" }}>{message}</h2>
      <button onClick={onDismiss}>Done</button>
    </div>
  );
}

const dialogStyle: React.CSSProperties = { padding: 16, border: "1px solid #ddd", borderRadius: 8, marginTop: 16 };
const journeyOptionStyle: React.CSSProperties = { display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%", padding: 16, marginBottom: 12, background: "#f8f8f8", border: 0, borderRadius: 8, cursor: "pointer" };
const overlayStyle: React.CSSProperties = { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", background: "rgba(255,255,255,0.95)" };
const captureOverlayStyle: React.CSSProperties = { position: "fixed", inset: 0, background: "rgba(0,0,0,0.95)", zIndex: 1000, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" };
const cancelStyle: React.CSSProperties = { position: "fixed", top: 12, left: 12, padding: "8px 16px", background: "rgba(0,0,0,0.6)", color: "white", border: 0, borderRadius: 6, cursor: "pointer", zIndex: 999 };
```

## Architecture overview

```mermaid theme={null}
graph TB
    subgraph Launch["Launch Phase"]
        DC[detectCapabilities] --> L[Launcher]
        L -->|user selects journey| CHECK{All capabilities<br/>available?}
        CHECK -->|Yes| LAUNCH[Launch journey]
        CHECK -->|No| WARN[Show warning]
        WARN -->|Continue| LAUNCH
        WARN -->|Cancel| L
    end

    subgraph Journey["Journey Phase"]
        LAUNCH --> JC[JourneyContainer]
        JC --> BHP[BridgeHostProvider]
        BHP --> DCH[DocumentCaptureHandler]
        BHP --> DIC[DeviceInfoCapability]
        BHP --> LF[LifecycleForwarder]
        BHP --> JSR[JourneyStateRouter]
        JSR -->|state changes| JC
    end

    subgraph Lifecycle["Lifecycle Events"]
        VC[visibilitychange] --> LF
        LF -->|host.background / .foreground| BHP
    end
```

### Key patterns demonstrated

Four patterns are worth calling out — they're the load-bearing pieces that turn the raw code above into a production-shaped integration.

#### Pre-launch capability validation

Before opening the journey iframe, the launcher checks whether all required capabilities are supported. If anything is missing, the user sees a warning with an explicit "Continue anyway" / "Cancel" choice.

```typescript theme={null}
const missing = config.requiredCapabilities.filter((c) => !isSupported(c));
if (missing.length === 0) {
  onLaunch(key);
} else {
  setWarning({ journey: key, missing });
}
```

This prevents users from starting journeys that will fail at a missing capability mid-flow.

#### Lifecycle event forwarding

The host forwards browser visibility changes as bridge events so the journey can pause / resume internal timers:

```typescript theme={null}
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    host?.sendEvent("host.background", {});
  } else {
    host?.sendEvent("host.foreground", {});
  }
});
```

Browsers also fire `pagehide` and `pageshow` events for back/forward cache transitions — wire those too if your journey state depends on it.

#### Journey state routing

Multiple `useBridgeHostEvent` subscriptions in a single component route inbound lifecycle events into a React state machine:

```typescript theme={null}
useBridgeHostEvent(BridgeActions.JOURNEY_COMPLETED, (data) => {
  onState({ kind: "completed", data });
});
useBridgeHostEvent(BridgeActions.JOURNEY_FAILED, (data) => {
  onState({ kind: "failed", reason: String(data.reason ?? "unknown") });
});
```

This pattern keeps the journey iframe hidden during loading, visible during the active phase, and replaced with a completion overlay once the journey ends.

#### Busy guard for capture handlers

The web SDK doesn't enforce single-flight on raw handlers (unlike iOS/Android typed slots). The example implements it explicitly using a `useRef` for the active responder:

```typescript theme={null}
if (responderRef.current) {
  responder.error({ code: "BUSY", message: "...", recoverable: true });
  return;
}
responderRef.current = responder;
```

Also cancel any in-flight responder on unmount — otherwise the journey can be left waiting if the user navigates away mid-capture.

### Two patterns side by side

* **Document capture** uses `useBridgeCapability` with a declared `camera.document` entry in the provider's `capabilities` prop — the standard two-step pattern.
* **Device info** uses `host.registerCustomCapability` via `useBridgeHost` — the one-call pattern. Both declaration and registration happen in a single call after the host attaches.

Choose based on whether the capability is known at provider construction time (two-step) or added dynamically (one-call).

### Graceful degradation

`detectCapabilities` runs once at mount. If the browser doesn't support `getUserMedia` or `navigator.permissions`, the launcher shows a warning before launching journeys that need camera capture. The journey itself sees `supported: false` on `camera.document` and can choose to skip the step or fall back to a JS-side implementation.

## Running this example

1. Install `@gbg/go-bridge-web` and `@gbg/go-bridge-web-react`.
2. Add the code above to your React project (e.g., as `src/App.tsx`).
3. Update the `JOURNEYS` config to point at your real journey URLs.
4. Add appropriate CSP `frame-src` and `iframe allow` for the journey origin.
5. Run your dev server (Vite, Next.js, etc.).
6. Open the page — pick a journey from the launcher and walk through the flow.

## Next steps

* [Capability Handling](/docs/go-v2/developer-integration/sdks/web/capability-handling) — Deep dive on declaring, registering, and dynamic capabilities.
* [Security](/docs/go-v2/developer-integration/sdks/web/security) — Origin allowlist, CSP, and production hardening.
* [API Reference](/docs/go-v2/developer-integration/sdks/web/api-reference) — Complete type reference.
* [Tutorial](/docs/go-v2/developer-integration/sdks/web/tutorial) — Build a full app with backend token exchange.
