Web fallback

Detect when your page isn't running inside the native app and fall back to a web experience.

Set up once, run anywhere

createBdkNative() returns a client that works inside the native app and in any plain browser. Outside the app, native commands resolve cleanly with triggered: false instead of throwing, so the same build is safe to load everywhere.

In a browser tab outside the app, commands resolve pending (reason: "waiting_for_agent"). With no window at all (SSR, Node, a Web Worker) they resolve skipped (reason: "not_native"). Either way triggered is false — branch on that.

import { createBdkNative } from "@bdk/native/browser";

// Safe to call on localhost, in a preview, or inside the native app.
// init() runs automatically; no agent is required to construct the client.
export const bdk = createBdkNative();

Check if you're in the app

bdk.isNative() returns a boolean synchronously. Use it inline to gate any native-only feature and fall back to a web equivalent.

isNative() can read false for a moment at startup before device info arrives. If you need certainty on the first tick, await ready() (next section) instead.

if (bdk.isNative()) {
  // Inside the app: use the native photo picker.
  await bdk.media.pickPhoto();
} else {
  // Plain browser: fall back to a normal file input.
  document.querySelector<HTMLInputElement>("#file")?.click();
}

Wait for the native handshake

bdk.ready(timeoutMs = 0) resolves to BdkDeviceInfo | null and never rejects. A null result means you're outside the native app, so use it to branch your UI.

  • ready() / ready(0) resolves immediately: the cached BdkDeviceInfo if present, otherwise null.
  • ready(timeoutMs) waits up to that many milliseconds for the device to report in, then resolves to null if it never does.
const info = await bdk.ready(3000); // wait up to 3s for the native handshake

if (info === null) {
  // No native shell — render the web experience.
  renderWebHome();
} else {
  renderNativeHome(info); // info.deviceOS, info.versionName, info.playerId, ...
}

Check whether a command ran

Every native command resolves a NativeCommandResult — inspect it to know whether the app received the command. triggered === true means it ran; anything else means there's no native shell, so branch on !result.triggered for a web fallback.

See Objects for the full shape. Outside the app a command resolves with triggered: false: in a browser tab it's pending with reason: "waiting_for_agent"; with no window at all it's skipped with reason: "not_native". A rejected command instead throws a BdkError with a code like BDK_NATIVE_UNAVAILABLE, BDK_UNSUPPORTED_VERSION, or BDK_UNSUPPORTED_PLATFORM.

Commands that return data (photos, location, pickers, biometrics, IAP) deliver it on an event — capturePhoto emits photoCaptured, pickPhoto emits photoSelected. Those events never fire in the browser, so always give such calls a non-native path and never block your UI on an event that can't arrive.

const result = await bdk.ui.showBanner({ title: "Saved" });

if (!result.triggered) {
  // No native shell: pending (browser tab) or skipped (no window).
  // result.reason tells you which ("waiting_for_agent" vs "not_native").
  console.warn(`Native toast didn't run: ${result.reason}`);
  showWebToast("Saved"); // your own DOM toast
}

Build a web fallback

Decide native vs. web once at startup with ready(), then route every native-only call through an isNative() guard with a browser equivalent behind it. Listeners are safe to attach everywhere — in the browser they simply stay dormant, and a listener that throws is surfaced as BDK_LISTENER_ERROR through onError rather than crashing the page.

Never import @bdk/native/server/* into a browser bundle — those modules are server-only. Use @bdk/native/browser (or @bdk/native) on the client.

import { createBdkNative } from "@bdk/native/browser";

const bdk = createBdkNative();

export async function bootstrap() {
  // One handshake at startup. null === running outside a native shell.
  const device = await bdk.ready(3000);
  return { isNative: device !== null, device };
}

export async function pickAvatar() {
  if (bdk.isNative()) {
    // Native path: the file arrives on the "photoSelected" event.
    const off = bdk.on("photoSelected", (media) => {
      uploadAvatar(media.fileUrl ?? media.dataUri);
      off();
    });
    await bdk.media.pickPhoto();
    return;
  }
  // Web path: a standard file input, no native dependency.
  openWebFilePicker();
}