# BDK Native > Reference documentation for the @bdk/native SDK — bring native iOS/Android features to any web app. Covers only the SDK's supported, customer-facing surface (the namespaced browser helpers and server helpers); internal/native-only commands are intentionally omitted. --- # Get started URL: https://docs.native.thebdk.com/getting-started/introduction ::::ref-section{title="Create the client"} `@bdk/native` lets your web app use native iOS and Android features — camera, location, in-app purchases, and more — when it runs inside the BDK Native shell. Create the client once with `createBdkNative()` and reuse it everywhere. ::callout Always create the client with `createBdkNative()`, never `new BdkNativeClient()`. It's ready the moment it returns. :: #code :::code-group ```ts [Create the client] import { createBdkNative } from "@bdk/native/browser"; // One instance, shared across your app. export const bdk = createBdkNative(); const info = await bdk.ready(3000); console.log(info ? `Native: ${info.deviceOS}` : "Plain web page"); ``` ::: :::: ::::ref-section{title="Detect the native app"} Use `bdk.ready(timeoutMs)` to wait for device info and `bdk.isNative()` to branch your code. In a plain browser there's no app, so render a web-only fallback. - `bdk.ready(0)` (the default) resolves immediately — cached `BdkDeviceInfo` if it arrived, else `null`. - `bdk.ready(3000)` waits up to the timeout (ms) for the `deviceInfo` event. ::callout Option keys you pass to browser methods are the native command contract — pass exactly the keys each native command expects. :: #code :::code-group ```ts [Detect the shell] const info = await bdk.ready(3000); if (bdk.isNative()) { // Cached device info: playerId, pushToken, deviceOS, bdkRelease, ... console.log(bdk.getDeviceInfo()?.deviceOS); } else { // No native shell — render web fallbacks. } ``` ::: :::: ::::ref-section{title="Get a command's result"} Sending a command and receiving its result are two steps. Every browser method returns a `Promise` (see [Objects](/reference/objects)) that only confirms the command was dispatched. Device data arrives later on an event you subscribe to with `bdk.on(event, listener)`, which returns an unsubscribe function. Subscribe before you dispatch. There are three result shapes: 1. **Dispatch-and-forget** — commands like navigation. `await` confirms dispatch; no follow-up event. 2. **Dispatch + event** — anything that returns device data: media (`photoSelected`, `photoCaptured`, `videoSelected`, `videoCaptured`), `location`, pickers (`datePicked`, `optionPicked`), `biometricResult`, purchases (`purchaseSuccess`, `purchaseFailed`, `receiptReceived`, …). Read the payload from the matching event. 3. **Resolved value** — the server SDK. `await` returns the real outcome (see below). ::callout{type="warn"} Match the event to the command: `bdk.media.capturePhoto()` emits `photoCaptured`, `bdk.media.pickPhoto()` emits `photoSelected`. Both deliver a `MediaResult` (`{ fileUrl, dataUri, data, contentType }`). :: #code :::code-group ```ts [Dispatch + event] // 2) Dispatch a command, then receive its result via an event. const off = bdk.on("photoCaptured", (result) => { console.log("Captured:", result.fileUrl, result.contentType); off(); // unsubscribe when you are done }); const dispatch = await bdk.media.capturePhoto(); console.log(dispatch.triggered, dispatch.queued); // dispatch status, not the photo ``` ```ts [Dispatch-and-forget] // 1) Navigation just dispatches — there is no follow-up event. const result = await bdk.navigation.navigate({ url: "/dashboard" }); console.log(result.command, result.triggered); ``` ```ts [Listen for errors] // Listener failures surface as a BdkError (code BDK_LISTENER_ERROR) // via both onError config and the "error" event; they do not recurse. const off = bdk.on("error", (error) => { console.error(error.code, error.message); }); ``` ::: :::: ::::ref-section{title="Browser and server SDKs"} Use the browser SDK in your web app and the server SDK on your backend. Awaiting a server call resolves to the real, typed result. **Browser SDK** — `@bdk/native/browser` (re-exported from `@bdk/native`). The client shown above. A CDN global build is at `dist/cdn/bdk-native.global.js`. **Server SDK** — `@bdk/native/server`, plus focused entry points `@bdk/native/server/onesignal`, `/server/iap`, `/server/branch`, `/server/chottulink`, and `/server/firebase-dynamic-links`. For example, `sendPushNotification(input)` returns a `PushNotificationResult` with `notificationId`, `numberOfRecipients`, and `sentSuccessfully`. ::callout{type="warn"} Never import `@bdk/native/server/*` into a browser bundle — those modules are server-only and may carry secrets. Only the root and `/browser` entry points are browser-safe. :: #code :::code-group ```ts [Server SDK (Node)] // Server-only. Awaiting resolves to the real, typed outcome. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ title: "New message", message: "You have a new reply", playerIds: ["a-onesignal-player-id"] }); console.log(result.sentSuccessfully, result.numberOfRecipients); ``` ::: :::: ::::ref-section{title="Start an in-app purchase"} Dispatch a purchase with `bdk.iap.purchaseIos()`, identifying the product with `{ id, type }` where `type` is `"product"` or `"subscription"`. Read the outcome from the `purchaseSuccess` or `purchaseFailed` event. #code :::code-group ```ts [Start a purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { console.log("Purchased on", platform, data); }); // Native payload contract: { id, type: "product" | "subscription" } await bdk.iap.purchaseIos({ id: "com.example.pro", type: "subscription" }); ``` ::: :::: --- # Install and import URL: https://docs.native.thebdk.com/getting-started/installation ::::ref-section{title="Install the package"} Add `@bdk/native` with your package manager. Server code needs Node.js 18 or newer. #code :::code-group ```bash [npm] npm install @bdk/native ``` ```bash [pnpm] pnpm add @bdk/native ``` ```bash [yarn] yarn add @bdk/native ``` ::: :::: ::::ref-section{title="Import for app code"} Import from `@bdk/native` for a single, obvious import path in app code. Use `createBdkNative()` to create a ready-to-use client. ::callout The root and `/browser` entries resolve to the same module. Pick whichever import path reads best. :: #code :::code-group ```ts [Root import] import { createBdkNative } from "@bdk/native"; export const bdk = createBdkNative(); ``` ```ts [Equivalent /browser import] import { createBdkNative, BdkNativeClient } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Import in the WebView"} Use `@bdk/native/browser` for code that runs inside the WebView or a browser. It exports `createBdkNative`, `BdkNativeClient`, `NativeBridge`, the error helpers, and the browser types (`BdkNativeConfig`, `NavigateOptions`, `LoadingScreenOptions`, `OpenLinkOptions`, `UrlParam`, `PermissionName`). Pass a `BdkNativeConfig` to tune behavior — common fields are `removeLoading` (`"automatic"` or `"manual"`), `pageFit`, `autoRequestDeviceInfo`, and an `onError` callback that receives a `BdkError`. Browser method options are passed straight through to the native command — match the key names it expects. ::callout{type="warn"} Awaiting a browser namespace method resolves when the call is sent, not when the native action finishes. For real resolved results, use the server helpers below. :: #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ```ts [With config] const bdk = createBdkNative({ removeLoading: "automatic", autoRequestDeviceInfo: true, onError: (error) => console.error(error.code, error.message) }); ``` ```ts [Native options are a Record] // `url` / `baseUrl` here are the NATIVE command contract, // not SDK-typed fields — they are forwarded as-is to the agent. await bdk.navigation.navigate({ url: "https://example.com" }); ``` ::: :::: ::::ref-section{title="Import on the server"} Use the `@bdk/native/server` entries for Node code — push notifications, receipt validation, deep links. These helpers are fully typed and return real resolved results. Import the aggregate `@bdk/native/server`, or a single concern to keep your bundle lean: - `@bdk/native/server/onesignal` — push notifications (e.g. `sendPushNotification`) - `@bdk/native/server/iap` — receipt validation (e.g. `verifyIosReceipt`, `verifyAndroidReceipt`, `consumeAndroidPurchase`) - `@bdk/native/server/branch` — Branch deep links - `@bdk/native/server/chottulink` — ChottuLink deep links - `@bdk/native/server/firebase-dynamic-links` — Firebase Dynamic Links ::callout{type="warn"} Never import a `@bdk/native/server/*` module into browser code — it pulls in Node-only dependencies and breaks the build. :: #code :::code-group ```ts [Aggregate import] import { sendPushNotification } from "@bdk/native/server"; const result = await sendPushNotification({ oneSignalAppId: process.env.ONESIGNAL_APP_ID, oneSignalApiKey: process.env.ONESIGNAL_API_KEY, title: "Hello", message: "Your order shipped" }); ``` ```ts [Single subpath] import { verifyIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt: base64Receipt, sharedSecret: process.env.APPLE_SHARED_SECRET, useSandboxFallback: true }); if (validation.isValid) { // grant entitlement } ``` ::: :::: ::::ref-section{title="Use without a bundler (CDN)"} Drop in the prebuilt IIFE bundle to use the SDK without a bundler. It is published to unpkg and jsdelivr and attaches a `BdkNative` global to `window`, so `createBdkNative` is available as `BdkNative.createBdkNative`. Server helpers are not included. #code :::code-group ```html [unpkg] ``` ```html [jsdelivr] ``` ::: :::: ::::ref-section{title="Browser-safe vs server-only"} The root, `/browser`, and the CDN global build are browser-safe. Every `/server` entry and subpath is server-only. ::callout{type="warn"} The single rule that prevents most build failures: never import `@bdk/native/server/*` into anything that ships to the browser. :: #code :::code-group ```ts [Safe in the browser] import { createBdkNative } from "@bdk/native"; // root import { BdkNativeClient } from "@bdk/native/browser"; // browser ``` ```ts [Server-only — do NOT bundle for the browser] import { sendPushNotification } from "@bdk/native/server"; import { verifyAndroidReceipt } from "@bdk/native/server/iap"; ``` ::: :::: --- # Set up the client URL: https://docs.native.thebdk.com/getting-started/initializing-the-client ::::ref-section{title="Create the client"} Create the client once at app startup and reuse the same instance everywhere. Import from `@bdk/native/browser` or the package root — both work. #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ```ts [From the root] // Identical — the package root is browser-safe too import { createBdkNative } from "@bdk/native"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Configure the client"} Every option is optional with a safe default, so most apps pass nothing. Set `removeLoading: "manual"` to dismiss the native splash screen yourself, and pass `onError` to centralize error reporting (it receives a `BdkError`). #code :::code-group ```ts [With options] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative({ removeLoading: "automatic", // "automatic" | "manual" pageFit: "normal", // "normal" | "cover" commandGapMs: 400, // delay between dispatched commands queueRetryMs: 400, // retry interval while waiting for the app maxPendingQueueMs: 15000, // drop queued commands after this long installGlobals: true, // install window callbacks the shell calls autoRequestDeviceInfo: true, // fetch device info on init onError: (error) => { console.error(error.code, error.message); } }); ``` ::: :::: ::::ref-section{title="Wait for the app to be ready"} Wait for device info at startup before using native features. Pass a timeout to wait that many milliseconds; in a plain browser it resolves to `null` when the timeout elapses. #code :::code-group ```ts [Wait up to 3s] const info = await bdk.ready(3000); if (info) { console.log("Running natively on", info.deviceOS, "release", info.bdkRelease); } else { console.log("No native shell — running as a normal web page"); } ``` ::: :::: ::::ref-section{title="Detect native vs. browser"} Branch between native-only features and web fallbacks. `bdk.isNative()` returns `true` once device info has arrived; `bdk.getDeviceInfo()` returns the cached `BdkDeviceInfo` or `null`. Prefer `await bdk.ready(...)` first. #code :::code-group ```ts [Branch on native] if (bdk.isNative()) { const info = bdk.getDeviceInfo(); console.log(info?.playerId, info?.pushToken, info?.versionName); } else { // Render a web-only experience } ``` ::: :::: ::::ref-section{title="Clean up in tests"} Tear down the client between instances, such as in tests or when hot-reloading a single-page app. You rarely need this in production. #code :::code-group ```ts [Cleanup] bdk.dispose(); ``` ::: :::: --- # Handling results URL: https://docs.native.thebdk.com/getting-started/interaction-models ::::ref-section{title="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. ::callout{type="warn"} Browser command `options` are passed straight to native — match the key names exactly (e.g. `{ id, type }` for purchases). :: #code :::code-group ```ts [The three at a glance] 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 }); ``` ::: :::: ::::ref-section{title="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. ::callout 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. :: #code :::code-group ```ts [Navigation] const result = await bdk.navigation.navigate({ url: "/checkout" }); // { command: "navigate", queued: true, triggered: true, skipped: false } // Nothing else arrives — navigation has no result event. ``` ```ts [UI commands] await bdk.ui.showBanner({ title: "Saved" }); await bdk.ui.showBanner({ title: "Welcome", description: "Glad you're here" }); await bdk.device.vibrate(); // All dispatch-and-forget: resolve on dispatch, no follow-up event. ``` ::: :::: ::::ref-section{title="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`. ::callout{type="warn"} `capturePhoto` emits `photoCaptured`, not `photoSelected` (which belongs to `pickPhoto`). Listening on the wrong one is the top reason a result "never arrives". :: ::callout `bdk.on(event, listener)` returns an unsubscribe function — call it to stop listening. :: #code :::code-group ```ts [Capture a photo] // 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(); ``` ```ts [Get location] bdk.on("location", (coords) => { console.log("device location:", coords); }); await bdk.location.getCurrentPosition(); ``` ```ts [In-app purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { console.log(`purchased on ${platform}`, data); }); bdk.on("purchaseFailed", ({ platform, data }) => { console.warn(`purchase failed on ${platform}`, data); }); // Native payload key is { id, type } — type is 'product' | 'subscription'. // (Do NOT use product_id — that's a legacy property name.) await bdk.iap.purchaseIos({ id: "com.example.pro", type: "subscription" }); ``` ```ts [Biometrics] bdk.on("biometricResult", ({ platform, status, data }) => { if (status === "success") unlockApp(); }); await bdk.auth.authenticateBiometrics(); ``` ::: :::: ::::ref-section{title="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`. ::callout{type="warn"} 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`. :: #code :::code-group ```ts [Verify a receipt (server)] 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); } ``` ```ts [Send a push (server)] import { sendPushNotification } from "@bdk/native/server/onesignal"; const outcome = await sendPushNotification({ message: "Your order shipped", playerIds: [playerId] }); console.log(outcome); // the actual PushNotificationResult ``` ::: :::: ::::ref-section{title="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. ```ts 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 `false` — `pending` in a browser tab, `skipped` with no `window` — so branch on `!triggered` to degrade gracefully. ::callout 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. :: #code :::code-group ```ts [Branch on the result] 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. } ``` ::: :::: ::::ref-section{title="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.** #code :::code-group ```ts [Wrong — the await has no payload] // ❌ capturePhoto resolves a NativeCommandResult, not a photo. const result = await bdk.media.capturePhoto(); console.log(result.fileUrl); // undefined — wrong model ``` ```ts [Right — listen for the event] // ✅ Subscribe for the data, await only to dispatch. bdk.on("photoCaptured", (photo) => { console.log(photo.fileUrl); // the real result }); await bdk.media.capturePhoto(); ``` ```ts [Right — promise-wrap an interaction if you need one await] function takePhoto() { return new Promise((resolve) => { const off = bdk.on("photoCaptured", (photo) => { off(); resolve(photo); }); void bdk.media.capturePhoto(); }); } const photo = await takePhoto(); ``` ::: :::: --- # Web fallback URL: https://docs.native.thebdk.com/getting-started/non-native-browser ::::ref-section{title="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. ::callout 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. :: #code :::code-group ```ts [Same code, every environment] 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(); ``` ::: :::: ::::ref-section{title="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. ::callout{type="warn"} `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. :: #code :::code-group ```ts [Guarding a native feature] 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("#file")?.click(); } ``` ```ts [Reading cached device info] // Returns the cached BdkDeviceInfo, or null when no native shell is present. const info = bdk.getDeviceInfo(); const platform = info?.deviceOS ?? "web"; ``` ::: :::: ::::ref-section{title="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. #code :::code-group ```ts [Wait, then branch on null] 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, ... } ``` ```ts [Immediate check, no waiting] const info = await bdk.ready(); // ready(0): resolves now, null if not cached yet console.log(info ? `native: ${info.deviceOS}` : "web fallback"); ``` ::: :::: ::::ref-section{title="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](/reference/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`. ::callout{type="warn"} 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. :: #code :::code-group ```ts [Inspect the result] 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 } ``` ```ts [Pending vs skipped] const result = await bdk.location.getCurrentPosition(); if (result.pending) { // queued, still waiting for the agent (reason: "waiting_for_agent") } else if (result.triggered) { // dispatched to the app; the actual coordinates arrive on the "location" event } else if (result.skipped) { // no native shell — use the browser Geolocation API instead } ``` ::: :::: ::::ref-section{title="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. ::callout Never import `@bdk/native/server/*` into a browser bundle — those modules are server-only. Use `@bdk/native/browser` (or `@bdk/native`) on the client. :: #code :::code-group ```ts [A reusable capability helper] 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(); } ``` ```ts [Error-safe listener] const bdk = createBdkNative({ onError: (err) => { // BDK_LISTENER_ERROR, BDK_NATIVE_UNAVAILABLE, etc. — never throws upward. console.warn(`[bdk] ${err.code}: ${err.message}`); } }); // Safe in the browser: this simply never fires without a native shell. bdk.on("location", (loc) => updateMap(loc)); ``` ::: :::: --- # Device & lifecycle URL: https://docs.native.thebdk.com/browser/device-and-lifecycle ::::ref-section{title="Read device info"} Get device facts like `playerId`, `deviceOS`, `versionName`, or a permission status. `bdk.getDeviceInfo()` returns the latest `BdkDeviceInfo`, or `null` if it hasn't arrived yet (for example, in a plain browser or before the first `deviceInfo` event). `BdkDeviceInfo` fields (each `string` or `null` unless noted): | Property | Type | Description | | --- | --- | --- | | `playerId` | `string` | OneSignal player id for push. | | `pushToken` | `string` | Device push token. | | `deviceModel` | `string` | Device model name. | | `deviceOS` | `string` | Operating system. | | `deviceOSVersion` | `string` | OS version. | | `deviceLanguage` | `string` | Device language. | | `deviceWidth` | `string \| number` | Screen width. | | `deviceHeight` | `string \| number` | Screen height. | | `versionName` | `string` | Host app version name. | | `versionCode` | `string \| number` | Host app version code. | | `biometricsAvailable` | `boolean` | Whether biometric login is available. | | `smartLoginAvailable` | `boolean` | Whether smart login is available. | | `cameraPermissionStatus` | `string` | Camera permission state. | | `contactsPermissionStatus` | `string` | Contacts permission state. | | `audiorecordPermissionStatus` | `string` | Audio-record permission state. | | `externalstoragePermissionStatus` | `string` | External-storage permission state. | | `locationPermissionStatus` | `string` | Location permission state. | | `idfa` | `string` | iOS advertising id. iOS only. | ::callout Getting `null` at startup? Wait for the value with `await bdk.ready(timeoutMs)`, or subscribe to the `deviceInfo` event. :: #code :::code-group ```ts [Read a field] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const info = bdk.getDeviceInfo(); if (info) { console.log(info.playerId, info.deviceOS, info.versionName); } ``` ```ts [Wait for it] const info = await bdk.ready(3000); // BdkDeviceInfo | null ``` ::: :::: ::::ref-section{title="Detect the native app"} Check whether your page is running inside the native app. Branch on it to enable native-only UI or fall back to web behavior. `bdk.isNative()` returns `true` once device info is available, `false` otherwise. #code :::code-group ```ts [Branch on environment] if (bdk.isNative()) { await bdk.device.vibrate(); } else { // web fallback } ``` ::: :::: ::::ref-section{title="Refresh device info"} Request a fresh `BdkDeviceInfo`, typically after a state change like a permission prompt. You don't need this at startup, since `createBdkNative()` requests device info automatically unless you pass `autoRequestDeviceInfo: false`. `bdk.app.requestDeviceInfo()`. ::callout{type="warn"} The fresh value arrives on the **`deviceInfo`** event (and updates `getDeviceInfo()`), not in the returned promise. Subscribe before you call. :: #code :::code-group ```ts [Request + listen] bdk.on("deviceInfo", (info) => { console.log("fresh info", info.locationPermissionStatus); }); await bdk.app.requestDeviceInfo(); ``` ::: :::: ::::ref-section{title="Hide the loading splash"} Dismiss the loading splash once your page is ready. Use this in `"manual"` mode to control exactly when the UI appears; with the default `removeLoading: "automatic"` you don't need to call it. `bdk.app.removeLoading()`. #code :::code-group ```ts [Manual dismiss] const bdk = createBdkNative({ removeLoading: "manual" }); // ...once your UI is mounted: await bdk.app.removeLoading(); ``` ::: :::: ::::ref-section{title="Restyle the loading splash"} Change the appearance of the loading splash. `bdk.app.updateLoading(options)`. Recognized `LoadingScreenOptions` keys: `splash_layer_url`, `splash_layer_width`, `splash_layer_top`, `splash_layer_left`, and `splash_section_background`. #code :::code-group ```ts [Custom splash] await bdk.app.updateLoading({ splash_layer_url: "https://cdn.example.com/splash.png", splash_layer_width: "60%", splash_layer_top: "40%", splash_layer_left: "20%", splash_section_background: "#0B0B0B" }); ``` ::: :::: ::::ref-section{title="Go back"} Navigate back, the equivalent of the native back action. `bdk.app.goBack()`. #code :::code-group ```ts [Back] await bdk.app.goBack(); ``` ::: :::: ::::ref-section{title="Change the launch page"} Set which URL the app opens on next launch, then clear it when done. `bdk.app.setLaunchPage(options)` sets the alternate launch URL. `bdk.app.resetLaunchPage()` removes the override and restores the default. #code :::code-group ```ts [Set] await bdk.app.setLaunchPage({ url: "https://app.example.com/home" }); ``` ```ts [Reset] await bdk.app.resetLaunchPage(); ``` ::: :::: ::::ref-section{title="Vibrate the device"} Trigger device haptics. `bdk.device.vibrate(options?)`. #code :::code-group ```ts [Vibrate] await bdk.device.vibrate(); ``` ::: :::: ::::ref-section{title="Read the device contacts"} Fetch the device address book. Ask for the `contacts` permission first. `bdk.device.getContacts()`. ::callout{type="warn"} The device address book arrives on the **`contacts`** event — an array of contacts, each with a name, phone number(s), and email(s) — not in the returned promise. Subscribe before you call. :: #code :::code-group ```ts [Get contacts] bdk.on("contacts", (addressBook) => { console.log("contacts payload", addressBook); }); await bdk.permissions.ask("contacts"); await bdk.device.getContacts(); ``` ::: :::: ::::ref-section{title="Save and read cache values"} Persist small values in native storage and read them back later — handy for flags like onboarding state. `bdk.device.saveToCache(options)` and `bdk.device.getFromCache(options)`. ::callout{type="warn"} The cached value arrives on the **`deviceVariable`** event as a `DeviceVariableResult` with the variable's `name` and its stored `data` value, not in the returned promise. Subscribe before you read. :: #code :::code-group ```ts [Save] await bdk.device.saveToCache({ key: "onboarded", value: "true" }); ``` ```ts [Read] bdk.on("deviceVariable", ({ name, data }) => { if (name === "onboarded") { console.log("cached value", data); } }); await bdk.device.getFromCache({ key: "onboarded" }); ``` ::: :::: ::::ref-section{title="Lifecycle events reference"} Subscribe with `bdk.on(event, listener)`, which returns an unsubscribe function. The events: - **`deviceInfo`** → `BdkDeviceInfo`. The shell reported device info; also feeds `getDeviceInfo()` and `ready()`. - **`contacts`**. The device address book from `device.getContacts()` — an array of contacts, each with a name, phone number(s), and email(s). - **`deviceVariable`** → `DeviceVariableResult` with the variable's `name` and its stored `data` value. A cached value from `device.getFromCache(...)`. - **`backButtonPressed`** → `undefined`. The hardware back button was pressed. ::callout{type="warn"} If a listener throws, the error surfaces once as a `BdkError` with code `BDK_LISTENER_ERROR`, delivered to your `onError` config callback and the `error` event. :: #code :::code-group ```ts [Subscribe + unsubscribe] const offDeviceInfo = bdk.on("deviceInfo", (info) => console.log(info.deviceModel)); const offContacts = bdk.on("contacts", (book) => console.log(book)); const offVariable = bdk.on("deviceVariable", ({ name, data }) => console.log(name, data)); const offBack = bdk.on("backButtonPressed", () => console.log("back pressed")); // call the returned functions to stop listening offDeviceInfo(); offContacts(); offVariable(); offBack(); ``` ::: :::: --- # Open links URL: https://docs.native.thebdk.com/browser/navigation-and-links ::::ref-section{title="Open a link"} Open a URL in an in-app web view, an external app, or a native screen. Set `view` to choose the destination. URLs are normalized by default (missing `https://` is added, `urlParams` applied); pass `useRawLink: true` to skip that. | Property | Type | Description | | --- | --- | --- | | `link` | `string` · required | The URL to open. | | `view` | `string` | Where to open it: `website`, `another app`, or `another screen`. | | `transition` | `string` | The screen transition animation. | | `useRawLink` | `boolean` | Open the link exactly as given, skipping URL normalization. | | `urlParams` | `UrlParam[]` | Query parameters to append to the URL. Each is `{ key, value }`. | #code :::code-group ```ts [Basic] await bdk.navigation.openLink({ link: "https://example.com/pricing", view: "website" }); ``` ```ts [With options] await bdk.navigation.openLink({ link: "example.com/account", // protocol added automatically view: "website", transition: "slide", urlParams: [ { key: "ref", value: "app" }, { key: "plan", value: "pro" } ] }); ``` ```ts [Raw URL] await bdk.navigation.openLink({ link: "myapp://deep/link?id=42", view: "another app", useRawLink: true }); ``` ::: :::: ::::ref-section{title="Navigate within the app"} Move to another screen inside the app. | Property | Type | Description | | --- | --- | --- | | `url` | `string` · required | The in-app screen to move to. | | `baseUrl` | `string` | The app's base URL, used to resolve relative links. | #code :::code-group ```ts [Navigate] await bdk.navigation.navigate({ url: "https://example.com/account", baseUrl: "example.com" }); ``` ::: :::: ::::ref-section{title="Fall back to web navigation"} These calls resolve to a `NativeCommandResult` (see [Handling results](/getting-started/interaction-models) for its fields). Outside the app the result comes back `skipped` or `pending` — use that to fall back to ordinary web navigation. #code :::code-group ```ts [Web fallback] const result = await bdk.navigation.openLink({ link: "https://example.com/pricing", view: "website" }); if (result.skipped || result.pending) { window.open("https://example.com/pricing", "_blank"); } ``` ::: :::: --- # Camera & media URL: https://docs.native.thebdk.com/browser/media ::::ref-section{title="Take a photo"} Open the camera to take a photo. The image arrives on the `photoCaptured` event, so subscribe before you call, and ask for camera permission first. ::callout `capturePhoto` emits `photoCaptured`; `pickPhoto` emits `photoSelected`. They are not interchangeable. :: #code :::code-group ```ts [Basic] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); bdk.on("photoCaptured", (photo) => { console.log(photo.fileUrl, photo.contentType); // MediaResult }); await bdk.permissions.ask("camera"); await bdk.media.capturePhoto(); ``` ::: :::: ::::ref-section{title="Use the photo result"} The `photoCaptured` listener receives a `MediaResult`. Use `dataUri` to preview inline and `fileUrl` to upload. `bdk.on(...)` returns an unsubscribe function. | Property | Type | Description | | --- | --- | --- | | `fileUrl` | `string \| null` | Hosted URL of the file — use this to upload. | | `dataUri` | `string \| null` | Data URI — use this to preview inline. | | `contentType` | `string \| null` | The file's MIME type. | | `data` | — | The raw file data from the device. | #code :::code-group ```ts [Preview + upload] const off = bdk.on("photoCaptured", (photo) => { if (photo.dataUri) { (document.querySelector("#preview") as HTMLImageElement).src = photo.dataUri; } if (photo.fileUrl) { void uploadAvatar(photo.fileUrl); } }); await bdk.media.capturePhoto(); off(); // when done ``` ::: :::: ::::ref-section{title="Pick from the photo library"} Open the photo library instead of the camera. Results arrive on the `photoSelected` event, not `photoCaptured`. #code :::code-group ```ts [Pick] bdk.on("photoSelected", (photo) => console.log("picked", photo.fileUrl)); await bdk.media.pickPhoto(); ``` ::: :::: ::::ref-section{title="Take a screenshot"} Capture a screenshot of the current screen. The image arrives on the `screenshot` event. #code :::code-group ```ts [Screenshot] bdk.on("screenshot", (image) => console.log("captured", image)); await bdk.media.captureScreenshot(); ``` ::: :::: ::::ref-section{title="Record audio"} Open the native audio recorder. The recording arrives on the `audioRecorded` event as a `MediaResult` — use `fileUrl` to upload it. #code :::code-group ```ts [Record] bdk.on("audioRecorded", (audio) => { if (audio.fileUrl) void uploadClip(audio.fileUrl); // MediaResult }); await bdk.media.recordAudio(); ``` ::: :::: ::::ref-section{title="Scan a barcode"} Open the scanner for QR codes and barcodes. The decoded value arrives on the `barcodeScanned` event. #code :::code-group ```ts [Scan] bdk.on("barcodeScanned", (code) => console.log("scanned", code)); await bdk.media.scanBarcode(); ``` ::: :::: ::::ref-section{title="Fall back on the web"} Outside the app the call resolves `skipped` or `pending` and `photoCaptured` never fires. Detect this and show a standard `` instead. #code :::code-group ```ts [Detect non-native] const result = await bdk.media.capturePhoto(); if (result.skipped || result.pending) { document.querySelector("#file-input")?.removeAttribute("hidden"); } ``` ::: :::: --- # Play audio & video URL: https://docs.native.thebdk.com/browser/media-playback ::::ref-section{title="Play audio"} Play a remote audio track in the native player. The option keys are the native command contract, so the exact keys depend on your native build. #code :::code-group ```ts [Play] const bdk = createBdkNative(); await bdk.media.playAudio({ url: "https://example.com/tracks/song.mp3" }); ``` ::: :::: ::::ref-section{title="Pause and stop audio"} Pause the current track or stop playback entirely. #code :::code-group ```ts [Pause / stop] await bdk.media.pauseAudio(); await bdk.media.stopAudio(); ``` ::: :::: ::::ref-section{title="Play a video"} Play a remote video in the native player. #code :::code-group ```ts [Play video] await bdk.media.playVideo({ url: "https://example.com/clips/demo.mp4" }); ``` ::: :::: ::::ref-section{title="Play a video playlist"} Play a sequence of videos back to back. ::callout{type="warn"} `playVideoPlaylist` is iOS only; elsewhere it doesn't run. :: #code :::code-group ```ts [Playlist] await bdk.media.playVideoPlaylist({ videos: [ "https://example.com/clips/one.mp4", "https://example.com/clips/two.mp4" ] }); ``` ::: :::: --- # Geolocation URL: https://docs.native.thebdk.com/browser/location ::::ref-section{title="Get started"} Readings are delivered on the `location` event, so subscribe before you call. Request the `location` permission first. #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Subscribe first — the position is delivered to this event, not the promise. bdk.on("location", (info) => { console.log("device location", info); }); await bdk.permissions.ask("location"); ``` ::: :::: ::::ref-section{title="Get the current position"} Get a single GPS fix. The reading arrives on the `location` event. #code :::code-group ```ts [One-shot fix] bdk.on("location", (info) => { console.log("current position", info); }); await bdk.location.getCurrentPosition(); ``` ::: :::: ::::ref-section{title="Stream location while the app is open"} Start a continuous stream of foreground updates. Each update fires the `location` event. Call `stopForegroundTracking()` to end it. #code :::code-group ```ts [Start a stream] const off = bdk.on("location", (info) => { console.log("moving", info); }); await bdk.location.startForegroundTracking(); // later, to stop receiving updates in your app code: off(); ``` ::: :::: ::::ref-section{title="Stop a foreground stream"} Stop an active foreground stream. #code :::code-group ```ts [Stop tracking] await bdk.location.stopForegroundTracking(); ``` ::: :::: ::::ref-section{title="Keep tracking in the background"} Keep receiving location updates while the app is backgrounded. The result fires the `backgroundLocationEnabled` event with `{ enabled, alreadyRunning, reason }`. ::callout Android only; elsewhere it doesn't run. Check `backgroundLocationEnabled` (especially `reason`) to know whether it actually started. :: #code :::code-group ```ts [Enable on Android] bdk.on("backgroundLocationEnabled", ({ enabled, alreadyRunning, reason }) => { console.log({ enabled, alreadyRunning, reason }); }); // options are required and forwarded to the native command. await bdk.location.enableBackground({ interval: 60000 }); ``` ::: :::: ::::ref-section{title="Stop background tracking"} Stop background updates. The result fires the `backgroundLocationDisabled` event with `{ enabled }`. Android only. #code :::code-group ```ts [Disable on Android] bdk.on("backgroundLocationDisabled", ({ enabled }) => { console.log("background location enabled?", enabled); }); await bdk.location.disableBackground(); ``` ::: :::: ::::ref-section{title="Events"} All position data arrives on events. `bdk.on(...)` returns an unsubscribe function. - `location` — every position reading (one-shot or streamed). - `backgroundLocationEnabled` — `{ enabled, alreadyRunning, reason }`. - `backgroundLocationDisabled` — `{ enabled }`. #code :::code-group ```ts [Subscribe to everything] const offs = [ bdk.on("location", (info) => console.log("location", info)), bdk.on("backgroundLocationEnabled", (p) => console.log("bg on", p)), bdk.on("backgroundLocationDisabled", (p) => console.log("bg off", p)) ]; // Clean up all listeners at once. offs.forEach((off) => off()); ``` ::: :::: --- # Runtime permissions URL: https://docs.native.thebdk.com/browser/permissions ::::ref-section{title="Request a permission"} Show the native OS permission prompt for one capability. Call it right before you use the feature. `permission` is one of `"camera"`, `"contacts"`, `"record audio"`, `"write to storage"`, or `"location"`. The resolved promise tells you the request was sent, not whether the user said yes — read the decision from the next `deviceInfo` event. ::callout{type="warn"} There is no permission-result event. Read the grant/deny outcome from the next `deviceInfo` event. :: #code :::code-group ```ts [Ask once] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Fires the native prompt. The resolved value is a dispatch // receipt, NOT whether the user granted access. await bdk.permissions.ask("camera"); ``` ```ts [All permission names] import type { PermissionName } from "@bdk/native/browser"; const all: PermissionName[] = [ "camera", "contacts", "record audio", "write to storage", "location" ]; for (const permission of all) { await bdk.permissions.ask(permission); } ``` ::: :::: ::::ref-section{title="Gate a feature behind a permission"} Check the current status, prompt only if it isn't granted, then wait for the `deviceInfo` event to confirm before using the feature. Get the current snapshot from `bdk.ready()` or `bdk.getDeviceInfo()`; the `deviceInfo` event delivers the updated one after the user answers. Status field names don't match the `PermissionName` strings: - `"camera"` → `cameraPermissionStatus` - `"contacts"` → `contactsPermissionStatus` - `"record audio"` → `audiorecordPermissionStatus` - `"write to storage"` → `externalstoragePermissionStatus` - `"location"` → `locationPermissionStatus` #code :::code-group ```ts [Gate a camera feature] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); async function openCamera() { const info = await bdk.ready(); if (info?.cameraPermissionStatus === "granted") { bdk.media.capturePhoto(); return; } // Ask, then wait for the refreshed deviceInfo to confirm. const off = bdk.on("deviceInfo", (next) => { if (next.cameraPermissionStatus === "granted") { off(); bdk.media.capturePhoto(); } }); await bdk.permissions.ask("camera"); } ``` ```ts [Check a status field] const info = bdk.getDeviceInfo(); // Field name differs from the PermissionName string. if (info?.audiorecordPermissionStatus !== "granted") { await bdk.permissions.ask("record audio"); } ``` ::: :::: --- # Biometric login URL: https://docs.native.thebdk.com/browser/biometrics ::::ref-section{title="Prompt for biometric auth"} Gate a sensitive action behind Face ID, Touch ID, or fingerprint. Use it before revealing a recovery phrase, confirming a payment, or unlocking a screen. Pass `options` your native build expects (e.g. a `reason` string). ::callout Check `getDeviceInfo()?.biometricsAvailable` before prompting, and fall back to PIN/password when it's `false`. :: ::callout{type="warn"} Awaiting the call only confirms the prompt was requested. The actual success or failure arrives on the `biometricResult` event. :: #code :::code-group ```ts [Trigger the prompt] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Dispatches authenticateBiometrics_ios or _android based on the device OS. await bdk.auth.authenticateBiometrics(); ``` ```ts [Native-contract options] // Option keys are the native command contract (forwarded as-is), // not SDK-typed fields. Pass what your native build expects. await bdk.auth.authenticateBiometrics({ reason: "Confirm it's you to view your recovery phrase" }); ``` ```ts [Guard on availability] const info = bdk.getDeviceInfo(); if (info?.biometricsAvailable) { await bdk.auth.authenticateBiometrics(); } else { // Fall back to PIN / password. } ``` ::: :::: ::::ref-section{title="Read the result"} Subscribe to `biometricResult` to learn whether auth succeeded. The `BiometricResult` payload has `status` (`"success"` or `"failed"`), `data` (the raw native auth result), and `platform` (`"ios" | "android"`). `bdk.on(...)` returns an unsubscribe function — call it when you're done. ::callout{type="warn"} A failed or cancelled prompt comes through `biometricResult`, not the `error` event, and does not reject the promise. Branch on `status` inside your listener. :: #code :::code-group ```ts [Handle the result] const off = bdk.on("biometricResult", ({ data, status, platform }) => { if (status === "success") { console.log(`Authenticated on ${platform}`, data); // Unlock the protected action here. } else { console.warn("Biometric auth did not succeed:", status, data); } }); // Later, when you're done listening: off(); ``` ::: :::: ::::ref-section{title="Subscribe before you prompt"} Register the `biometricResult` listener before calling `authenticateBiometrics()`, or a fast result can be missed. #code :::code-group ```ts [subscribe-before-dispatch] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); function unlockWithBiometrics() { return new Promise((resolve) => { // 1. Subscribe FIRST so no result is missed. const off = bdk.on("biometricResult", ({ status }) => { off(); resolve(status === "success"); }); // 2. Then dispatch the prompt. void bdk.auth.authenticateBiometrics({ reason: "Unlock your account" }); }); } const ok = await unlockWithBiometrics(); ``` ::: :::: --- # Saved login URL: https://docs.native.thebdk.com/browser/smart-login ::::ref-section{title="Saved login"} Let returning users sign back in without re-typing their credentials. Methods live on `bdk.auth`, with one event for the response. - `auth.updateCredentials({ email, password })` — save credentials after login. - `auth.clearCredentials()` — wipe saved credentials, e.g. on logout. - `auth.loginViaCredentials()` — request saved credentials; they arrive on the `smartLoginCredentials` event. ::callout{type="warn"} `loginViaCredentials()` doesn't return the credentials — read them from the `smartLoginCredentials` event. :: ::callout On the web or runtimes below `1.8`, the save/clear commands are skipped — check `result.skipped` and `result.reason`. To check support up front, read `bdk.getDeviceInfo()?.smartLoginAvailable`. :: #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Save credentials after login"} Call this right after a user authenticates so they sign in instantly next time. ::callout{type="warn"} Only save after you've verified the login is real. :: #code :::code-group ```ts [Save after login] const result = await bdk.auth.updateCredentials({ email: "ada@example.com", password: "hunter2" }); if (result.skipped) { // Runtime is below 1.8 or not native — Smart Login storage unavailable. console.warn("Smart Login not available:", result.reason); } ``` ```ts [Guard on device support] const info = bdk.getDeviceInfo(); if (info?.smartLoginAvailable) { await bdk.auth.updateCredentials({ email, password }); } ``` ::: :::: ::::ref-section{title="Clear saved credentials"} Call this on logout to forget the saved login. Takes no arguments. #code :::code-group ```ts [On logout] async function logout() { await bdk.auth.clearCredentials(); // ...continue your app's sign-out flow } ``` ::: :::: ::::ref-section{title="Log in with saved credentials"} On a returning visit, request the saved credentials. They arrive on the `smartLoginCredentials` event as `{ email: string | null; password: string | null }`. If either field is `null`, nothing usable is saved — fall back to your login screen. ::callout{type="warn"} Register your `smartLoginCredentials` listener with `bdk.on(...)` **before** you call `loginViaCredentials()`, or you'll miss the response. :: #code :::code-group ```ts [Request + listen] const off = bdk.on("smartLoginCredentials", ({ email, password }) => { if (email && password) { // Auto-fill your form or call your sign-in endpoint. signIn(email, password); } else { // Nothing saved (or values failed validation) — show the login screen. showLoginForm(); } }); await bdk.auth.loginViaCredentials(); // Later, when the screen unmounts: off(); ``` ```ts [Full saved-credentials flow] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // 1. On a returning launch, ask for any saved credentials. bdk.on("smartLoginCredentials", async ({ email, password }) => { if (!email || !password) return showLoginForm(); await signIn(email, password); }); await bdk.auth.loginViaCredentials(); // 2. After a fresh manual login, save them for next time. async function onManualLoginSuccess(email: string, password: string) { await bdk.auth.updateCredentials({ email, password }); } // 3. On logout, forget them. async function onLogout() { await bdk.auth.clearCredentials(); } ``` ::: :::: --- # In-app purchases URL: https://docs.native.thebdk.com/browser/in-app-purchases ::::ref-section{title="Read results from events, not the call"} Awaiting a `bdk.iap.*` call only confirms the command was sent — the purchase result arrives later on an event. Subscribe before you call. Every purchase and consume takes `{ id, type }`: `id` is the App Store / Play product identifier, and `type` is `"product"` (one-time / consumable) or `"subscription"`. Use the `Ios` / `Android` method for the device you're on. ::callout{type="warn"} The payload key is `id`, not `product_id`. :: ::callout Results from the device are unverified. Treat `purchaseSuccess` as "the store reported a transaction", then verify the receipt server-side before granting entitlements. See [Verify in-app purchases](/server/iap-verification). :: :::: ::::ref-section{title="Buy a product on iOS"} Opens the App Store purchase sheet. The result lands on `purchaseSuccess`, or `purchaseFailed` on cancel or error — both carry `{ platform, data }`. #code :::code-group ```ts [iOS purchase] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); bdk.on("purchaseSuccess", ({ platform, data }) => { // data is the raw, UNVERIFIED store payload — send it to your server to verify. console.log("purchased on", platform, data); }); bdk.on("purchaseFailed", ({ data }) => { console.warn("purchase failed or cancelled", data); }); await bdk.iap.purchaseIos({ id: "pro_monthly", type: "subscription" }); ``` ::: :::: ::::ref-section{title="Buy a product on Android"} Opens the Google Play purchase sheet. Results surface on the same `purchaseSuccess` / `purchaseFailed` events; the `platform` field tells you which store they came from. #code :::code-group ```ts [Android purchase] bdk.on("purchaseSuccess", ({ platform, data }) => { if (platform === "android") void verifyOnServer(data); }); await bdk.iap.purchaseAndroid({ id: "coins_100", type: "product" }); ``` ::: :::: ::::ref-section{title="Buy from one shared codebase"} Pick the right method by platform when you ship to both stores. The call resolves with no effect off-device, so add a web fallback. #code :::code-group ```ts [Platform switch] async function buy(id: string, type: "product" | "subscription") { const os = bdk.getDeviceInfo()?.deviceOS?.toLowerCase(); if (os === "ios") return bdk.iap.purchaseIos({ id, type }); if (os === "android") return bdk.iap.purchaseAndroid({ id, type }); // Not running natively — show a web fallback / upsell. } await buy("pro_monthly", "subscription"); ``` ::: :::: ::::ref-section{title="Consume a product so it can be bought again"} Use for consumables (coins, lives, refills) — a consumable can't be repurchased until it's consumed. ::callout Consuming on the device only updates local state. The authoritative consume for Google Play happens server-side — see [Verify in-app purchases](/server/iap-verification). :: #code :::code-group ```ts [Android consume] await bdk.iap.consumeAndroidPurchase({ id: "coins_100", type: "product" }); ``` ::: :::: ::::ref-section{title="IAP events reference"} Which event answers which call. Each carries the `platform` (`"ios"` or `"android"`) and a `data` object with the store's purchase details — product id, transaction id, receipt data, and result/error codes. Treat `data` as the raw store payload and verify it server-side. `bdk.on(...)` returns an unsubscribe function. | Event | Fired by | | --- | --- | | `purchaseSuccess` | `purchaseIos` / `purchaseAndroid` succeeded | | `purchaseFailed` | purchase cancelled or errored | | `receiptReceived` | a store receipt / token was returned | #code :::code-group ```ts [Subscribe to all IAP events] const offs = [ bdk.on("purchaseSuccess", (e) => console.log("success", e.platform, e.data)), bdk.on("purchaseFailed", (e) => console.log("failed", e.data)), bdk.on("receiptReceived", (e) => console.log("receipt", e.data)) ]; // later offs.forEach((off) => off()); ``` ::: :::: ::::ref-section{title="Verify on the server"} The step that actually grants entitlements. Send the `data` from `purchaseSuccess` / `receiptReceived` to your backend and validate it. See [Verify in-app purchases](/server/iap-verification) for `verifyIosReceipt`, `verifyAndroidReceipt`, `readAndroidReceipt`, and server-side `consumeAndroidPurchase`. #code :::code-group ```ts [Server route — Node only, never bundled in the browser] import { verifyAndroidReceipt } from "@bdk/native/server/iap"; // In your API handler, with the purchaseToken forwarded from the client: const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Inspect validation.payload / validation.raw before granting entitlements. } ``` ::: :::: --- # Native UI URL: https://docs.native.thebdk.com/browser/ui-and-feedback ::::ref-section{title="Set up"} Use `bdk.ui` to drive native surfaces from your web code. Calls that return a value (menu taps, popup buttons, picks) deliver it on an event — subscribe with `bdk.on(...)` before you call. ::callout{type="warn"} Outside the app these calls don't run (`triggered: false`) and no native surface appears. Provide a web fallback for anything the user must respond to. :: #code :::code-group ```ts [Setup] import { createBdkNative } from "@bdk/native/browser"; export const bdk = createBdkNative(); ``` ::: :::: ::::ref-section{title="Show a banner"} Show a non-blocking in-app banner for status messages like "You're offline." #code :::code-group ```ts [Banner] await bdk.ui.showBanner({ title: "You're offline", description: "Changes will sync when you reconnect." }); ``` ::: :::: ::::ref-section{title="Show an alert"} Show a blocking system alert the user must acknowledge before continuing. #code :::code-group ```ts [Alert] await bdk.ui.showAlert({ title: "Upload failed", description: "Please try again." }); ``` ::: :::: ::::ref-section{title="Ask the user to confirm"} Show a popup with action buttons. The pressed button arrives on the `popupClosed` event — subscribe first. ::callout The `popupClosed` payload tells you which button dismissed the popup — the OK or Cancel button. :: #code :::code-group ```ts [Popup + popupClosed] const off = bdk.on("popupClosed", (button) => { console.log("popup dismissed via", button); }); await bdk.ui.showPopup({ title: "Delete this item?", description: "This cannot be undone.", positive_button: "Delete", negative_button: "Cancel" }); // later, when you no longer need it off(); ``` ::: :::: ::::ref-section{title="Show a menu of choices"} Show a list or action sheet. The tapped item arrives on the `menuClicked` event — subscribe first. #code :::code-group ```ts [Menu + menuClicked] bdk.on("menuClicked", (item) => { console.log("menu item tapped:", item); }); await bdk.ui.showMenu({ options: ["Share", "Edit", "Delete"] }); ``` ::: :::: ::::ref-section{title="Prompt for an app-store rating"} Show the OS rating prompt at a natural moment. #code :::code-group ```ts [Request rating] await bdk.ui.requestRating(); ``` ::: :::: ::::ref-section{title="Pick a date or option"} Open a native date/time picker or option list. Date/time picks arrive on `datePicked`; option picks arrive on `optionPicked`. Subscribe to the one your picker produces before you call. #code :::code-group ```ts [Date picker] bdk.on("datePicked", (value) => { console.log("date chosen:", value); }); await bdk.ui.pickDateTime({ mode: "date", min: "2026-01-01" }); ``` ```ts [Option picker] bdk.on("optionPicked", (value) => { console.log("option chosen:", value); }); await bdk.ui.pickDateTime({ mode: "options", options: ["Small", "Medium", "Large"] }); ``` ::: :::: ::::ref-section{title="Style the status bar"} Set the status-bar color and light/dark content. #code :::code-group ```ts [Status bar] await bdk.ui.updateStatusBar({ style: "dark" }); ``` ::: :::: ::::ref-section{title="Control screen orientation"} Set or lock the screen orientation. ::callout{type="warn"} `lockOrientation` is **Android-only** and is skipped on iOS. :: #code :::code-group ```ts [Orientation] await bdk.ui.setOrientation({ orientation: "landscape" }); // Android only — skipped on iOS await bdk.ui.lockOrientation({ orientation: "portrait" }); ``` ::: :::: ::::ref-section{title="Disable the iOS back-swipe"} Suppress the iOS left-edge back-swipe when a screen owns that gesture itself. ::callout{type="warn"} iOS only. Skipped on Android. :: #code :::code-group ```ts [Disable left swipe] await bdk.ui.disableLeftSwipe({ disable: true }); ``` ::: :::: ::::ref-section{title="Event reference"} Register handlers with `bdk.on(event, listener)`, which returns an unsubscribe function. - **`menuClicked`** — the menu item the user tapped (its title and data). - **`popupClosed`** — which button dismissed the popup. - **`datePicked`** — the date/time the user selected. - **`optionPicked`** — the option the user chose. ::callout A throwing listener surfaces once as a `BdkError` with code `BDK_LISTENER_ERROR`, via both the `onError` config callback and the `error` event. :: #code :::code-group ```ts [Subscribe to all UI events] const offs = [ bdk.on("menuClicked", (item) => console.log("menu", item)), bdk.on("popupClosed", (button) => console.log("popup", button)), bdk.on("datePicked", (value) => console.log("date", value)), bdk.on("optionPicked", (value) => console.log("option", value)) ]; // Surface listener errors centrally const bdkWithErrors = createBdkNative({ onError: (err) => console.error(err.code, err.message) }); // Clean up when the screen unmounts offs.forEach((off) => off()); ``` ::: :::: --- # Native sharing URL: https://docs.native.thebdk.com/browser/sharing ::::ref-section{title="Open the share sheet"} Hand content to the device's native share sheet so the user can send it to another app like Messages, Mail, or Instagram. There is no result event — the call resolves once the sheet is requested, not when the user completes or cancels the share. ::callout When not running in the app, the call doesn't run — `triggered` is `false` (`pending` in a browser tab, `skipped` with no `window`). :: #code :::code-group ```ts [Every share call] const bdk = createBdkNative(); // Each method dispatches and forgets — await confirms the sheet was requested. await bdk.share.text({ text: "Check out this app!" }); ``` ::: :::: ::::ref-section{title="Share text"} Share a string of text, with an optional `url` or `title`. Use it for "Share this link" and "Tell a friend" flows. #code :::code-group ```ts [Share text] const bdk = createBdkNative(); await bdk.share.text({ text: "Check out this app!", url: "https://example.com" }); ``` ```ts [Just text] await bdk.share.text({ text: "Hello from my app" }); ``` ::: :::: ::::ref-section{title="Share an image"} Share a hosted image by URL. Pass a bare string, not an object. ::callout A protocol-relative URL beginning with `//` is upgraded to `https://`. :: #code :::code-group ```ts [Share image] const bdk = createBdkNative(); await bdk.share.image("https://example.com/photos/sunset.jpg"); ``` ```ts [Protocol-relative URL] // "//cdn.example.com/img.png" becomes "https://cdn.example.com/img.png" await bdk.share.image("//cdn.example.com/img.png"); ``` ::: :::: ::::ref-section{title="Share a video"} Share a video by URL. Pass a bare string. #code :::code-group ```ts [Share video] const bdk = createBdkNative(); await bdk.share.video("https://example.com/clips/demo.mp4"); ``` ::: :::: ::::ref-section{title="Share a file"} Share any file by URL — a PDF, a document, a download. Pass a bare string. #code :::code-group ```ts [Share file] const bdk = createBdkNative(); await bdk.share.file("https://example.com/docs/invoice.pdf"); ``` ::: :::: ::::ref-section{title="Share to an Instagram Story"} Open content directly in the Instagram Stories composer for a one-tap "Share to your Story" button. #code :::code-group ```ts [Instagram Story] const bdk = createBdkNative(); await bdk.share.instagramStory({ background_image_url: "https://example.com/story-bg.jpg", sticker_image_url: "https://example.com/sticker.png" }); ``` ::: :::: ::::ref-section{title="Check the result"} Each method resolves a `NativeCommandResult`. Inspect `triggered` and `skipped` to confirm the share sheet opened — there is no event reporting whether the user finished the share. #code :::code-group ```ts [Inspect the dispatch receipt] const bdk = createBdkNative(); const result = await bdk.share.image("https://example.com/photo.jpg"); console.log(result.command); // "shareImage" console.log(result.triggered); // true if it reached the app console.log(result.skipped); // true if skipped (e.g. unsupported version) ``` ::: :::: --- # Listen for events URL: https://docs.native.thebdk.com/browser/events ::::ref-section{title="Subscribe to an event"} Register a listener for a native event. The payload is typed from the event name (see `BdkNativeEvents`). Subscribe right after `createBdkNative()` so you don't miss a result. `bdk.on` returns an unsubscribe function — call it on unmount. #code :::code-group ```ts [Subscribe + cleanup] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); const off = bdk.on("deviceInfo", (info) => console.log(info.deviceOS, info.bdkRelease, info.playerId) ); off(); // stop listening ``` ::: :::: ::::ref-section{title="Get the result of a call"} Subscribe before you call the method. Awaiting the call confirms it was dispatched, not that it succeeded — the result arrives on the event. Some events fire quickly (photos, barcode, location, pickers, popups, contacts, screenshots); others may resolve much later or never (purchases, receipts, biometrics, Smart Login). ::callout `iap.purchaseIos` / `purchaseAndroid` take `{ id, type }`, where `type` is `"product"` or `"subscription"`. Match these key names exactly — they are the native contract. :: #code :::code-group ```ts [Quick callback] bdk.on("barcodeScanned", (code) => console.log("scanned", code)); bdk.on("location", (loc) => console.log("location", loc)); await bdk.media.scanBarcode(); await bdk.location.getCurrentPosition(); ``` ```ts [Webhook style] bdk.on("purchaseSuccess", ({ platform, data }) => console.log("purchased on", platform, data)); bdk.on("purchaseFailed", ({ platform, data }) => console.warn("failed", platform, data)); bdk.on("receiptReceived", ({ platform, data }) => { // send data to your backend to verify }); // The await below only confirms dispatch — NOT that the purchase succeeded. await bdk.iap.purchaseAndroid({ id: "pro_monthly", type: "subscription" }); ``` ::: :::: ::::ref-section{title="All events and payloads"} Every event and the payload its listener receives. The field-level shape of each object is in [Objects](/reference/objects). | Event | Listener receives | |---|---| | `deviceInfo` | `BdkDeviceInfo` | | `photoSelected` / `photoCaptured` | `MediaResult` | | `audioRecorded` | `MediaResult` | | `screenshot` | the captured screenshot image (a data URI you can preview or upload) | | `barcodeScanned` | the scanned code: its `type` and decoded `content` | | `contacts` | the device address book: each contact's name, phone numbers, and emails | | `location` | the device location: `latitude`, `longitude`, `address` | | `backgroundLocationEnabled` | `{ enabled, alreadyRunning, reason }` | | `backgroundLocationDisabled` | `{ enabled }` | | `deviceVariable` | `{ name, data }` | | `menuClicked` | the menu item the user tapped | | `popupClosed` | which button dismissed the popup | | `datePicked` | the date/time the user selected | | `optionPicked` | the option the user chose | | `backButtonPressed` | `undefined` | | `biometricResult` | `{ data, status, platform }` | | `smartLoginCredentials` | `{ email, password }` | | `purchaseSuccess` / `purchaseFailed` | `{ platform, data }` | | `receiptReceived` | `{ platform, data }` | | `error` | `BdkError` | #code :::code-group ```ts [Typed payloads] // The payload type is inferred from the event name. bdk.on("deviceInfo", (info) => info.playerId); // BdkDeviceInfo bdk.on("photoCaptured", (photo) => photo.fileUrl); // MediaResult bdk.on("biometricResult", ({ status, platform }) => {}); // { data, status, platform } bdk.on("error", (err) => err.code); // BdkError ``` ::: :::: ::::ref-section{title="Handle errors"} A throwing listener doesn't break the others. Catch errors centrally via the `error` event or the `onError` config callback — both receive a `BdkError`. #code :::code-group ```ts [Centralized errors] bdk.on("error", (err) => console.error(err.code, err.message, err.details)); // or at init: const bdk = createBdkNative({ onError: (err) => reportToSentry(err) }); ``` ::: :::: --- # Server helpers URL: https://docs.native.thebdk.com/server/overview ::::ref-section{title="Call provider APIs from your server"} Server helpers call third-party providers — OneSignal, App Store / Google Play receipts, Branch, ChottuLink, and Firebase Dynamic Links — from your Node backend. Each helper returns a typed result you can `await` directly. ::callout{type="warn"} Never import `@bdk/native/server` or any `@bdk/native/server/*` subpath into a browser bundle — these modules read `process.env` and call provider APIs with secret keys. The browser-safe entry points are `@bdk/native` (root) and `@bdk/native/browser`. :: #code :::code-group ```ts [Server-only helper] // Node 18+ / server runtime ONLY — never in a browser bundle. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ title: "Hello", message: "Your order shipped", includeSegments: ["Subscribed Users"] }); // `result` is a real, typed PushNotificationResult — not an event. console.log(result.notificationId, result.numberOfRecipients, result.sentSuccessfully); ``` ::: :::: ::::ref-section{title="Import a provider"} Import the helper for the provider you need from its subpath. Requires Node 18+. ::callout `@bdk/native/server` is a barrel that re-exports `onesignal`, `iap`, `branch`, `chottulink`, and `firebase-dynamic-links`. Prefer a specific subpath when you only need one provider. :: #code :::code-group ```ts [Per-provider subpaths] import { sendPushNotification } from "@bdk/native/server/onesignal"; import { verifyIosReceipt, verifyAndroidReceipt } from "@bdk/native/server/iap"; import { createBranchLink } from "@bdk/native/server/branch"; import { createChottuLink } from "@bdk/native/server/chottulink"; import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; ``` ```ts [Barrel entry] // Re-exports every provider from one module. import { sendPushNotification, createChottuLink } from "@bdk/native/server"; ``` ::: :::: ::::ref-section{title="Configure credentials"} Pass credentials as function options, or set the matching environment variables and let each helper read them. Each row lists the option and the variables checked, in order. | Provider | Option | Environment variables (in lookup order) | | --- | --- | --- | | OneSignal | `oneSignalAppId` | `BDK_ONESIGNAL_APP_ID`, `ONESIGNAL_APP_ID` | | OneSignal | `oneSignalApiKey` | `BDK_ONESIGNAL_API_KEY`, `ONESIGNAL_API_KEY` | | IAP (Apple) | `sharedSecret` | `BDK_IOS_INAPP_SHARED_SECRET`, `IOS_INAPP_SHARED_SECRET`, `APP_STORE_SHARED_SECRET` | | IAP (Google) | `accessToken` | `BDK_GOOGLE_PLAY_ACCESS_TOKEN`, `GOOGLE_PLAY_ACCESS_TOKEN` | | IAP (Google) | `serviceAccount` | `BDK_GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`, `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`, `GOOGLE_APPLICATION_CREDENTIALS_JSON` | | Branch | `branchKey` | `BDK_BRANCH_KEY`, `BRANCH_KEY`, `BRANCH_LIVE_KEY` | | Branch | `branchLinkDomain` | `BDK_BRANCH_LINK_DOMAIN`, `BRANCH_LINK_DOMAIN` | | ChottuLink | `apiKey` | `BDK_CHOTTULINK_API_KEY`, `CHOTTULINK_API_KEY` | | ChottuLink | `domain` | `BDK_CHOTTULINK_DOMAIN`, `CHOTTULINK_DOMAIN`, `CHOTTULINK_LINK_DOMAIN` | | Firebase | `firebaseWebApiKey` | `BDK_FIREBASE_WEB_API_KEY`, `FIREBASE_WEB_API_KEY`, `FIREBASE_DYNAMIC_LINKS_WEB_API_KEY` | | Firebase | `domainUriPrefix` | `BDK_FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX`, `FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX` | | Firebase | `iosBundleId` | `BDK_IOS_BUNDLE_ID`, `IOS_BUNDLE_ID` | | Firebase | `iosAppStoreId` | `BDK_IOS_APPSTORE_ID`, `IOS_APPSTORE_ID`, `IOS_APP_STORE_ID` | | Firebase | `androidPackageName` | `BDK_ANDROID_BUNDLE_ID`, `ANDROID_BUNDLE_ID`, `ANDROID_PACKAGE_NAME` | ::callout An explicit option always wins over the environment. A missing required value throws a `ValidationError` — except a missing Google Play credential (`serviceAccount`/`accessToken`), which surfaces as a `ProviderError` (`BDK_PROVIDER_ERROR`). :: #code :::code-group ```ts [Resolve from the environment] // Reads BDK_CHOTTULINK_API_KEY (or CHOTTULINK_API_KEY) and // BDK_CHOTTULINK_DOMAIN (or CHOTTULINK_DOMAIN / CHOTTULINK_LINK_DOMAIN). import { createChottuLink } from "@bdk/native/server/chottulink"; const link = await createChottuLink({ destinationUrl: "https://example.com/welcome" }); ``` ```ts [Pass options explicitly] import { createChottuLink } from "@bdk/native/server/chottulink"; const link = await createChottuLink({ apiKey: process.env.MY_CHOTTU_KEY, domain: "links.example.com", destinationUrl: "https://example.com/welcome" }); ``` ::: :::: ::::ref-section{title="Use a custom fetch"} Pass a `fetch` option to use your own HTTP client — for proxies, retries, or mocking in tests. It defaults to the global `fetch` on Node 18+, so you usually skip it. The type is `FetchLike` (`typeof fetch`). ::callout{type="warn"} If no fetch implementation can be resolved, the call throws a `ProviderError`. Any standard-fetch-compatible function (e.g. `undici`, `node-fetch`, or a test spy) is accepted. :: #code :::code-group ```ts [Inject a custom fetch] import { createChottuLink } from "@bdk/native/server/chottulink"; // FetchLike is `typeof fetch` — any standard-fetch-compatible function works. const tracingFetch: typeof fetch = (input, init) => { console.log("provider request →", input); return globalThis.fetch(input, init); }; const link = await createChottuLink({ destinationUrl: "https://example.com/welcome", fetch: tracingFetch }); ``` ```ts [Mock fetch in a test] import { sendPushNotification } from "@bdk/native/server/onesignal"; const fakeFetch: typeof fetch = async () => new Response(JSON.stringify({ id: "fake-id", recipients: 1 }), { status: 200 }); const result = await sendPushNotification({ oneSignalAppId: "test-app", oneSignalApiKey: "test-key", message: "hi", fetch: fakeFetch }); ``` ::: :::: ::::ref-section{title="Handle errors"} Catch `ProviderError` when a provider returns a non-OK HTTP status, and `ValidationError` when a required option or env var is missing. Both extend `BdkError` and carry a typed `code`. ::callout{type="warn"} Inspect `error.code` rather than parsing message strings. For a `ProviderError`, `error.details?.status` and `error.details?.response` give you the upstream provider's status and body. :: #code :::code-group ```ts [Catch a provider failure] import { sendPushNotification } from "@bdk/native/server/onesignal"; import { ProviderError } from "@bdk/native"; try { const result = await sendPushNotification({ message: "hi" }); return result; } catch (error) { if (error instanceof ProviderError) { // error.code === "BDK_PROVIDER_ERROR" console.error("provider rejected", error.details?.status, error.details?.response); } throw error; } ``` ```ts [Discriminate by code] import { createBranchLink } from "@bdk/native/server/branch"; import { ProviderError, ValidationError } from "@bdk/native"; try { // Throws ValidationError if branchKey is absent from options and env. await createBranchLink({ deepLinkUrl: "https://example.com/welcome" }); } catch (error) { if (error instanceof ValidationError) { // BDK_VALIDATION_ERROR — a required option/env var was missing. } else if (error instanceof ProviderError) { // BDK_PROVIDER_ERROR — Branch returned a non-OK status. } } ``` ::: :::: --- # Verify purchases URL: https://docs.native.thebdk.com/server/iap-verification ::::ref-section{title="Verify a Google Play purchase"} Confirm a purchase token with Google Play. Authenticate with a service account or access token, passed in the call or via `BDK_GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` / `BDK_GOOGLE_PLAY_ACCESS_TOKEN`. Returns `{ isValid, payload, errorMessage, raw }`. #code :::code-group ```ts [Env service account] import { verifyAndroidReceipt } from "@bdk/native/server/iap"; const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Read state before granting — see the next section. } ``` ```ts [Explicit auth] await verifyAndroidReceipt({ serviceAccount: JSON.parse(process.env.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON!), packageName: "com.example.app", productId: "coins_100", purchaseToken, productType: "product" }); ``` ::: :::: ::::ref-section{title="Check entitlement before granting"} `isValid: true` only means the token was accepted — not that the user owns the product. Use `readAndroidReceipt(input)` to read purchase state, acknowledgement state, and expiry before granting anything. ::callout{type="warn"} Never grant an entitlement off `isValid` alone. An expired or unacknowledged purchase can still report `isValid: true`. :: #code :::code-group ```ts [Read meaningful fields] import { verifyAndroidReceipt, readAndroidReceipt } from "@bdk/native/server/iap"; const validation = await verifyAndroidReceipt({ packageName: "com.example.app", productId: "pro_monthly", purchaseToken, productType: "subscription" }); if (validation.isValid) { // Google's purchase response has no productId, so tag the entry with the // one you verified — readAndroidReceipt filters receiptData by productId. const product = readAndroidReceipt({ productId: "pro_monthly", receiptData: [{ productId: "pro_monthly", ...(validation.raw as Record) }] }); if (Number(product.expiresDateMs) > Date.now() && product.acknowledgementState !== null) { grantEntitlement(product.id); } } ``` ::: :::: ::::ref-section{title="Consume a one-time product"} Mark a consumable product as consumed so it can be purchased again. #code :::code-group ```ts [Consume] import { consumeAndroidPurchase } from "@bdk/native/server/iap"; await consumeAndroidPurchase({ packageName: "com.example.app", productId: "coins_100", purchaseToken }); ``` ::: :::: ::::ref-section{title="Verify an App Store receipt"} Validate an iOS receipt with Apple. Pass the base64 `receipt`, and your `sharedSecret` (or set `BDK_IOS_INAPP_SHARED_SECRET`). A production receipt that Apple flags as sandbox is retried against the sandbox endpoint automatically; set `useSandboxFallback: false` to opt out. Returns `{ isValid, resultData, errorData, raw }` — `resultData` holds Apple's full response on success, `errorData` the failure reason. #code :::code-group ```ts [Verify] import { verifyIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt, sharedSecret: process.env.APP_STORE_SHARED_SECRET }); if (validation.isValid) { // resultData is Apple's full verifyReceipt payload — read it next. } else { console.error(validation.errorData); } ``` ::: :::: ::::ref-section{title="Read an App Store entitlement"} `isValid: true` means Apple accepted the receipt, not that the entitlement is live. Pass the verification's `resultData` to `readIosReceipt(input)` to pull the latest purchase for a product — its purchase, original-purchase, and expiry timestamps — before granting. #code :::code-group ```ts [Read meaningful fields] import { verifyIosReceipt, readIosReceipt } from "@bdk/native/server/iap"; const validation = await verifyIosReceipt({ receipt, sharedSecret: process.env.APP_STORE_SHARED_SECRET }); if (validation.isValid) { const product = readIosReceipt({ productId: "pro_monthly", receiptData: validation.resultData as Record }); if (Number(product.expiresDateMs) > Date.now()) { grantEntitlement(product.id); } } ``` ::: :::: ::::ref-section{title="Verify inside an Express route"} These are plain async functions, so they drop into any handler — Express, a Next API route, a Firebase Function, or any Node service. #code :::code-group ```ts [Express] import express from "express"; import { verifyAndroidReceipt } from "@bdk/native/server/iap"; const app = express(); app.use(express.json()); app.post("/iap/android/verify", async (req, res) => { const validation = await verifyAndroidReceipt({ packageName: req.body.packageName, productId: req.body.productId, purchaseToken: req.body.purchaseToken, productType: req.body.productType }); res.json(validation); }); app.listen(3000); ``` ::: :::: --- # Push notifications URL: https://docs.native.thebdk.com/server/onesignal-push ::::ref-section{title="Send a push notification"} Send a push through OneSignal from your backend. `await` resolves with the delivery result — notification id, recipient count, and whether OneSignal accepted it. Target recipients with `playerIds`, `subscriptionIds`, or `includeSegments` / `excludeSegments`. Set content with `message`, `title`, `subtitle`, `imageUrl`, `data`, or a saved `templateId`. ::callout{type="warn"} Server-only. Never import `@bdk/native/server/onesignal` (or any `@bdk/native/server/*` entry) into a browser bundle — it carries your OneSignal REST API key. :: #code :::code-group ```ts [Basic] import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ oneSignalAppId: "00000000-0000-0000-0000-000000000000", oneSignalApiKey: "os_v2_app_...", message: "Your order has shipped!", title: "Order update", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); if (result.sentSuccessfully) { console.log("Delivered", result.notificationId, "to", result.numberOfRecipients); } else { console.error("OneSignal rejected the push:", result.errorMessage); } ``` ```ts [Target a segment] const result = await sendPushNotification({ message: "Flash sale ends tonight", title: "Last chance", includeSegments: ["Subscribed Users"], excludeSegments: ["Engaged Last 24h"] }); ``` ```ts [Rich content + deep link data] const result = await sendPushNotification({ message: "New reply on your post", title: "Inbox", subtitle: "From @jess", imageUrl: "https://cdn.example.com/preview.png", onLoadUrl: "https://app.example.com/posts/42", urlParams: [{ key: "ref", value: "push" }], data: { postId: 42, kind: "reply" }, subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); ``` ::: :::: ::::ref-section{title="Input fields"} The object you pass to `sendPushNotification`. Every field is optional, but each call needs credentials (inline or via env) and at least one targeting field. - `message`, `title`, `subtitle` — body, heading, and iOS subtitle. - `playerIds` / `subscriptionIds` — device targets. Prefer `subscriptionIds`; `playerIds` is the legacy key. - `includeSegments` / `excludeSegments` — target or exclude named OneSignal segments. - `templateId` — send a saved OneSignal template instead of inline content. - `imageUrl` — a media attachment. - `onLoadUrl` + `urlParams` — a launch URL. `urlParams` entries are appended as query parameters and passed through in `data`. - `data` — an arbitrary `Record` payload. - `iosBadgeType`, `iosBadgeCount` — iOS badge controls. - `oneSignalAppId`, `oneSignalApiKey` — inline credentials. - `targetChannel`, `endpoint`, `authorizationScheme`, `fetch` — advanced overrides. ::callout The `OneSignalUrlParam` type used by `urlParams` is `{ key: string; value: string | number | boolean }`. :: #code :::code-group ```ts [Type] import type { SendPushNotificationInput, OneSignalUrlParam } from "@bdk/native/server/onesignal"; const input: SendPushNotificationInput = { message: "Welcome aboard", title: "Hello", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"], data: { onboarding: true } }; ``` ```ts [Send a saved template] const result = await sendPushNotification({ templateId: "5a5a1018-1656-4e8e-8a52-8a52a5b5a5b5", includeSegments: ["Total Subscriptions"] }); ``` ::: :::: ::::ref-section{title="Read the result"} `sendPushNotification` resolves with a `PushNotificationResult`. Check it to confirm delivery and surface failures. - `notificationId: string | null` — the OneSignal notification id. - `numberOfRecipients: number | null` — how many recipients OneSignal reported. - `sentSuccessfully: boolean` — `true` only when OneSignal returned no `errors`. - `errorMessage: string | null` — the errors OneSignal returned, or `null` on success. - `raw: unknown` — the full parsed OneSignal response. ::callout{type="warn"} A `200 OK` can still carry errors (e.g. "All included players are not subscribed"), so always check `sentSuccessfully` — not just the absence of a thrown error. A non-2xx status throws a `BdkError` with code `BDK_PROVIDER_ERROR`. :: #code :::code-group ```ts [Handle the result] import { sendPushNotification } from "@bdk/native/server/onesignal"; import { BdkError } from "@bdk/native"; try { const result = await sendPushNotification({ message: "Ping", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); if (!result.sentSuccessfully) { // 2xx response but OneSignal reported errors in the body. console.warn("Not delivered:", result.errorMessage, result.raw); } } catch (err) { if (err instanceof BdkError && err.code === "BDK_PROVIDER_ERROR") { console.error("OneSignal HTTP error:", err.details); } else { throw err; } } ``` ::: :::: ::::ref-section{title="Set credentials"} Pass your OneSignal app id and REST API key inline as `oneSignalAppId` / `oneSignalApiKey`, or set them in the environment and omit them from the call. For the app id it reads `BDK_ONESIGNAL_APP_ID`, then `ONESIGNAL_APP_ID`; for the key, `BDK_ONESIGNAL_API_KEY`, then `ONESIGNAL_API_KEY`. Inline values win. Missing either throws a `BdkError` with code `BDK_VALIDATION_ERROR`. The `Authorization` scheme is picked from your key automatically — override it with `authorizationScheme` (`"Basic" | "Key" | "Bearer"`) if needed. ::callout Prefer env vars — keep secrets out of your code and rotate keys without redeploying. :: #code :::code-group ```ts [From env] // BDK_ONESIGNAL_APP_ID and BDK_ONESIGNAL_API_KEY (or ONESIGNAL_APP_ID / // ONESIGNAL_API_KEY) are read from the environment — no credentials in code. import { sendPushNotification } from "@bdk/native/server/onesignal"; const result = await sendPushNotification({ message: "Server-driven push", includeSegments: ["Subscribed Users"] }); ``` ```ts [Explicit credentials + scheme override] const result = await sendPushNotification({ oneSignalAppId: process.env.MY_APP_ID, oneSignalApiKey: process.env.MY_REST_KEY, authorizationScheme: "Bearer", message: "Custom auth scheme", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"] }); ``` ::: :::: ::::ref-section{title="Call from a backend"} Drop the call into any backend route, queue worker, or cron job. Node 18+ uses the global `fetch`; on older runtimes pass your own via `fetch`. Override `endpoint` to point at a proxy or test server (defaults to `https://onesignal.com/api/v1/notifications`). ::callout{type="warn"} If no `fetch` is available and you don't pass one, the request throws a `BdkError` with code `BDK_PROVIDER_ERROR`. :: #code :::code-group ```ts [Express route] import express from "express"; import { sendPushNotification } from "@bdk/native/server/onesignal"; const app = express(); app.use(express.json()); app.post("/notify", async (req, res) => { const result = await sendPushNotification({ message: req.body.message, title: req.body.title, subscriptionIds: req.body.subscriptionIds // credentials read from env }); res.status(result.sentSuccessfully ? 200 : 502).json(result); }); ``` ```ts [Custom fetch / endpoint] import { sendPushNotification } from "@bdk/native/server/onesignal"; import fetch from "node-fetch"; const result = await sendPushNotification({ message: "Via a custom client", subscriptionIds: ["a1b2c3d4-1111-2222-3333-444455556666"], fetch: fetch as unknown as typeof globalThis.fetch, endpoint: "https://onesignal.com/api/v1/notifications" }); ``` ::: :::: --- # Branch deep links URL: https://docs.native.thebdk.com/server/branch ::::ref-section{title="Create a deep link"} Generate a Branch deep link from your backend — a share link, invite URL, or anything you return from an API route. Server-only: import from `@bdk/native/server/branch` and never include it in a browser bundle. Only `deepLinkUrl` is required. If it has no protocol, `https://` is added; any `urlParams` are appended as query parameters. ::callout{type="warn"} On success, `dynamicLink` holds the link and `errorMessage` is `null`. If Branch returns no URL, the call resolves with `dynamicLink: null` and an `errorMessage` — check it. Transport or non-`2xx` errors throw a `BdkError` with code `BDK_PROVIDER_ERROR`, so wrap the call in `try/catch`. :: #code :::code-group ```ts [Basic] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://product/42" }); if (result.dynamicLink) { console.log("Share this link:", result.dynamicLink); } else { console.error(result.errorMessage); } ``` ```ts [With analytics and social] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://product/42", alias: "summer-sale", iosUrl: "https://apps.apple.com/app/id000000000", androidUrl: "https://play.google.com/store/apps/details?id=com.example", desktopUrl: "https://example.com/product/42", analytics: { channel: "email", feature: "promo", campaign: "summer-2026", stage: "launch", tags: ["sale", "newsletter"] }, social: { title: "50% off today", description: "Tap to open the deal in the app.", imageUrl: "https://example.com/og/sale.png" } }); ``` ::: :::: ::::ref-section{title="Input and result types"} Import these types for autocomplete on the options you pass and the result you get back. `deepLinkUrl` is the only required input. `CreateBranchLinkInput`: - `deepLinkUrl: string` — the destination the link opens (required). - `branchKey?: string` — Branch key; falls back to env if omitted. - `branchLinkDomain?: string` — your Branch link domain; falls back to env if omitted. - `desktopUrl?`, `iosUrl?`, `androidUrl?: string` — platform fallback URLs. `desktopUrl` defaults to `deepLinkUrl` when not set. - `alias?: string` — a custom path for the generated link. - `analytics?: BranchAnalytics` — `{ channel?, feature?, campaign?, stage?, tags? }`, where `tags` is a `string[]`. - `social?: BranchSocial` — `{ title?, description?, imageUrl? }`, mapped to Open Graph fields. - `urlParams?: Record` — query parameters appended to `deepLinkUrl`. - `baseUrl?: string` — override the Branch API base (defaults to `https://api2.branch.io/v1`). - `fetch?: FetchLike` — provide a custom `fetch` implementation (defaults to the global `fetch`). `BranchLinkResult`: - `dynamicLink: string | null` — the generated Branch URL, or `null` on failure. - `errorMessage: string | null` — a message when no URL was returned, otherwise `null`. - `raw: unknown` — the raw Branch response payload, for debugging or reading extra fields. #code :::code-group ```ts [Types] import type { CreateBranchLinkInput, BranchLinkResult, BranchAnalytics, BranchSocial } from "@bdk/native/server/branch"; const input: CreateBranchLinkInput = { deepLinkUrl: "myapp://invite", urlParams: { ref: "user_123", utm_source: "share" } }; ``` ```ts [Inspecting the raw response] const result: BranchLinkResult = await createBranchLink({ deepLinkUrl: "myapp://invite" }); console.log(result.raw); // full Branch payload ``` ::: :::: ::::ref-section{title="Set your Branch credentials"} Branch needs a key and a link domain. Set them once as environment variables, or pass them per call to target a different Branch app. A value on `input` always wins; otherwise these env vars are read in order: - `branchKey` — `BDK_BRANCH_KEY`, then `BRANCH_KEY`, then `BRANCH_LIVE_KEY`. - `branchLinkDomain` — `BDK_BRANCH_LINK_DOMAIN`, then `BRANCH_LINK_DOMAIN`. If neither input nor env supplies a value, the call throws a `BdkError` with code `BDK_VALIDATION_ERROR` naming the missing field (e.g. `branchKey is required.`). ::callout{type="warn"} Only call `createBranchLink` from server-side code. Importing `@bdk/native/server/branch` into a browser bundle would leak your Branch key. :: #code :::code-group ```ts [Via environment] // Set in your server environment: // BDK_BRANCH_KEY=key_live_xxx // BDK_BRANCH_LINK_DOMAIN=example.app.link import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ deepLinkUrl: "myapp://home" }); ``` ```ts [Explicit key] import { createBranchLink } from "@bdk/native/server/branch"; const result = await createBranchLink({ branchKey: "key_live_xxx", branchLinkDomain: "example.app.link", deepLinkUrl: "myapp://home" }); ``` ```ts [Handling missing credentials] import { createBranchLink } from "@bdk/native/server/branch"; import { BdkError } from "@bdk/native"; try { await createBranchLink({ deepLinkUrl: "myapp://home" }); } catch (err) { if (err instanceof BdkError && err.code === "BDK_VALIDATION_ERROR") { console.error("Missing Branch config:", err.message); } } ``` ::: :::: --- # ChottuLink deep links URL: https://docs.native.thebdk.com/server/chottulink ::::ref-section{title="Create a deep link"} Call `createChottuLink(input)` to generate a dynamic deep link. It resolves to the finished link. Only `destinationUrl` is required. A `destinationUrl` without a scheme gets `https://` added for you. ::callout{type="warn"} Server-only. This uses your secret API key, so never import `@bdk/native/server/chottulink` into a browser bundle. :: #code :::code-group ```ts [Basic] import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "https://example.com/products/42" }); console.log(result.dynamicLink); // the generated short link, or null ``` ```ts [Protocol optional] // destinationUrl without a scheme is upgraded to https:// for you. const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "example.com/welcome" }); ``` ::: :::: ::::ref-section{title="Set credentials"} Pass `apiKey` and `domain` directly, or set them as environment variables. Use env vars in production to keep secrets out of code. The key is read from `BDK_CHOTTULINK_API_KEY`, then `CHOTTULINK_API_KEY`. The domain is read from `BDK_CHOTTULINK_DOMAIN`, then `CHOTTULINK_DOMAIN`, then `CHOTTULINK_LINK_DOMAIN`. An explicit value always wins over the environment. ::callout{type="warn"} The API key is a secret. Never ship it to the browser or commit it to source control. :: #code :::code-group ```ts [From environment] // Reads BDK_CHOTTULINK_API_KEY / CHOTTULINK_API_KEY for the key, // and BDK_CHOTTULINK_DOMAIN / CHOTTULINK_DOMAIN / CHOTTULINK_LINK_DOMAIN for the domain. import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ destinationUrl: "https://example.com/invite/abc" }); ``` ```ts [Explicit key] const result = await createChottuLink({ apiKey: "ck_live_xxx", domain: "https://links.example.com/", // protocol + trailing slash are stripped destinationUrl: "https://example.com/invite/abc" }); ``` ::: :::: ::::ref-section{title="Input options"} Every field you can pass to `createChottuLink`. Only `destinationUrl` is required; `apiKey` and `domain` are required either here or via the environment. - `destinationUrl: string` — where the link resolves to. Gets `https://` added if no scheme is present. - `apiKey?: string` — ChottuLink API key. Falls back to env vars. - `domain?: string` — the link domain. Falls back to env vars; protocol and trailing slashes are stripped. - `linkName?: string` — a human-readable name. Defaults to `selectedPath`, then to `"BDK Native Deeplink"`. - `selectedPath?: string` — custom path segment; a leading `/` is removed and the value trimmed. - `iosBehavior?: 1 | 2` — iOS open behavior. Defaults to `2`. - `androidBehavior?: 1 | 2` — Android open behavior. Defaults to `2`. - `utm?: ChottuLinkUtm` — UTM attribution (`source`, `medium`, `campaign`, `term`, `content`), all optional. - `social?: ChottuLinkSocial` — social preview (`title`, `description`, `imageUrl`), all optional. - `urlParams?: Record` — query params appended onto `destinationUrl` (empty/null/undefined values are skipped). - `extraParams?: Record` — raw fields merged into the request body for options not modeled here. - `baseUrl?: string` — override the API base. Defaults to `https://api2.chottulink.com/chotuCore/pa/v1`. - `fetch?: FetchLike` — a custom `fetch` (for testing or non-Node runtimes). #code :::code-group ```ts [Full input] import { createChottuLink } from "@bdk/native/server/chottulink"; const result = await createChottuLink({ apiKey: process.env.CHOTTULINK_API_KEY, domain: "links.example.com", destinationUrl: "https://example.com/products/42", linkName: "summer-promo", selectedPath: "/promo/summer", iosBehavior: 2, androidBehavior: 1, urlParams: { ref: "newsletter", discount: 20 }, utm: { source: "newsletter", medium: "email", campaign: "summer_sale" }, social: { title: "Summer Sale", description: "Up to 50% off", imageUrl: "https://cdn.example.com/sale.png" } }); ``` ::: :::: ::::ref-section{title="Add UTM, social preview, and custom params"} Use `utm` and `social` to attach attribution and rich link previews. Empty values are dropped, so you only send what you set. Use `urlParams` to append query parameters onto the destination URL itself. For any request-body field not modeled here, use `extraParams`. #code :::code-group ```ts [urlParams + extraParams] const result = await createChottuLink({ destinationUrl: "https://example.com/landing", // appended to the destination URL: ...?ref=app&v=3 urlParams: { ref: "app", v: 3 }, // merged into the raw request body for unmodeled options extraParams: { custom_field: "value" } }); ``` ::: :::: ::::ref-section{title="Read the result"} The resolved value is a `ChottuLinkResult` with three fields: - `dynamicLink: string | null` — the generated link (the API's `short_url`, falling back to `long_url`), or `null`. - `errorMessage: string | null` — an error message from the API response, or `null`. - `raw: unknown` — the full, unmodified parsed response. ::callout{type="warn"} Check `dynamicLink` for `null` and inspect `errorMessage` before using the link. :: #code :::code-group ```ts [Handling the result] const { dynamicLink, errorMessage, raw } = await createChottuLink({ destinationUrl: "https://example.com/share" }); if (errorMessage || !dynamicLink) { console.error("ChottuLink failed", errorMessage, raw); } else { console.log("Deep link:", dynamicLink); } ``` ::: :::: ::::ref-section{title="Handle errors"} `createChottuLink` throws a `BdkError` (importable from `@bdk/native`) when a required field is missing (`BDK_VALIDATION_ERROR`) or the request fails (`BDK_PROVIDER_ERROR`). For failed requests, `details` carries the HTTP `status` and response body. ::callout A transport or validation failure throws; an application error inside a successful response surfaces as `result.errorMessage` instead. :: #code :::code-group ```ts [Catching errors] import { createChottuLink } from "@bdk/native/server/chottulink"; import { BdkError } from "@bdk/native"; try { const result = await createChottuLink({ destinationUrl: "https://example.com/share" }); // ...use result.dynamicLink } catch (err) { if (err instanceof BdkError) { // err.code === "BDK_VALIDATION_ERROR" | "BDK_PROVIDER_ERROR" console.error(err.code, err.message, err.details); } throw err; } ``` ::: :::: --- # Firebase Dynamic Links URL: https://docs.native.thebdk.com/server/firebase-dynamic-links ::::ref-section{title="Create a short link"} Turn a deep link into a short Firebase Dynamic Link from your server. Resolves to a `FirebaseDynamicLinkResult` with `dynamicLink`, `previewLink`, and the `raw` response. Import from `@bdk/native/server/firebase-dynamic-links`; server-only. ::callout{type="warn"} Deprecated — Firebase Dynamic Links has been sunset by Google, so this helper no longer produces working links. For new work, use `@bdk/native/server/branch` (`createBranchLink`) or `@bdk/native/server/chottulink`. :: #code :::code-group ```ts [Basic] import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; const result = await createFirebaseDynamicLink({ firebaseWebApiKey: process.env.FIREBASE_WEB_API_KEY, domainUriPrefix: "https://example.page.link", deepLinkUrl: "https://example.com/items/42", iosBundleId: "com.example.app", iosAppStoreId: "1234567890", androidPackageName: "com.example.app" }); if (result.dynamicLink) { console.log("Short link:", result.dynamicLink); } else { console.error("Failed:", result.errorMessage); } ``` ::: :::: ::::ref-section{title="Set inputs from the environment"} Each required value can be passed inline or read from an environment variable. Only `deepLinkUrl` has no env fallback — always pass it. - `firebaseWebApiKey` → `BDK_FIREBASE_WEB_API_KEY`, `FIREBASE_WEB_API_KEY`, `FIREBASE_DYNAMIC_LINKS_WEB_API_KEY` - `domainUriPrefix` → `BDK_FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX`, `FIREBASE_DYNAMIC_LINK_DOMAIN_URI_PREFIX` - `iosBundleId` → `BDK_IOS_BUNDLE_ID`, `IOS_BUNDLE_ID` - `iosAppStoreId` → `BDK_IOS_APPSTORE_ID`, `IOS_APPSTORE_ID`, `IOS_APP_STORE_ID` - `androidPackageName` → `BDK_ANDROID_BUNDLE_ID`, `ANDROID_BUNDLE_ID`, `ANDROID_PACKAGE_NAME` A `domainUriPrefix` with no scheme gets `https://` prepended. ::callout{type="warn"} A missing required value (in both input and environment) throws a `BdkError` with code `BDK_VALIDATION_ERROR` before any network request. :: #code :::code-group ```ts [From environment] // With BDK_FIREBASE_WEB_API_KEY, *_DOMAIN_URI_PREFIX, IOS_BUNDLE_ID, // IOS_APPSTORE_ID, and ANDROID_PACKAGE_NAME set in the environment: import { createFirebaseDynamicLink } from "@bdk/native/server/firebase-dynamic-links"; const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/promo" }); ``` ```ts [Inline values] const result = await createFirebaseDynamicLink({ firebaseWebApiKey: "AIza...", domainUriPrefix: "example.page.link", // normalized to https://example.page.link deepLinkUrl: "https://example.com/promo", iosBundleId: "com.example.app", iosAppStoreId: "1234567890", androidPackageName: "com.example.app" }); ``` ::: :::: ::::ref-section{title="Read the result"} The promise resolves to a `FirebaseDynamicLinkResult`. Check `dynamicLink` or `errorMessage` before using the link. - `dynamicLink: string | null` — the short link, or `null` on failure. - `previewLink: string | null` — the preview link, or `null` if absent. - `errorMessage: string | null` — `null` on success, otherwise the API error. - `raw: unknown` — the full JSON response. ::callout{type="warn"} A failed API call does not throw — it returns `dynamicLink: null` with `errorMessage` set, so always branch on one of those. :: #code :::code-group ```ts [Handling the result] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42" }); if (result.errorMessage) { // Inspect result.raw for the full Firebase response payload. throw new Error(`Dynamic link failed: ${result.errorMessage}`); } return { link: result.dynamicLink, preview: result.previewLink }; ``` ::: :::: ::::ref-section{title="Add query params and redirect options"} Shape the URL and redirect behavior of the generated link. - `urlParams` appends query parameters to `deepLinkUrl`; `null`/`undefined` values are dropped. - `suffixOption` sets the path length: `"SHORT"` (case-insensitive) for a short suffix, otherwise `"UNGUESSABLE"`. - `enableForcedRedirect: true` skips the app preview page. #code :::code-group ```ts [Params + suffix] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/checkout", urlParams: { ref: "newsletter", discount: 20 }, suffixOption: "SHORT", enableForcedRedirect: true }); ``` ::: :::: ::::ref-section{title="Set platform fallbacks"} Control where users without the app installed are sent with `iosFallback` and `androidFallback` (both `FirebaseDynamicLinkFallback`). The `type` field (case- and spacing-insensitive) picks the behavior: - `"app-store-page"` / `"play-store-page"` — sends to the store; the iOS / Android default. No `url` needed. - `"same-url"` — redirects to `deepLinkUrl`. - `"fallback-url"` — redirects to a custom `url` (required), with optional `urlParams`. Use `iosIpadFallbackUrl` for a dedicated iPad fallback. #code :::code-group ```ts [Custom fallbacks] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42", iosFallback: { type: "fallback-url", url: "https://example.com/get-the-app", urlParams: { platform: "ios" } }, androidFallback: { type: "same-url" }, iosIpadFallbackUrl: "https://example.com/ipad" }); ``` ::: :::: ::::ref-section{title="Add a social preview and analytics tags"} Add a rich preview card or campaign tracking. Empty sections are omitted from the request. - `social` (`FirebaseDynamicLinkSocial`) — the preview card: `title`, `description`, `imageUrl`. - `googlePlayAnalytics` (`FirebaseGooglePlayAnalytics`) — UTM params (`utmSource`, `utmMedium`, `utmCampaign`, `utmTerm`, `utmContent`) plus `gclid`. - `itunesConnectAnalytics` (`FirebaseItunesConnectAnalytics`) — Apple campaign fields `at`, `ct`, `mt`, `pt`. #code :::code-group ```ts [With metadata] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/promo", social: { title: "Summer Sale", description: "Up to 50% off — open in the app.", imageUrl: "https://example.com/og.png" }, googlePlayAnalytics: { utmSource: "newsletter", utmMedium: "email", utmCampaign: "summer_2026" }, itunesConnectAnalytics: { at: "affiliate-token", ct: "summer_2026" } }); ``` ::: :::: ::::ref-section{title="Use a custom endpoint or fetch"} Point at a different endpoint or inject a `fetch` for testing or proxying. Set `baseUrl` (default `https://firebasedynamiclinks.googleapis.com/v1`) and pass a `fetch` (`FetchLike`) matching the global `fetch` signature. #code :::code-group ```ts [Injected fetch] const result = await createFirebaseDynamicLink({ deepLinkUrl: "https://example.com/items/42", baseUrl: "https://my-proxy.internal/firebase/v1", fetch: myCustomFetch }); ``` ::: :::: --- # Objects URL: https://docs.native.thebdk.com/reference/objects ::::ref-section{title="NativeCommandResult"} What every browser command resolves to. It tells you whether the call reached the app — not the outcome of the native action (that arrives on an event). The common case is `triggered: true`. Outside the app a command comes back with `triggered: false` — `pending` in a browser tab, `skipped` in a non-DOM environment. | Property | Type | Description | | --- | --- | --- | | `command` | `string` | The native command that was dispatched. | | `queued` | `boolean` | Whether it was added to the queue. | | `triggered` | `boolean` | Whether it was handed to the app. Branch on `!triggered` for a web fallback. | | `skipped` | `boolean` | Whether it was dropped without triggering. | | `pending` | `boolean` | Queued, waiting for the app. A browser tab outside the app resolves here. | | `reason` | `string` | Why it was skipped or pending — e.g. `waiting_for_agent`, `not_native`. | :::: ::::ref-section{title="BdkDeviceInfo"} The device snapshot returned by `bdk.ready()` and `bdk.getDeviceInfo()`, and delivered on the `deviceInfo` event. Every field is `null` until the app reports it. | Property | Type | Description | | --- | --- | --- | | `playerId` | `string \| null` | OneSignal player id for push. | | `pushToken` | `string \| null` | Device push token. | | `deviceModel` | `string \| null` | Device model name. | | `deviceOS` | `string \| null` | Operating system. | | `deviceOSVersion` | `string \| null` | OS version. | | `deviceLanguage` | `string \| null` | Device language. | | `deviceWidth` | `string \| number \| null` | Screen width. | | `deviceHeight` | `string \| number \| null` | Screen height. | | `versionName` | `string \| null` | Host app version name. | | `versionCode` | `string \| number \| null` | Host app version code. | | `biometricsAvailable` | `boolean` | Whether biometric login is available. | | `smartLoginAvailable` | `boolean` | Whether saved login is available. | | `cameraPermissionStatus` | `string \| null` | Camera permission state. | | `contactsPermissionStatus` | `string \| null` | Contacts permission state. | | `audiorecordPermissionStatus` | `string \| null` | Audio-record permission state. | | `externalstoragePermissionStatus` | `string \| null` | Storage permission state. | | `locationPermissionStatus` | `string \| null` | Location permission state. | | `idfa` | `string` | iOS advertising id. iOS only. | :::: ::::ref-section{title="MediaResult"} Delivered by `photoCaptured`, `photoSelected`, `audioRecorded`, and `screenshot`. Use `dataUri` to preview inline and `fileUrl` to upload. | Property | Type | Description | | --- | --- | --- | | `fileUrl` | `string \| null` | Hosted URL of the file — use to upload. | | `dataUri` | `string \| null` | Data URI — use to preview inline. | | `contentType` | `string \| null` | The file's MIME type. | | `data` | — | The raw file data from the device. | :::: ::::ref-section{title="BiometricResult"} Delivered on the `biometricResult` event after `bdk.auth.authenticateBiometrics()`. | Property | Type | Description | | --- | --- | --- | | `status` | `string` | `"success"` or `"failed"`. | | `platform` | `string` | `"ios"` or `"android"`. | | `data` | — | The raw native auth result. | :::: ::::ref-section{title="SmartLoginCredentials"} Delivered on the `smartLoginCredentials` event. If either field is `null`, nothing usable is saved — fall back to your login screen. | Property | Type | Description | | --- | --- | --- | | `email` | `string \| null` | The saved email. | | `password` | `string \| null` | The saved password. | :::: ::::ref-section{title="DeviceVariableResult"} Delivered on the `deviceVariable` event after reading a cached value. | Property | Type | Description | | --- | --- | --- | | `name` | `string` | The variable name. | | `data` | — | The stored value. | :::: ::::ref-section{title="UrlParam"} A query parameter you pass to `bdk.navigation.openLink({ urlParams })`. | Property | Type | Description | | --- | --- | --- | | `key` | `string` | The parameter name. | | `value` | `string \| number \| boolean` | The parameter value. | :::: ::::ref-section{title="BdkError"} Thrown by a rejected command and passed to the `onError` config callback and the `error` event. | Property | Type | Description | | --- | --- | --- | | `code` | `string` | One of `BDK_NOT_NATIVE`, `BDK_NATIVE_UNAVAILABLE`, `BDK_UNSUPPORTED_VERSION`, `BDK_UNSUPPORTED_PLATFORM`, `BDK_LISTENER_ERROR`, `BDK_PROVIDER_ERROR`, `BDK_VALIDATION_ERROR`. | | `message` | `string` | A human-readable description. | | `details` | `object` | Extra context about the error, when available. | :::: ::::ref-section{title="Event payloads"} A few events deliver raw native data rather than a fixed object. Here's what each one contains. | Event | Delivers | | --- | --- | | `contacts` | The device address book — an array of contacts, each with a name, phone number(s), and email(s). | | `location` | The device location — `latitude`, `longitude`, and a resolved `address`. | | `barcodeScanned` | The scanned code — its `type` (e.g. `qr`) and decoded `content`. | | `screenshot` | The captured image, as a `MediaResult`. | | `menuClicked` | The menu item the user tapped. | | `popupClosed` | Which button dismissed the popup. | | `datePicked` | The date/time the user selected. | | `optionPicked` | The option the user chose. | | `purchaseSuccess` / `purchaseFailed` / `receiptReceived` | `{ platform, data }` — the store's raw purchase details; verify server-side. | :::: --- # Use with AI agents URL: https://docs.native.thebdk.com/reference/ai-agents ::::ref-section{title="Built for AI agents"} The SDK is designed for AI coding tools (Lovable, Cursor, Replit, and the like). Every helper is fully typed, so the agent infers the right call straight from the types — and the docs are published as machine-readable text it can pull in directly. - **Index:** [docs.native.thebdk.com/llms.txt](https://docs.native.thebdk.com/llms.txt) — a short map of every page. - **Full text:** [docs.native.thebdk.com/llms-full.txt](https://docs.native.thebdk.com/llms-full.txt) — the entire reference in one file. Point your agent at those URLs, or paste the starter prompt below, and it has what it needs. :::: ::::ref-section{title="Prime your agent"} Drop this into your agent's context before you ask it to build a native feature. #code :::code-group ```text [Starter prompt] You're building with @bdk/native (docs: https://docs.native.thebdk.com). Install: npm install @bdk/native Browser: import { createBdkNative } from "@bdk/native/browser" const bdk = createBdkNative(); Rules: - Use the typed namespaced helpers: bdk.media.*, bdk.ui.*, bdk.navigation.*, bdk.iap.*, bdk.auth.*, bdk.location.*, bdk.device.*, bdk.share.*, bdk.permissions.*. - Results arrive on an event — subscribe with bdk.on(event, cb) BEFORE you call. - Outside the app a call resolves with triggered: false; branch on !result.triggered to fall back to the web. - Server features: import from @bdk/native/server/* in backend code only, and read credentials from env — never hardcode keys. Full machine-readable reference: https://docs.native.thebdk.com/llms-full.txt ``` ::: :::: ::::ref-section{title="The patterns to follow"} A handful of rules keep generated code correct. - **Subscribe before you call.** Interactive features (camera, pickers, biometrics, purchases) deliver their result on an event, not the returned promise. - **Branch on `!result.triggered`.** Outside the native app a call doesn't run — handle that path so your web build keeps working. - **Keep server code on the server.** `@bdk/native/server/*` runs on your backend with your credentials; never import it into a browser bundle. #code :::code-group ```ts [Correct usage] import { createBdkNative } from "@bdk/native/browser"; const bdk = createBdkNative(); // Subscribe first… bdk.on("photoCaptured", (photo) => console.log(photo.fileUrl)); // …then call. Outside the app, fall back to the web. const result = await bdk.media.capturePhoto(); if (!result.triggered) openWebFilePicker(); ``` ::: :::: ::::ref-section{title="Keys stay in the environment"} Agents copy your examples closely, so every example here reads credentials from the environment. Follow the same rule: pass keys from env (or a secrets manager), never in client code or source. ::callout{type="warn"} Server helpers read their credentials from arguments or environment variables. Don't hardcode API keys, and never ship a server import to the browser. :: #code :::code-group ```ts [Server, keys from env] import { sendPushNotification } from "@bdk/native/server/onesignal"; await sendPushNotification({ message: "Your order shipped", playerIds: [playerId], oneSignalAppId: process.env.ONESIGNAL_APP_ID, oneSignalApiKey: process.env.ONESIGNAL_API_KEY }); ``` ::: ::::