# Documentation — WebBLE

Add Web Bluetooth support to iOS Safari in under 5 minutes.

## Quick Start

WebBLE brings the Web Bluetooth API to iOS Safari via a free Safari extension + a lightweight developer SDK. Your existing Web Bluetooth code works unchanged.

### Option 1: CDN Script (Simplest)

Add one script tag before your application code:

```html
<!-- Add before your app script -->
<script src="https://ioswebble.com/webble.js"
        data-key="wbl_YOUR_API_KEY"></script>
```

On iOS Safari, if the extension isn't installed, users see a native-feeling install prompt when they try to use Bluetooth. On all other browsers, the script is a no-op.

### Option 2: NPM Package

```bash
npm install @ios-web-bluetooth/detect
```

```typescript
import { initIOSWebBLE } from '@ios-web-bluetooth/detect';

initIOSWebBLE({
  key: 'wbl_YOUR_API_KEY',
  operatorName: 'My App',
});
```

### Option 3: React SDK

```bash
npm install @ios-web-bluetooth/react
```

```typescript
import { WebBLEProvider, useBluetooth } from '@ios-web-bluetooth/react';

function App() {
  return (
    <WebBLEProvider config={{ apiKey: 'wbl_YOUR_API_KEY' }}>
      <MyBluetoothApp />
    </WebBLEProvider>
  );
}
```

## How It Works

The system has three parts:

1. **Safari Extension** — A free iOS app with a Safari Web Extension that bridges CoreBluetooth to web pages
2. **Content Script** — Injected by the extension at `document_start`, stays dormant by default, and activates the native WebBLE pipeline for the current tab when the user requests it
3. **Developer SDK** — Detects the extension, shows install prompts on iOS Safari, and provides React hooks for BLE operations

When a user visits your site on iOS Safari:

1. SDK checks if the extension is installed
2. If **yes**: the SDK activates WebBLE and the optional CDN bridge makes `navigator.bluetooth` available. Your code runs unchanged.
3. If **no**: When the user tries a BLE action, an iOS-native install prompt appears guiding them to the free App Store download

> **Zero lock-in.** WebBLE polyfills the standard Web Bluetooth API. If the user has Chrome, Edge, or any browser with native support, the SDK stays out of the way. Your code is always standard.

## Browser Support

| Browser | Web Bluetooth | WebBLE Action |
|---------|--------------|------------------|
| Safari iOS 26+ | No (Apple blocks it) | Extension provides full support |
| Chrome 56+ | Native | No-op (native works) |
| Edge 79+ | Native | No-op |
| Firefox | No | Not supported |

## CDN Script

The CDN script (`webble.js`) is a zero-dependency, <10KB script that handles everything automatically.

### Basic Usage

```html
<script src="https://ioswebble.com/webble.js"
        data-key="wbl_YOUR_API_KEY"></script>
```

### What It Does

- Detects iOS Safari and checks for the extension
- Polyfills `navigator.bluetooth` via the extension's `navigator.webble`
- Injects a Smart App Banner meta tag (passive install layer)
- On `requestDevice()` without extension: shows an iOS-native bottom sheet install prompt
- Saves return context (Universal Link + localStorage) for the return-to-web-app flow
- Reports detection analytics (installed vs. not installed)

### Configuration via Data Attributes

| Attribute | Description | Default |
|-----------|-------------|---------|
| `data-key` | Your API key (required) | — |
| `data-name` | Your app name shown in prompts | Page title |
| `data-prompt` | Set to `"none"` to disable auto-prompt | Auto |

> **Tip:** The CDN script automatically injects a `<meta name="apple-itunes-app">` Smart App Banner tag, giving you a passive always-on install layer for free.

## React SDK

The `@ios-web-bluetooth/react` package provides a complete React SDK with hooks for every BLE operation.

```bash
npm install @ios-web-bluetooth/react
```

### Provider Setup

Wrap your app with `WebBLEProvider`:

```typescript
import { WebBLEProvider } from '@ios-web-bluetooth/react';

function App() {
  return (
    <WebBLEProvider config={{
      apiKey: 'wbl_YOUR_API_KEY',
      operatorName: 'My App',
    }}>
      <MyBluetoothApp />
    </WebBLEProvider>
  );
}
```

