Riftix Console Bridge

React Integration

ConsoleHostProvider and ConsoleIframe for React host apps.

pnpm add @riftix/console-bridge-host

The React adapter lives at @riftix/console-bridge-host/react.


ConsoleHostProvider

Wraps your app (or a subtree) and makes the ConsoleHost instance available via context.

import { ConsoleHostProvider } from "@riftix/console-bridge-host/react";

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <ConsoleHostProvider
      defaultContextProvider={() => ({ theme: "light", locale: "en-US" })}
      defaultAuthorizationProvider={(channel) => ({
        clientId:       channel.productId,
        organizationId: "org_123",
        token:          "your-jwt-here",
        expiresAt:      Date.now() + 3_600_000,
      })}
    >
      {children}
    </ConsoleHostProvider>
  );
}

Props

interface ConsoleHostProviderProps extends ConsoleHostOptions {
  children: ReactNode;

  /**
   * Provide a pre-constructed ConsoleHost instance.
   * If omitted, the provider creates and manages one internally.
   */
  host?: ConsoleHost;
}

Accepts all ConsoleHostOptions (defaultContextProvider, defaultAuthorizationProvider, defaultCapabilities) as props in addition to the optional host override.

StrictMode safe: the host instance is created inside useEffect so React's double-invocation in development mode does not leave a disposed instance in the tree. The provider renders null until the effect has run.


useConsoleHost

Access the ConsoleHost instance anywhere inside the provider.

import { useConsoleHost } from "@riftix/console-bridge-host/react";

function ToastHandler() {
  const host = useConsoleHost(); // throws if called outside ConsoleHostProvider
  // ...
}

Throws "useConsoleHost must be used inside <ConsoleHostProvider>" if no provider is found in the tree.


ConsoleIframe

A thin wrapper around <iframe> that registers and unregisters the channel automatically.

import { ConsoleIframe } from "@riftix/console-bridge-host/react";

export function BillingPage() {
  return (
    <ConsoleIframe
      src="https://billing.example.com"
      origin="https://billing.example.com"
      productId="billing"
      style={{ width: "100%", height: "100%", border: "none" }}
    />
  );
}

Props

interface ConsoleIframeProps
  extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, "src"> {
  src:     string;
  origin:  string;
  productId: string;

  contextProvider?:       RegisterIframeOptions["contextProvider"];
  authorizationProvider?: RegisterIframeOptions["authorizationProvider"];
  handlers?:              RegisterIframeOptions["handlers"];
  capabilities?:          readonly string[];

  /** Called with the IframeChannel after registration. */
  onChannel?: (channel: IframeChannel) => void;

  /**
   * Called whenever the iframe reports a URL change via navigation.locationChanged.
   * Use this to mirror the iframe path into the host URL bar.
   * `path` is the iframe-relative path (e.g. "/invoices/123").
   */
  onLocation?: (path: string, channel: IframeChannel) => void;
}

Re-registers the channel whenever origin, productId, contextProvider, authorizationProvider, handlers, or capabilities change. Accepts a forwarded ref for the underlying <iframe> element.

URL sync pattern

The canonical pattern for mirroring iframe navigation into the host URL bar — without reloading the iframe:

import { useCallback } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { ConsoleIframe } from "@riftix/console-bridge-host/react";
import type { IframeChannel } from "@riftix/console-bridge-host";

const CLIENT_ORIGIN = "https://billing.example.com";

// Catch-all route: /billing and /billing/*
export const Route = createFileRoute("/billing/$")({
  component: BillingPage,
});

function BillingPage() {
  const { _splat } = Route.useParams();
  const subPath = _splat ? `/${_splat}` : "/";

  const handleLocation = useCallback((path: string, _channel: IframeChannel) => {
    const hostPath = path === "/" ? "/billing" : `/billing${path}`;
    if (window.location.pathname !== hostPath) {
      // Use replaceState (not router.navigate) — no iframe reload
      window.history.replaceState(null, "", hostPath);
    }
  }, []);

  return (
    <ConsoleIframe
      src={`${CLIENT_ORIGIN}${subPath}`}
      origin={CLIENT_ORIGIN}
      productId="billing"
      onLocation={handleLocation}
      style={{ width: "100%", height: "100%", border: "none" }}
    />
  );
}

Deep links work automatically: when the user opens /billing/invoices/123, _splat is "invoices/123" and the iframe src becomes https://billing.example.com/invoices/123.


RPC handlers

Register global handlers with useConsoleHost() + host.handle():

import { useEffect } from "react";
import { useConsoleHost } from "@riftix/console-bridge-host/react";
import { toast } from "sonner";

export function RpcHandlers() {
  const host = useConsoleHost();

  useEffect(() => {
    const cleanup = [
      host.handle("toast.show", ({ message, variant }) => {
        toast[variant ?? "info"](message);
      }),
      host.handle("modal.confirm", ({ title, body, confirmLabel, cancelLabel, destructive }) => {
        // Return a Promise<boolean> resolved by your modal component
        return openConfirmDialog({ title, body, confirmLabel, cancelLabel, destructive });
      }),
    ];
    return () => cleanup.forEach((fn) => fn());
  }, [host]);

  return null;
}

Subscribe to breadcrumb changes from registered channels:

import { useState, useEffect } from "react";
import { useConsoleHost } from "@riftix/console-bridge-host/react";
import type { BridgeBreadcrumb } from "@riftix/console-bridge-types";

export function useBreadcrumbs(productId: string) {
  const host = useConsoleHost();
  const [crumbs, setCrumbs] = useState<ReadonlyArray<BridgeBreadcrumb>>([]);

  useEffect(() => {
    const unsubs: Array<() => void> = [];

    const unregister = host.onChannelRegistered((channel) => {
      if (channel.productId !== productId) return;
      const unsub = channel.onBreadcrumbsChanged(setCrumbs);
      unsubs.push(unsub);
    });

    return () => {
      unregister();
      unsubs.forEach((fn) => fn());
    };
  }, [host, productId]);

  return crumbs;
}

Auth rotation

Re-issue authorization tokens before they expire:

useEffect(() => {
  const id = setInterval(() => {
    host.setAuthorizationForProduct("billing", {
      clientId:       "billing",
      organizationId: "org_123",
      token:          refreshToken(),
      expiresAt:      Date.now() + 3_600_000,
    });
  }, 30_000);
  return () => clearInterval(id);
}, [host]);

On this page