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

# Capture Screens

> Build a host-page capture surface for document and selfie requests in the browser.

The journey delegates capture to the host via the `camera.document.capture` or `camera.selfie.capture` actions. Your host page presents the capture UI, produces the image data, and returns it through the bridge response.

The Web Bridge SDK does not ship a built-in capture component. Browsers vary too widely in camera-API surface for a single component to work everywhere.

This page shows how to build a development-grade capture surface using the browser's native APIs and how to wire it to the bridge.

<Warning>
  Development and testing only. The patterns below produce a plain image — they do not perform document detection, auto-cropping, liveness checks, or biometric encryption. Placeholder images will not pass server-side verification. Swap for a production capture SDK before shipping.
</Warning>

## When you need a capture surface

The journey requests host-side capture through the `camera.document.capture` and `camera.selfie.capture` actions. A development capture surface lets you:

* **Prove early integration** — Verify the bridge protocol, capability negotiation, and round-trip data flow before plugging in a production capture SDK.
* **Develop without hardware** — A file-input or placeholder fallback covers laptops without cameras, browsers in restricted contexts, and CI environments.
* **Prototype the host UX** — Build the host shell with a working capture flow while you focus on routing, branding, and integration concerns.

## Three rungs of capture affordance

A capture surface typically offers three fallback levels, in priority order:

| Rung                      | Mechanism                                                                                      | When it applies                                                                                                |
| ------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| 1. Live preview + shutter | `navigator.mediaDevices.getUserMedia` + `<video>` + `<canvas>` snapshot                        | Browser supports `getUserMedia`, page is in a secure context (HTTPS / `localhost`), camera permission granted. |
| 2. File picker            | `<input type="file" accept="image/*" capture="environment">` (or `capture="user"` for selfies) | The `capture` attribute hints "use camera" on mobile browsers; on desktop it falls back to the file chooser.   |
| 3. Placeholder image      | A canvas-rendered stand-in or a hard-coded base64 image                                        | Dev / CI fallback for cameraless environments. Never ship in production.                                       |

## Live capture with getUserMedia (rung 1)

A minimal React component that opens a live preview, lets the user snap a photo, and returns base64-encoded JPEG:

```tsx theme={null}
import { useEffect, useRef, useState } from "react";

interface CaptureSurfaceProps {
  facingMode: "environment" | "user";
  onCapture: (imageBase64: string, width: number, height: number) => void;
  onCancel: () => void;
}

export function CaptureSurface({ facingMode, onCapture, onCancel }: CaptureSurfaceProps) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [stream, setStream] = useState<MediaStream | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let active = true;
    navigator.mediaDevices
      .getUserMedia({ video: { facingMode } })
      .then((s) => {
        if (!active) {
          s.getTracks().forEach((t) => t.stop());
          return;
        }
        setStream(s);
        if (videoRef.current) {
          videoRef.current.srcObject = s;
        }
      })
      .catch((e) => setError(String(e)));
    return () => {
      active = false;
    };
  }, [facingMode]);

  useEffect(() => () => stream?.getTracks().forEach((t) => t.stop()), [stream]);

  function snap() {
    if (!videoRef.current || !canvasRef.current) return;
    const video = videoRef.current;
    const canvas = canvasRef.current;
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    canvas.getContext("2d")?.drawImage(video, 0, 0);
    const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
    const base64 = dataUrl.split(",")[1];
    onCapture(base64, canvas.width, canvas.height);
  }

  if (error) {
    return <div role="alert">Camera unavailable: {error}</div>;
  }

  return (
    <div className="capture-surface">
      <video ref={videoRef} autoPlay playsInline />
      <canvas ref={canvasRef} style={{ display: "none" }} />
      <div className="capture-controls">
        <button onClick={snap}>Capture</button>
        <button onClick={onCancel}>Cancel</button>
      </div>
    </div>
  );
}
```

For document capture, use `facingMode: "environment"` (back camera on mobile). For selfies, use `facingMode: "user"` (front camera).

## File picker fallback (rung 2)

When `getUserMedia` fails or isn't available, fall back to a file input. On mobile browsers, the `capture` attribute hints that the camera should open directly; on desktop, the OS file chooser appears.

```tsx theme={null}
function FilePickerFallback({
  capture,
  onPick,
}: {
  capture: "environment" | "user";
  onPick: (imageBase64: string, width: number, height: number) => void;
}) {
  function onChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result as string;
      const base64 = result.split(",")[1];
      const img = new Image();
      img.onload = () => onPick(base64, img.width, img.height);
      img.src = result;
    };
    reader.readAsDataURL(file);
  }

  return (
    <label>
      <input type="file" accept="image/*" capture={capture} onChange={onChange} />
      Pick or capture an image
    </label>
  );
}
```