### Using Hooks

```typescript
import { useWebBLE, useDevice, useCharacteristic } from '@ios-web-bluetooth/react';

function HeartRateMonitor() {
  const { requestDevice, isAvailable } = useWebBLE();

  const handleConnect = async () => {
    const device = await requestDevice({
      filters: [{ services: ['heart_rate'] }],
    });
    // device is a standard BluetoothDevice
  };

  return (
    <button onClick={handleConnect} disabled={!isAvailable}>
      Connect Heart Rate Monitor
    </button>
  );
}
```

### Available Hooks

| Hook | Purpose |
|------|---------|
| `useWebBLE()` | Core context: availability, extension status, requestDevice |
| `useBluetooth()` | Simplified BLE access with auto-detection |
| `useDevice()` | Device connection state, services, connect/disconnect |
| `useCharacteristic()` | Read, write, subscribe to a GATT characteristic |
| `useNotifications()` | Subscribe to characteristic notifications with history |
| `useScan()` | BLE scanning with device list management |
| `useConnection()` | Connection state, RSSI monitoring, auto-reconnect |

### Components

| Component | Purpose |
|-----------|---------|
| `<DeviceScanner />` | Pre-built device scanner UI |
| `<ServiceExplorer />` | GATT service/characteristic browser |
| `<ConnectionStatus />` | Connection state indicator |
| `<InstallationWizard />` | Extension install prompt (iOS-native bottom sheet) |

> **Optional: @ios-web-bluetooth/detect.** If you install `@ios-web-bluetooth/detect` as a peer dependency, the `WebBLEProvider` will automatically show iOS-native install prompts when an `apiKey` is configured. Without it, the basic `InstallationWizard` component is used as a fallback.

## @ios-web-bluetooth/detect

A lightweight (~4KB) package focused on detection and install prompting. Use this if you want fine-grained control or aren't using React.

```bash
npm install @ios-web-bluetooth/detect
```

### Programmatic API

```typescript
import { initIOSWebBLE } from '@ios-web-bluetooth/detect';

await initIOSWebBLE({
  key: 'wbl_YOUR_API_KEY',
  operatorName: 'FitTracker',
  banner: {
    mode: 'sheet',          // 'sheet' (default) or 'banner'
    dismissDays: 14,        // suppress after dismiss
  },
  onReady() { /* extension installed */ },
  onNotInstalled() { /* show fallback */ },
});
```

### Auto-Init (Zero Code)

Import the auto module and configure via meta tags:

```html
<meta name="ioswebble-key" content="wbl_YOUR_API_KEY">
<meta name="ioswebble-name" content="My App">
<script type="module">
  import '@ios-web-bluetooth/detect/auto';
</script>
```

### React Integration

```typescript
import { IOSWebBLEProvider, useIOSWebBLE } from '@ios-web-bluetooth/detect/react';

function Layout({ children }) {
  return (
    <IOSWebBLEProvider
      apiKey="wbl_YOUR_API_KEY"
      operatorName="FitTracker">
      {children}
    </IOSWebBLEProvider>
  );
}
```

## Detection API

### `isIOSSafari()`

Returns `true` if the current browser is Safari on iOS (including iPad).

```typescript
import { isIOSSafari } from '@ios-web-bluetooth/detect';

if (isIOSSafari()) {
  // Running on iOS Safari
}
```

### `isExtensionInstalled()`

Returns a `Promise<boolean>` that resolves after checking for the extension (waits up to 2 seconds for injection).

```typescript
import { isExtensionInstalled } from '@ios-web-bluetooth/detect';

const installed = await isExtensionInstalled();
```

### `initIOSWebBLE(options)`

Main entry point. Detects iOS Safari, checks extension, shows install prompt if needed.

| Option | Type | Description |
|--------|------|-------------|
| `key` | `string` | API key (required) |
| `operatorName` | `string?` | App name for install prompt |
| `banner` | `object \| false` | Banner configuration or `false` to disable |
| `onReady` | `() => void` | Called when extension is detected |
| `onNotInstalled` | `() => void` | Called when extension is NOT installed |

## Banner Options

