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

# Two-Way Communication

> Bidirectional messaging between your host page and the journey iframe.

This example demonstrates bidirectional messaging: sending host-emitted events to the iframe, handling incoming requests with capability handlers, and observing all bridge traffic via a delegate.

## What this example demonstrates

* Sending events from host to iframe via `host.sendEvent`
* Registering capture handlers via `useBridgeCapability`
* Registering custom capabilities at runtime via `host.registerCustomCapability`
* Responding with success / error / cancelled / unsupported statuses
* Observing all bridge traffic via `BridgeHostDelegate`
* Logging the full message stream in a debug panel

## Complete source

The single-file React app below wires up event listeners, capability handlers, a runtime-registered custom capability, and a debug log panel showing every message crossing the bridge.

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

const JOURNEY_URL = "https://journey.example.com";

interface LogEntry {
  id: string;
  timestamp: number;
  direction: "in" | "out";
  type: string;
  action: string;
}

export default function App() {
  const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null);
  const [log, setLog] = useState<LogEntry[]>([]);

  function append(entry: Omit<LogEntry, "id" | "timestamp">) {
    setLog((prev) =>
      [
        ...prev,
        {
          ...entry,
          id: crypto.randomUUID(),
          timestamp: Date.now(),
        },
      ].slice(-100),
    );
  }

  const delegate: BridgeHostDelegate = {
    onMessage(_host, message) {
      append({
        direction: "in",
        type: message.type,
        action: (message.payload as { action: string }).action,
      });
    },
    onMessageSent(_host, message) {
      append({
        direction: "out",
        type: message.type,
        action: (message.payload as { action: string }).action,
      });
    },
    onUnhandledRequest(host, request) {
      const action = (request.payload as { action: string }).action;
      append({ direction: "in", type: "unhandled", action });
      host.respond({
        correlationId: request.correlationId,
        status: "unsupported",
        error: {
          code: "UNSUPPORTED",
          message: `Action '${action}' is not supported`,
          recoverable: false,
        },
      });
    },
    onError(_host, error) {
      console.error("[Bridge error]", error);
    },
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      <BridgeHostProvider
        iframe={iframe}
        allowedOrigins={[new URL(JOURNEY_URL).origin]}
        hostVersion="1.0.0"
        delegate={delegate}
        capabilities={{
          "camera.document": { supported: true, version: "1.0" },
        }}
      >
        <DocumentCaptureHandler />
        <DeviceInfoCapability />
        <PingButton />

        <iframe
          ref={setIframe}
          src={JOURNEY_URL}
          allow="camera"
          style={{ flex: 1, border: 0 }}
        />
      </BridgeHostProvider>

      <LogPanel entries={log} />
    </div>
  );
}

function DocumentCaptureHandler() {
  useBridgeCapability("camera.document.capture", {
    handle: async (_request, responder) => {
      // Simulate a capture delay
      await new Promise((resolve) => setTimeout(resolve, 1000));

      // Return a 1x1 transparent PNG as a mock
      const mockImage =
        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4//8/AwAI/AL+jllaPwAAAABJRU5ErkJggg==";

      responder.success({
        imageBase64: mockImage,
        imageWidth: 1920,
        imageHeight: 1080,
        mimeType: "image/png",
      });
    },
  });
  return null;
}

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,
        });
      },
    });
  }, [host]);

  return null;
}

function PingButton() {
  const host = useBridgeHost();
  if (!host) return null;

  return (
    <button
      onClick={() =>
        host.sendEvent("host.ping", {
          timestamp: Date.now(),
        })
      }
      style={{
        position: "fixed",
        top: 12,
        right: 12,
        padding: "8px 16px",
        background: "#4D4DFF",
        color: "white",
        border: 0,
        borderRadius: 6,
        cursor: "pointer",
      }}
    >
      Send Ping
    </button>
  );
}

