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>
);
}