Configure how the install prompt appears (passed as `banner` in `initIOSWebBLE` or `showInstallBanner`).

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `mode` | `'sheet' \| 'banner'` | `'sheet'` | Bottom sheet (iOS-native) or lightweight banner bar |
| `position` | `'top' \| 'bottom'` | `'bottom'` | Bar position (banner mode only) |
| `text` | `string` | Auto-generated | Custom banner text |
| `buttonText` | `string` | `'Get WebBLE (Free)'` | CTA button text |
| `operatorName` | `string` | Page title / hostname | Your app name in the prompt |
| `appStoreUrl` | `string` | WebBLE listing | App Store URL override |
| `dismissDays` | `number` | `14` | Days to suppress after dismiss |
| `style` | `Record<string, string>` | — | Custom CSS for banner bar mode |

## React Hooks

### `useWebBLE()`

Returns the core WebBLE context. Must be used inside `<WebBLEProvider>`.

| Property | Type | Description |
|----------|------|-------------|
| `isAvailable` | `boolean` | Whether Bluetooth is available |
| `isExtensionInstalled` | `boolean` | Whether the WebBLE extension is detected |
| `isLoading` | `boolean` | Whether detection is in progress |
| `isScanning` | `boolean` | Whether a BLE scan is active |
| `devices` | `BluetoothDevice[]` | Discovered devices |
| `error` | `Error \| null` | Last error |
| `requestDevice()` | `function` | Request a BLE device (standard API) |
| `getDevices()` | `function` | Get previously paired devices |
| `requestLEScan()` | `function` | Start a BLE scan |
| `stopScan()` | `function` | Stop the current scan |

### `useIOSWebBLE()`

From `@ios-web-bluetooth/detect/react`. Returns detection state.

| Property | Type | Description |
|----------|------|-------------|
| `isInstalled` | `boolean \| null` | Extension installed (`null` while detecting) |
| `isDetecting` | `boolean` | Whether detection is still running |
| `isIOSSafari` | `boolean` | Whether on iOS Safari |

## Events

The SDK dispatches custom events on the `window` object:

| Event | When |
|-------|------|
| `ioswebble:ready` | Extension detected and ready |
| `ioswebble:notinstalled` | Extension NOT installed on iOS Safari |
| `webble:extension:ready` | Extension content script injected (from extension itself) |

```typescript
// Listen for extension detection
window.addEventListener('ioswebble:ready', () => {
  console.log('Extension is active!');
});
```

## Background Sync

> **iOS-only.** Background Sync runs through the WebBLE companion app, and notification-based workflows require notification permission. Call `requestPermission()` before registering characteristic alerts or beacon scans.

> Standalone mode remains supported for foreground Web Bluetooth in Safari, but Background Sync and Live Activities require the companion app relay path. Live Activities are relay-only because they reflect companion-app-managed monitoring state, not transient foreground extension activity.

Use `navigator.webble.backgroundSync` to keep approved BLE devices active while Safari is backgrounded and to deliver iOS notifications for important BLE events.

### `requestPermission()`

Call this from a direct user gesture. It resolves to `'granted'`, `'denied'`, or `'prompt'`.

```typescript
document.querySelector('#enable-background-sync')?.addEventListener('click', async () => {
  const permission = await navigator.webble.backgroundSync.requestPermission();

  if (permission !== 'granted') {
    console.log('Background notifications not enabled:', permission);
    return;
  }

  console.log('Background notifications enabled');
});
```

### Background Connection

`requestBackgroundConnection({ deviceId })` asks the companion app to keep a granted device connected in the background. It is silent by design and does not show an iOS notification on its own.

```typescript
const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['heart_rate'] }]
});

await device.gatt.connect();

const connection = await navigator.webble.backgroundSync.requestBackgroundConnection({
  deviceId: device.id
});

console.log('Keep-alive registration:', connection.id);
```

### Characteristic Notifications

`registerCharacteristicNotifications(options)` fires an iOS notification when a characteristic changes while Safari is backgrounded.

`condition` is required and is evaluated natively before iOS notification delivery.