function LogPanel({ entries }: { entries: LogEntry[] }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    ref.current?.scrollTo({ top: ref.current.scrollHeight });
  }, [entries.length]);

  return (
    <div
      ref={ref}
      style={{
        height: 200,
        background: "#111",
        color: "#eee",
        fontFamily: "monospace",
        fontSize: 12,
        overflow: "auto",
        borderTop: "1px solid #333",
      }}
    >
      <div style={{ padding: "6px 12px", background: "#222" }}>
        Bridge log — {entries.length} messages
      </div>
      {entries.map((e) => (
        <div key={e.id} style={{ padding: "2px 12px" }}>
          <span style={{ color: "#888" }}>
            {new Date(e.timestamp).toISOString().slice(11, 23)}
          </span>{" "}
          <span style={{ color: e.direction === "in" ? "#4FC3F7" : "#81C784" }}>
            {e.direction.toUpperCase().padEnd(3)}
          </span>{" "}
          <span style={{ color: "#FFB74D" }}>{e.type.padEnd(10)}</span>{" "}
          <span>{e.action}</span>
        </div>
      ))}
    </div>
  );
}
```

### Code explanation

The sections below cover what each part of the app does, in roughly the order they're exercised at runtime.

### Sending events (host → iframe)

```typescript theme={null}
host.sendEvent("host.ping", { timestamp: Date.now() });
```

`sendEvent` posts a message to the iframe with `type: "event"` and a fresh correlation ID in the format `web-event-{uuid}`. Events are fire-and-forget — the iframe-side SDK delivers them to journey listeners. The "Send Ping" button shows how to send an event in response to a user action.

Use `BridgeActions` constants for known event names — see the [bridge event catalogue](/docs/go-v2/developer-integration/sdks/web/messaging#bridge-event-catalogue-iframe-host) for the standard set.

### Handling capture requests (iframe → host)

```typescript theme={null}
useBridgeCapability("camera.document.capture", {
  handle: async (_request, responder) => {
    const image = await captureDocument();
    responder.success({ imageBase64: image, imageWidth: 1920, imageHeight: 1080, mimeType: "image/jpeg" });
  },
});
```

`useBridgeCapability` registers a handler for the `camera.document.capture` action. The hook wraps the inline `{ handle: ... }` object in a stable proxy, so renders don't cause re-registration. On unmount it cleans up only if its own proxy is still active.

Pair the registration with a declaration in the provider's `capabilities` prop so the journey's pre-flight `capability.query` reports the capability as supported.

### Custom capabilities at runtime

```typescript theme={null}
const host = useBridgeHost();
useEffect(() => {
  if (!host) return;
  host.registerCustomCapability("device.info", "1.0", {
    handle: (_request, responder) => {
      responder.success({ userAgent: navigator.userAgent, ... });
    },
  });
}, [host]);
```

`registerCustomCapability` is the one-call alternative to the two-step declare-then-register pattern. It both declares the capability (appears in `capability.query` responses) and registers the handler. Use this for capabilities added dynamically after mount.

The capability map is read live on every query, so capabilities registered this way show up immediately.

### Unhandled requests (delegate fallback)

```typescript theme={null}
onUnhandledRequest(host, request) {
  host.respond({
    correlationId: request.correlationId,
    status: "unsupported",
    error: { code: "UNSUPPORTED", message: "...", recoverable: false },
  });
}
```

When the journey sends a request for an action no handler is registered for, the SDK pushes it into `host.pendingRequests` and fires `delegate.onUnhandledRequest`. The example responds immediately with `unsupported` so the journey isn't left waiting.

Without this fallback, pending requests accumulate up to the buffer cap (50) and then start being dropped. Always have a strategy for unhandled actions — either respond, or surface them in your UI so the user (or you, during dev) can decide.

### Delegate-based observability

```typescript theme={null}
const delegate: BridgeHostDelegate = {
  onMessage(_host, message) { /* log inbound */ },
  onMessageSent(_host, message) { /* log outbound */ },
  onUnhandledRequest(host, request) { /* respond + log */ },
  onError(_host, error) { /* log + report */ },
};
```

The delegate exposes four hooks for full message-flow visibility. Set it via the provider's `delegate` prop (read from the latest render on each fire — no snapshot, unlike `capabilities`). The example pipes everything into a state-backed log panel so you can watch messages live during development.

### Why a ref-less delegate works

The provider reads the delegate from the latest render on every dispatch — it does NOT snapshot it. That means inline delegate objects work without re-mount: changing the delegate prop changes which handlers run, no rebuild needed. Just be aware that recreating the delegate object on every render still triggers a re-render in the provider's children (because the prop reference changed); wrap in `useMemo` if that matters for your app.

## Running this example

1. Install `@gbg/go-bridge-web` and `@gbg/go-bridge-web-react`.
2. Replace your `App.tsx` with the code above.
3. Update `JOURNEY_URL` to your journey endpoint.
4. Run your dev server (Vite, Next.js, etc.).
5. Open the page — the log panel at the bottom shows messages flowing in real time. Click "Send Ping" to fire an outbound event.

## Next steps

* [Advanced Integration](/docs/go-v2/developer-integration/sdks/web/examples/advanced-integration) — Pre-launch capability validation, lifecycle, error states.
* [Messaging Guide](/docs/go-v2/developer-integration/sdks/web/messaging) — Deep dive on message patterns and the full event catalogue.
* [Capability Handling](/docs/go-v2/developer-integration/sdks/web/capability-handling) — Declaring vs. registering, dynamic capabilities, permission state.
