Handling results

How to read the result of a call — immediately, from an event, or as a returned value.

The three ways a call returns

How you get a result depends on the call. There are three patterns.

  1. Fire-and-forget. UI and navigation calls (bdk.navigation.navigate, bdk.ui.showBanner) just do something. await confirms the call went out; there's nothing else to read.
  2. Result on an event. Interactive calls (media, location, pickers, biometrics, purchases) finish later. Subscribe with bdk.on(...) to get the data.
  3. Returned value. Server helpers (@bdk/native/server/*) run on your backend and await returns the real value.

Models 1 and 2 run in the browser and resolve to a NativeCommandResult. Model 3 returns a typed value.

Browser command options are passed straight to native — match the key names exactly (e.g. { id, type } for purchases).

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

const bdk = createBdkNative();

// 1. Dispatch-and-forget — result is just the dispatch ack, no event follows.
await bdk.navigation.navigate({ url: "/profile" });

// 2. Dispatch + result event — start the interaction here...
await bdk.media.capturePhoto();
// ...and read the outcome here.
bdk.on("photoCaptured", (photo) => console.log(photo.fileUrl));

// 3. Resolved promise with the real value — server only.
// import { verifyIosReceipt } from "@bdk/native/server/iap";
// const result = await verifyIosReceipt({ receipt });

Fire-and-forget calls

Use these when the app just does something visible — navigate, show a toast, vibrate. await resolves to a NativeCommandResult confirming the call went out. There's no event to listen for.

Outside the app these calls resolve with triggered: false instead of throwing — pending in a browser tab, skipped with no window — so your web build keeps working.

const result = await bdk.navigation.navigate({ url: "/checkout" });
// { command: "navigate", queued: true, triggered: true, skipped: false }
// Nothing else arrives — navigation has no result event.

Get a result from an event

Use this when the user has to act — take a photo, pick a date, approve Face ID, confirm a purchase. Subscribe to the event before you make the call; the data arrives on the listener, not on the await.

Event names don't always match the method:

  • bdk.media.capturePhoto() emits photoCaptured.
  • bdk.media.pickPhoto() emits photoSelected.
  • bdk.location.getCurrentPosition() emits location.
  • bdk.ui.pickDateTime() emits datePicked.
  • bdk.auth.authenticateBiometrics() emits biometricResult.
  • bdk.iap.purchaseIos() / purchaseAndroid() emit purchaseSuccess or purchaseFailed.

capturePhoto emits photoCaptured, not photoSelected (which belongs to pickPhoto). Listening on the wrong one is the top reason a result "never arrives".

bdk.on(event, listener) returns an unsubscribe function — call it to stop listening.

// Subscribe first — the result arrives later, on the event.
const off = bdk.on("photoCaptured", (photo) => {
  console.log(photo.fileUrl, photo.dataUri, photo.contentType);
  off(); // unsubscribe when you're done
});

// Dispatch. This resolves on dispatch, NOT when the photo is taken.
await bdk.media.capturePhoto();

Server calls that return a value

Use these on your backend for receipt verification, link creation, and push notifications. They're fully typed and await returns the real result — no event needed.

For example, verifyIosReceipt returns an IosReceiptValidationResult with isValid, resultData, errorData, and raw.

Never import @bdk/native/server/* into a browser bundle — those modules use credentials and Node APIs. The browser-safe entry points are the package root and @bdk/native/browser.

import { verifyIosReceipt } from "@bdk/native/server/iap";

// await gives the real result — no event, no NativeCommandResult.
const result = await verifyIosReceipt({ receipt });
if (result.isValid) {
  grantEntitlement(result.resultData);
}

The NativeCommandResult shape

Every browser call (Models 1 and 2) resolves to a NativeCommandResult. Use it to confirm the call went out, or to detect that you're running on the web.

interface NativeCommandResult {
  command: string;    // the native command name that was dispatched
  queued: boolean;    // was it added to the queue?
  triggered: boolean; // was it actually handed to the app?
  skipped: boolean;   // was it dropped without triggering?
  pending?: boolean;  // queued, waiting for the agent to become available
  reason?: string;    // why it was skipped / pending, when applicable
}

The common case is { triggered: true }. Outside the app triggered is falsepending in a browser tab, skipped with no window — so branch on !triggered to degrade gracefully.

A few commands are platform-specific (e.g. bdk.iap.purchaseIos is iOS only). On a device that can't run one, the call rejects with a BdkError — wrap it in try/catch, or check bdk.isNative() first.

const res = await bdk.ui.showBanner({ title: "Hi" });

if (res.triggered) {
  // dispatched to the app
} else {
  // outside the app — pending in a browser tab, skipped with no window.
  // Either way it didn't run natively, so degrade gracefully.
}

Why await isn't the result

Coming from fetch(), unlearn one habit: for browser calls, await does not give you the data — it confirms the call was dispatched.

  • Model 1: nothing more to wait for — the NativeCommandResult is the whole result.
  • Model 2: the real outcome arrives later through bdk.on(...). Reading result.fileUrl off the await never works.
  • Model 3: the only one where await returns the value.

The rule: await to confirm dispatch, subscribe to receive data.

// ❌ capturePhoto resolves a NativeCommandResult, not a photo.
const result = await bdk.media.capturePhoto();
console.log(result.fileUrl); // undefined — wrong model