| Option | Type | Description |
|--------|------|-------------|
| `deviceId` | `string` | Device ID from a previous `requestDevice()` call in the current tab |
| `serviceUUID` | `BluetoothServiceUUID` | Service containing the characteristic to watch |
| `characteristicUUID` | `BluetoothCharacteristicUUID` | Characteristic to watch for value changes |
| `condition` | `NotificationCondition` | Native condition that gates notification delivery |
| `template` | `NotificationTemplate` | Notification content shown by iOS |
| `replyAction` | `ReplyActionConfig?` | Optional inline reply configuration |
| `cooldownSeconds` | `number?` | Minimum seconds between notifications for the same registration (default `5`) |

#### `NotificationTemplate`

| Property | Type | Description |
|----------|------|-------------|
| `title` | `string` | Notification title. Supports placeholders |
| `body` | `string` | Notification body. Supports placeholders |
| `url` | `string` | HTTPS URL opened when the user taps the notification |
| `sound` | `boolean?` | Play the default notification sound. Defaults to `true` |

Supported placeholders:

| Placeholder | Meaning |
|-------------|---------|
| `{{device.name}}` | Device name |
| `{{device.id}}` | Device ID |
| `{{value.hex}}` | Characteristic value as lowercase hex |
| `{{value.utf8}}` | Characteristic value decoded as UTF-8 |
| `{{value.int16be}}` | First two bytes as signed big-endian int16 |
| `{{value.int32be}}` | First four bytes as signed big-endian int32 |
| `{{timestamp}}` | ISO 8601 timestamp for the received value |

#### `ReplyActionConfig`

| Property | Type | Description |
|----------|------|-------------|
| `actionTitle` | `string` | Inline reply button label |
| `placeholder` | `string?` | Input placeholder shown in the reply sheet |

Use friendly text here for forward compatibility. The initial release uses the standard iOS inline reply UI.

```typescript
const messageAlerts = await navigator.webble.backgroundSync.registerCharacteristicNotifications({
  deviceId: device.id,
  serviceUUID: '12345678-1234-1234-1234-123456789abc',
  characteristicUUID: '87654321-4321-4321-4321-cba987654321',
  condition: {
    decode: 'uint8',
    operator: 'changed',
    threshold: 0
  },
  cooldownSeconds: 15,
  template: {
    title: 'Message from {{device.name}}',
    body: '{{value.utf8}}',
    url: 'https://example.com/messages',
    sound: true
  },
  replyAction: {
    actionTitle: 'Reply',
    placeholder: 'Send a quick response'
  }
});
```

### Beacon Scanning

`registerBeaconScanning(options)` matches BLE advertisements and delivers an iOS notification when a filter hits.

| Option | Type | Description |
|--------|------|-------------|
| `filters` | `BLEScanFilter[]` | One or more advertisement filters. At least one filter must include `services` |
| `cooldownSeconds` | `number?` | Minimum seconds between notifications for the same detected device (default `5`) |
| `template` | `NotificationTemplate` | Notification content shown by iOS |

`cooldownSeconds` throttles how often iOS shows a notification. It does not control scan frequency.

#### `BLEScanFilter`

| Property | Type | Description |
|----------|------|-------------|
| `services` | `BluetoothServiceUUID[]?` | Service UUIDs to match in advertisements |
| `namePrefix` | `string?` | Optional device or advertisement name prefix |

> **Fast catch-up.** Background beacon delivery on iOS is designed for quick catch-up rather than fixed real-time discovery. Use service UUID filters and sensible cooldowns for the most reliable alerts.

```typescript
const beaconRegistration = await navigator.webble.backgroundSync.registerBeaconScanning({
  filters: [
    { services: ['feaa'] },
    {
      services: ['12345678-1234-1234-1234-123456789abc'],
      namePrefix: 'Sensor-'
    }
  ],
  cooldownSeconds: 60,
  template: {
    title: 'Nearby BLE beacon',
    body: '{{device.name}} is advertising nearby',
    url: 'https://example.com/beacons',
    sound: true
  }
});
```

### Managing Registrations

Use `getRegistrations()`, `unregister(id)`, and `update(id, template)` to manage registrations for the current origin.