User backs out of the picker without choosing? `event.target.files` is empty — treat as "no selection yet" and leave the capture surface open. Don't fire `cancelled` to the journey on a picker back-out, just on an explicit user "Cancel" tap on your surface.

## Wiring it to the bridge

Pair the capture surface with a `useBridgeCapability` hook that tracks the active responder and toggles UI visibility.

```tsx theme={null}
import { useBridgeCapability } from "@gbg/go-bridge-web-react";
import { useRef, useState } from "react";
import type { BridgeResponder } from "@gbg/go-bridge-web";

export function DocumentCapture() {
  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 document capture is already active",
          recoverable: true,
        });
        return;
      }
      responderRef.current = responder;
      setActive(true);
    },
  });

  function onCapture(imageBase64: string, width: number, height: number) {
    responderRef.current?.success({
      imageBase64,
      imageWidth: width,
      imageHeight: height,
      mimeType: "image/jpeg",
    });
    responderRef.current = null;
    setActive(false);
  }

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

  if (!active) return null;

  return (
    <CaptureSurface
      facingMode="environment"
      onCapture={onCapture}
      onCancel={onCancel}
    />
  );
}
```

The handler stores the responder, flips state to render the surface, and the surface invokes callbacks that respond to the journey. If the user navigates away with an active responder still held, fire `cancelled` in your cleanup to avoid leaving the journey waiting.

### Cleanup on unmount

If the component unmounts mid-capture, cancel the active responder so the journey isn't stuck:

```tsx theme={null}
useEffect(() => {
  return () => {
    responderRef.current?.cancelled("Capture surface closed");
    responderRef.current = null;
  };
}, []);
```

## Returning the captured image

However the image was produced — live capture, file picker, or placeholder — the bridge response shape is the same:

```typescript theme={null}
responder.success({
  imageBase64: base64String,  // base64-encoded image (no data URL prefix)
  imageWidth: width,
  imageHeight: height,
  mimeType: "image/jpeg",
});
```

| Key                          | Value                                                                            |
| ---------------------------- | -------------------------------------------------------------------------------- |
| `imageBase64`                | Base64-encoded image bytes. Do NOT include the `data:image/jpeg;base64,` prefix. |
| `imageWidth` / `imageHeight` | Pixel dimensions.                                                                |
| `mimeType`                   | `"image/jpeg"` for JPEG (recommended) or `"image/png"`.                          |

The journey-side SDK decodes this into a standard image representation. Keep image dimensions reasonable (typically 1280×720 or 1920×1080) — multi-megabyte base64 strings stress the bridge channel and the journey's upload step.

## Permissions

Browser camera permissions are gated by the page being in a **secure context** (HTTPS, or `localhost`) and the iframe's `allow="camera"` attribute. The host page also needs permission for itself — if your capture surface lives on the host page (not in the iframe), the host origin needs camera access.

Check and surface permission state up front via `navigator.permissions.query({ name: "camera" })`:

```typescript theme={null}
async function getCameraPermissionState(): Promise<string> {
  if (!navigator.permissions) return "prompt";
  try {
    const status = await navigator.permissions.query({ name: "camera" as PermissionName });
    return status.state; // "granted" | "denied" | "prompt"
  } catch {
    return "prompt";
  }
}
```

Surface the result via `CapabilityInfo.permissionState` in your `capabilities` map so the journey can prompt before triggering the capture. See [Capability Handling → Permission state](/docs/go-v2/developer-integration/sdks/web/capability-handling#permission-state).

## Swapping for a production capture SDK

The capture surface is deliberately swappable. The handler, the `BridgeResponder` contract, and the wire format all stay the same — only the UI that produces the image changes. When you adopt a production capture SDK (with document detection, auto-cropping, glare check, liveness, etc.), replace the `<CaptureSurface />` component with the SDK's component and produce the same response shape.

The `onCapture` / `onCancel` seam shown above is the integration point. Production SDKs typically expose `onResult` and `onError` callbacks that map cleanly to `responder.success(...)` and `responder.error(...)`.

## Next steps

* [Capability Handling](/docs/go-v2/developer-integration/sdks/web/capability-handling) — Declaring capabilities and the two-step declare-then-register pattern.
* [Messaging](/docs/go-v2/developer-integration/sdks/web/messaging) — Request/response semantics and the bridge event catalogue.
* [Embedding](/docs/go-v2/developer-integration/sdks/web/embedding) — Where capture surfaces fit in the React tree.
* [Examples — Advanced Integration](/docs/go-v2/developer-integration/sdks/web/examples/advanced-integration) — End-to-end pattern with capture, lifecycle, and error handling.