```typescript
const registrations = await navigator.webble.backgroundSync.getRegistrations();

for (const registration of registrations) {
  console.log(registration.id, registration.type);
}

await navigator.webble.backgroundSync.update(messageAlerts.id, {
  title: 'New device message',
  body: '{{value.utf8}}'
});

await navigator.webble.backgroundSync.unregister(beaconRegistration.id);
```

### `BackgroundRegistration` handle

| Property / Method | Type | Description |
|-------------------|------|-------------|
| `id` | `string` | Unique registration ID |
| `type` | `'connection' \| 'characteristic-notification' \| 'beacon-scan'` | Registration kind |
| `createdAt` | `number` | Registration creation timestamp in Unix milliseconds |
| `lastTriggeredAt` | `number?` | Last delivery timestamp, when available |
| `unregister()` | `() => Promise<void>` | Remove this registration |
| `update()` | `(template: Partial<NotificationTemplate>) => Promise<void>` | Update title, body, URL, or sound |

### Security notes

- `template.url` must use `https://` and is only opened when it matches the same origin as the page that registered it.
- `deviceId` is scoped to devices previously granted via `requestDevice()` for the current site flow.
- Template values are sanitized before iOS renders the notification, and only supported `{{...}}` placeholders are interpolated.

### Complete example

```typescript
const permission = await navigator.webble.backgroundSync.requestPermission();

if (permission !== 'granted') {
  throw new Error('Enable notifications to use background BLE alerts');
}

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['heart_rate'] }]
});

await device.gatt.connect();

await navigator.webble.backgroundSync.requestBackgroundConnection({
  deviceId: device.id
});

const heartRateAlerts = await navigator.webble.backgroundSync.registerCharacteristicNotifications({
  deviceId: device.id,
  serviceUUID: 'heart_rate',
  characteristicUUID: 'heart_rate_measurement',
  condition: {
    decode: 'uint8',
    operator: 'gt',
    threshold: 120
  },
  cooldownSeconds: 30,
  template: {
    title: 'Heart rate update',
    body: '{{device.name}} sent {{value.hex}} at {{timestamp}}',
    url: 'https://example.com/monitor',
    sound: true
  }
});

const activeRegistrations = await navigator.webble.backgroundSync.getRegistrations();
console.log(activeRegistrations.map(({ id, type }) => ({ id, type })));

await heartRateAlerts.update({
  body: 'Latest payload from {{device.name}}: {{value.hex}}'
});

// Later, when you no longer need background delivery:
await heartRateAlerts.unregister();
```

## Troubleshooting

### Extension not detected after installation

The user may have enabled the extension but not granted website permissions. They need to:

1. In Safari, tap **aA** in the address bar
2. Tap the **WebBLE** icon (may have a warning badge)
3. Choose **"Always Allow"**
4. Then **"Always Allow on Every Website"**

> **"Allow for One Day" expires silently.** If a user chose "Allow for One Day" instead of "Always Allow," the extension will stop working the next day with no notification. Guide users to "Always Allow" in your onboarding.

### Extension works on some sites but not others

The user may have chosen per-site permissions. They need to grant "Always Allow on Every Website" for consistent behavior.

### HTTPS required

Web Bluetooth requires a secure context. Ensure your site uses HTTPS (or `localhost` for development).

### `navigator.bluetooth` is undefined

- On iOS Safari: the extension is not installed or not granted permissions
- On Firefox: Web Bluetooth is not supported
- On Chrome/Edge: should work natively — check `chrome://flags/#enable-experimental-web-platform-features`

## Privacy

WebBLE is designed with privacy as a core principle:

- **All BLE data processed locally.** Bluetooth communication happens entirely on-device between the browser and CoreBluetooth. No data is ever proxied through a server.
- **No browsing data collected.** The extension cannot see page content, URLs, or any browsing activity. It only activates when a website specifically calls the Web Bluetooth API.
- **Minimal analytics.** The SDK reports only two data points: whether the extension is installed, and the origin domain. No PII, no device data, no cookies.
- **Open source.** The extension source code is available for audit on GitHub.

> **"Always Allow on Every Website" does NOT mean "always watching."** It means the extension is *available* on every website, but only activates when a site explicitly requests Bluetooth access via the Web Bluetooth API.
