# appmintNative (AI-built apps) - JavaScript bridge API

> The typed wrappers every AI-built project imports from @/lib/appmintNative. Each picks the native path inside the app and a web path, or an honest none, in a browser - one call works everywhere.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/native

### `appmintNative.actionSheet`

```js
actionSheet(title: string, options: { title: string; destructive?: boolean }[]): Promise<number | null>
```

**Example**

Shows a "choose one of these" bottom sheet - the native control for a row's actions menu.

**Returns:** `Promise<number | null>` - the index of the chosen option, or `null` when the user dismissed the sheet (backdrop tap, Escape, the system cancel). Dismissal is normal, not an error. **Needs:** nothing.

```ts
import { actionSheet, haptic } from '@/lib/appmintNative';

export async function onNoteMenu(note: { id: string }, api: {
  edit(id: string): void; share(id: string): void; remove(id: string): void;
}) {
  const i = await actionSheet('Note', [
    { title: 'Edit' },
    { title: 'Share' },
    { title: 'Delete', destructive: true },
  ]);
  if (i === null) return;                 // dismissed
  if (i === 0) api.edit(note.id);
  if (i === 1) api.share(note.id);
  if (i === 2) { haptic('warning'); api.remove(note.id); }
}
```

Open it from a visible button, and from a long press on the row:

```ts
import { useEffect, useRef } from 'react';
import { actionSheet, longPress } from '@/lib/appmintNative';

export function Row({ label }: { label: string }) {
  const ref = useRef<HTMLLIElement>(null);
  const openMenu = () => actionSheet(label, [{ title: 'Rename' }, { title: 'Delete', destructive: true }]);
  useEffect(() => (ref.current ? longPress(ref.current, () => void openMenu()) : undefined), []);
  return <li ref={ref}>{label} <button aria-label="More actions" onClick={() => void openMenu()}>⋯</button></li>;
}
```

The Capacitor-shaped form `ActionSheet.showActions({ title?, options })` takes the same options and resolves `{ index }`, with `-1` for a dismissal:

```ts
import { ActionSheet } from '@/lib/appmintNative';

const { index } = await ActionSheet.showActions({
  title: 'Photo',
  options: [{ title: 'Save' }, { title: 'Delete', destructive: true }],
});
if (index === 1) console.log('delete');   // -1 = dismissed; never test with `if (index)`
```

**Notes:**
- In the Android app the Capacitor ActionSheet plugin draws the system sheet; `destructive` uses its destructive style.
- Elsewhere it is a real sheet in the app's palette (`--surface`, `--ink`, `--danger`), with backdrop, Escape, focus moved into the sheet and a selection haptic.
- An empty `options` list resolves `null` at once. For any content other than a list of actions, use `sheet()`.

### `appmintNative.ActionSheet`

```js
import { ActionSheet } from '@/lib/appmintNative'
```

**Example**

Documented together with `appmintNative.actionSheet` - the example there shows this one too.

Shows a "choose one of these" bottom sheet - the native control for a row's actions menu.

**Returns:** `Promise<number | null>` - the index of the chosen option, or `null` when the user dismissed the sheet (backdrop tap, Escape, the system cancel). Dismissal is normal, not an error. **Needs:** nothing.

```ts
import { actionSheet, haptic } from '@/lib/appmintNative';

export async function onNoteMenu(note: { id: string }, api: {
  edit(id: string): void; share(id: string): void; remove(id: string): void;
}) {
  const i = await actionSheet('Note', [
    { title: 'Edit' },
    { title: 'Share' },
    { title: 'Delete', destructive: true },
  ]);
  if (i === null) return;                 // dismissed
  if (i === 0) api.edit(note.id);
  if (i === 1) api.share(note.id);
  if (i === 2) { haptic('warning'); api.remove(note.id); }
}
```

Open it from a visible button, and from a long press on the row:

```ts
import { useEffect, useRef } from 'react';
import { actionSheet, longPress } from '@/lib/appmintNative';

export function Row({ label }: { label: string }) {
  const ref = useRef<HTMLLIElement>(null);
  const openMenu = () => actionSheet(label, [{ title: 'Rename' }, { title: 'Delete', destructive: true }]);
  useEffect(() => (ref.current ? longPress(ref.current, () => void openMenu()) : undefined), []);
  return <li ref={ref}>{label} <button aria-label="More actions" onClick={() => void openMenu()}>⋯</button></li>;
}
```

The Capacitor-shaped form `ActionSheet.showActions({ title?, options })` takes the same options and resolves `{ index }`, with `-1` for a dismissal:

```ts
import { ActionSheet } from '@/lib/appmintNative';

const { index } = await ActionSheet.showActions({
  title: 'Photo',
  options: [{ title: 'Save' }, { title: 'Delete', destructive: true }],
});
if (index === 1) console.log('delete');   // -1 = dismissed; never test with `if (index)`
```

**Notes:**
- In the Android app the Capacitor ActionSheet plugin draws the system sheet; `destructive` uses its destructive style.
- Elsewhere it is a real sheet in the app's palette (`--surface`, `--ink`, `--danger`), with backdrop, Escape, focus moved into the sheet and a selection haptic.
- An empty `options` list resolves `null` at once. For any content other than a list of actions, use `sheet()`.

### `appmintNative.addContact`

```js
addContact(contact: Contact): Promise<boolean>
```

**Example**

Saves a new contact into the phone's address book. Android app only: the shell's bridge saves it directly (no Contacts screen opens).

**Returns:** `Promise<boolean>` - `true` once saved, `false` when the user refused the contacts permission or the save failed. It rejects with an `Error` whose `code` is `'not_enabled'` (the build did not switch on writing contacts) or `'foreign_origin'`, and after a 5-minute wait with no answer. **Needs:** the **Edit Contacts** switch in Step 4 (Access) - the build turns it on when your code calls this - and the user's permission on first use.

**Save a customer from a form.**

```ts
import { addContact, can } from '@/lib/appmintNative';

export async function saveCustomer(name: string, phone: string, email: string): Promise<string> {
  if (can('contactAdd') === 'none') return 'Saving contacts works in the app only.';
  try {
    const ok = await addContact({ name, phones: [phone], emails: email ? [email] : [] });
    return ok ? 'Saved to contacts' : 'Allow contacts access to save it';
  } catch (e) {
    return (e as { code?: string }).code === 'not_enabled'
      ? 'Saving contacts is not switched on in this app.'
      : 'No answer from the phone. Try again.';     // 5-minute timeout
  }
}
```

**Notes:** It saves at once - no contact editor opens - so confirm with the user in your own screen first. It saves the phone numbers as mobile numbers and the contact on the device (no account). Writing contacts is sensitive: declare it in your Play Data safety form and privacy policy.

### `appmintNative.App`

```js
App.getInfo(): Promise<{ name, id, build, version }> / App.exitApp(): void
```

**Example**

The app's name, package id and version, and a way to close the app - in the shape of Capacitor's App plugin.

**Returns:** `getInfo()` → `Promise<{ name: string; id: string; build: string; version: string }>`. `exitApp()` → `void`. **Needs:** nothing.

```ts
import { App } from '@/lib/appmintNative';

const info = await App.getInfo();
aboutLine.textContent = `${info.name} ${info.version}${info.build ? ` (${info.build})` : ''}`;
```

**Close the app** from a "Quit" item that is not the only way forward:

```ts
import { App, Dialog } from '@/lib/appmintNative';

async function quit() {
  const { value } = await Dialog.confirm({ title: 'Quit?', message: 'Unsaved changes will be lost.' });
  if (value) App.exitApp();
}
```

**Notes:** In the app `getInfo` reads the Capacitor App plugin; when that is missing it uses the shell bridge's name and version (`id` and `build` are then `''`), and on the web `document.title` with an empty version. `exitApp` uses the shell's `closeWindow` in the app and does nothing on the web, so never make Quit the only way forward. For pause/resume events and deep links see `Capacitor.Plugins.App`, `launchUrl()` and `onDeepLink()`.

### `appmintNative.appLauncher`

```js
appLauncher.canOpen(url): Promise<boolean> / appLauncher.open(url): Promise<boolean>
```

**Example**

Opens another app by link - `whatsapp://send?text=…`, `tel:`, `mailto:`, `geo:`, or an https link the OS gives to its app - and asks first whether that can work.

**Returns:** `canOpen()` → `Promise<boolean>`. `open()` → `Promise<boolean>` (`false` when no app took the link). **Needs:** nothing.

```ts
import { appLauncher } from '@/lib/appmintNative';

async function chat(phone: string, text: string) {
  const url = `whatsapp://send?phone=${phone}&text=${encodeURIComponent(text)}`;
  if (!(await appLauncher.canOpen(url))) {
    showMessage('WhatsApp is not installed on this phone.');
    return;
  }
  await appLauncher.open(url);
}
```

**Call or email** (no check needed; a phone always has an app for these):

```ts
import { appLauncher } from '@/lib/appmintNative';

await appLauncher.open('tel:+15551234567');
await appLauncher.open('mailto:help@example.com?subject=Order%2042');
```

**Notes:** In the app it uses the Capacitor AppLauncher plugin. On Android 11+ `canOpen` can only see schemes the app declares (`https`, `geo`, `google.navigation`, `tel`, `mailto`, `sms`, `whatsapp`, `intent`, `upi`); for others it answers `false` even when the app is installed - call `open()` and check its result instead. On the web a custom scheme cannot be checked, so `canOpen` is `true` only for http(s), and `open` navigates to the link.

### `appmintNative.appLock`

```js
appLock({ timeoutSec?, title?, onLocked? }) — from '@/lib/appmintNative'
```

**Example**

Locks the app again when the user comes back after it was in the background for a while: it asks for fingerprint / face / PIN (`unlock()`), and turns on the privacy screen so the app switcher does not show your content.

**Returns:** a function that stops the lock (call it on sign-out or unmount). `onLocked(true)` fires when the app locks, `onLocked(false)` after a successful unlock. **Needs:** the same as `unlock()` - Android: **Fingerprint** ticked when you build.

```ts
import { useEffect, useState } from 'react';
import { appLock } from '@/lib/appmintNative';

/** Call once in your root component; render a lock screen while it returns true. */
export function useAppLock(): boolean {
  const [locked, setLocked] = useState(false);
  useEffect(() => appLock({
    timeoutSec: 60,                 // lock after 1 minute in the background (default 30)
    title: 'Unlock MyNotes',
    onLocked: setLocked,            // true → cover the screen, false → show the app again
  }), []);                          // appLock returns its own stop function = the cleanup
  return locked;
}
```

In the root component: `const locked = useAppLock(); if (locked) return <LockScreen />;`.

**Notes:** If the device has nothing to unlock with (`can('unlock') === 'none'`) it does nothing and returns a no-op - an app lock that could never open would trap the user. If the user cancels, it asks again up to five times, then stays locked (keep your lock screen up) until the app goes to the background and comes back. This is separate from the **PIN Lock** option in the build wizard, which the app shows natively at launch.

### `appmintNative.appUpdate`

```js
appUpdate.check(): Promise<AppUpdateInfo | null> / appUpdate.start(): Promise<boolean>
```

**Example**

Checks Google Play for a newer version and starts Play's in-app update (downloads and installs in place).

**Returns:** `check()` → `Promise<{ currentVersion, availableVersion?, currentVersionCode?, updateAvailable, inPlace } | null>` (`null` on the web, without Play services, or when Play fails). `updateAvailable` is the store's own answer. `start()` → `Promise<boolean>`: `true` only when Play's update completed, `false` when the user cancelled, it failed or none was available. **Needs:** nothing; the app must be installed from Google Play.

```ts
import { appUpdate } from '@/lib/appmintNative';

async function offerUpdate() {
  const info = await appUpdate.check();
  if (!info?.inPlace) return;                    // Play can install an update now
  showBanner('A new version is ready', async () => {
    const done = await appUpdate.start();        // Play's full-screen flow / the App Store page
    if (!done) showBanner('Update later from the store.', null);
  });
}
```

**Notes:** `updateAvailable` is Play's own answer; `inPlace` adds that Play can run its in-app update right now. `availableVersion` is Play's version CODE (Play gives no name) - compare it with `currentVersionCode`, never with `currentVersion`, which is the version name.

### `appmintNative.backgroundSync`

```js
import { backgroundSync } from '@/lib/appmintNative'
```

**Example**

Native HTTP requests that run while the app is closed: `enqueue` (a one-off, durable auto-save sent when online), `schedule` (a repeating request, at least every 15 minutes), `cancel` and `list`.

**Returns:** `enqueue` / `schedule` → `Promise<boolean>`; `cancel` → `Promise<void>`; `list` → `Promise<BackgroundJob[]>` (`{ id, url, periodic, createdAt?, pendingDelivery? }` - `pendingDelivery` on Android: a one-off not delivered yet). **Needs:** nothing in the Android app (it uses WorkManager). On the web `can('backgroundSync')` is `'none'` and `enqueue` / `schedule` resolve `false`.

**Durable auto-save:** the same id replaces the save that has not been sent yet.

```ts
import { backgroundSync, can } from '@/lib/appmintNative';

export async function saveDraft(draft: object, token: string) {
  if (can('backgroundSync') === 'none') return false;   // web: save normally instead
  return backgroundSync.enqueue({
    id: 'draft',
    url: 'https://api.example.com/drafts',
    method: 'PUT',
    headers: { Authorization: `Bearer ${token}` },
    body: JSON.stringify(draft),
  });
}
```

**Repeating sync, list and cancel:**

```ts
await backgroundSync.schedule({
  id: 'sync', url: 'https://api.example.com/sync', body: '{}', intervalMinutes: 60,
});

const jobs = await backgroundSync.list();
console.log(jobs.map((j) => `${j.id} ${j.periodic ? 'repeating' : 'one-off'}`));

await backgroundSync.cancel('sync');
```

**Raw event** (what the wrapper waits for on Android):

```js
window.addEventListener('appmint:work', function (e) {
  // { id, status: 'enqueued' | 'scheduled' | 'cancelled' | 'failed', reason?, intervalMinutes? }
  // or { requestId, jobs: [...] } for a list
  console.log('work', e.detail);
});
```

**Notes:** `true` means the job was queued, not delivered. `false` means it was not queued: the shell answered `status: 'failed'` (a URL not starting with `http`, an empty id, or the system refused) or did not answer within 10 seconds. The page's JavaScript never runs in the background; only this request does. A 4xx reply is not retried; network errors and 5xx retry up to 8 times.

### `appmintNative.badge`

```js
import { badge } from '@/lib/appmintNative'
```

**Example**

Sets or clears the unread count on the app's launcher icon.

**Returns:** `badge.set(count)` → `Promise<boolean>` (`false` when the Badge plugin is not there, e.g. on the web); `badge.clear()` → `Promise<void>`. **Needs:** nothing on Android (it uses the built-in Badge plugin).

```ts
import { badge, pushInbox } from '@/lib/appmintNative';

export async function syncBadge() {
  const unread = pushInbox().filter((m) => !m.read).length;
  await badge.set(unread);        // 0 or less clears it
}

export async function onInboxOpened() {
  await badge.clear();
}
```

**Notes:** on Android the look depends on the launcher: many show a number, some show only a dot, some show nothing - do not put information only in the badge. On the web `can('badge')` is `'none'`.

### `appmintNative.barcodeFormats`

```js
barcodeFormats(): Promise<string[]>
```

**Example**

Lists the barcode and QR formats this device's `BarcodeDetector` can read.

**Returns:** `Promise<string[]>` - format names such as `'qr_code'`, `'ean_13'`, `'code_128'`; an empty array when barcode reading is not available here. It never rejects. **Needs:** nothing.

Show the "Scan a product barcode" option only when the device reads that format:

```ts
import { useEffect, useState } from 'react';
import { barcodeFormats } from '@/lib/appmintNative';

export function useCanReadEan(): boolean {
  const [ok, setOk] = useState(false);
  useEffect(() => {
    void barcodeFormats().then((f) => setOk(f.includes('ean_13')));
  }, []);
  return ok;
}
```

Narrow a scan to what the device supports:

```ts
import { barcodeFormats, readBarcodeFrom, type BarcodeFormat } from '@/lib/appmintNative';

declare const photo: File;
const wanted: BarcodeFormat[] = ['qr_code', 'ean_13'];
const have = await barcodeFormats();
const codes = await readBarcodeFrom(photo, wanted.filter((f) => have.includes(f)));
```

**Notes:**
- It asks the browser engine's `BarcodeDetector.getSupportedFormats()`. The Android app's WebView has it (measured: aztec, code_128, code_39, code_93, codabar, data_matrix, ean_13, ean_8, itf, pdf417, qr_code, upc_a, upc_e); many desktop browsers and Safari do not, and answer `[]`.
- This list is about `readBarcodeFrom()` / `scanBarcode()`. `scanCode()` in the app uses Google's scanner instead and does not depend on it.

### `appmintNative.bleSensors`

```js
import { bleSensors } from '@/lib/appmintNative'
```

**Example**

Standard Bluetooth fitness sensors - heart rate, cycling speed/cadence, power, running speed/cadence and sensor battery - with the numbers already parsed, in the Android app.

**Returns:** every method returns `Promise<void>` (`setWheelCircumference` returns `void`). Everything else arrives as `appmint:ble` window events with `detail` = `{kind:'device', address, name, sensors, rssi}` | `{kind:'state', state, address?, services?}` | `{kind:'data', type, data}` | `{kind:'error', error}`. **Needs:** Bluetooth, which the build turns on by itself when the code uses `bleSensors`. In a browser (web preview) there is no sensor bridge: `startScan()` answers with `{kind:'error', error:'not_enabled'}`, so handle that error. To know up front, test `can('bluetooth') === 'native'` - not `!== 'none'`: in Chrome `can('bluetooth')` is `'web'` (Web Bluetooth), which `bleSensors` does not use.

```ts
import { useEffect, useState } from 'react';
import { bleSensors } from '@/lib/appmintNative';

type BleDetail = {
  kind: 'device' | 'state' | 'data' | 'error';
  address?: string; name?: string | null; sensors?: string[]; rssi?: number;
  state?: 'scanning' | 'scan_stopped' | 'connecting' | 'connected' | 'ready' | 'disconnected';
  type?: 'heart_rate' | 'speed_cadence' | 'power' | 'running' | 'battery';
  data?: { bpm?: number; speedKmh?: number; cadenceRpm?: number; watts?: number; cadenceSpm?: number; percent?: number };
  error?: string;
};

export function SensorPanel() {
  const [devices, setDevices] = useState<BleDetail[]>([]);
  const [bpm, setBpm] = useState<number | null>(null);
  const [status, setStatus] = useState('');

  useEffect(() => {
    const onBle = (e: Event) => {
      const d = (e as CustomEvent<BleDetail>).detail;
      if (d.kind === 'device') setDevices((list) => [...list, d]);
      else if (d.kind === 'state') setStatus(d.state ?? '');
      else if (d.kind === 'data' && d.type === 'heart_rate') setBpm(d.data?.bpm ?? null);
      else if (d.kind === 'error') {
        setStatus(d.error === 'not_enabled' ? 'Sensors work in the installed app.' : `Bluetooth: ${d.error}`);
      }
    };
    window.addEventListener('appmint:ble', onBle);
    return () => { window.removeEventListener('appmint:ble', onBle); void bleSensors.disconnect(); };
  }, []);

  return (
    <div>
      <button onClick={() => { setDevices([]); bleSensors.setWheelCircumference(2105); void bleSensors.startScan(); }}>
        Find sensors
      </button>
      {devices.map((d) => (
        <button key={d.address} onClick={() => void bleSensors.connect(d.address!)}>
          {d.name ?? 'Sensor'} ({d.sensors?.join(', ')})
        </button>
      ))}
      <p>{status}</p>
      {bpm !== null && <p>{bpm} bpm</p>}
    </div>
  );
}
```

**Notes:** Scans last 15 seconds (`stopScan()` ends one early); one sensor is connected at a time. Error codes: `not_enabled`, `permission_denied`, `bluetooth_unavailable`, `bluetooth_off`, `scan_failed`, `invalid_address`. Speed needs two packets and the wheel size (default 2096 mm). The same messages also go to `window.onAppMintBle`; listening to the event is simpler. For any other Bluetooth device use `navigator.bluetooth`.

### `appmintNative.brightness`

```js
brightness.get() / brightness.set(value) / brightness.restore()
```

**Example**

Turns the screen up for a boarding pass or QR code, then gives control back to the system. Native only - a web page cannot drive the backlight.

**Returns:** `get()` → `Promise<number | null>` (0-1 while the app overrides the level; `-1` when the app is following the system level; `null` on the web or when it cannot be read). `set(value)` → `Promise<boolean>` (value clamped to 0-1; `false` on the web). `restore()` → `Promise<void>`. **Needs:** nothing.

```ts
import { brightness, can } from '@/lib/appmintNative';
import { useEffect } from 'react';

function TicketScreen() {
  useEffect(() => {
    if (can('brightness') === 'none') return;
    void brightness.set(1);
    return () => { void brightness.restore(); };
  }, []);
  return <QrCode />;
}
```

**Notes:** It changes only this app's window, not the phone's brightness setting, and the override ends when the app closes. `restore()` sets `-1` (follow the system). `get()` reads the window's override, so it is `-1` until you call `set()` - it is not the system level.

### `appmintNative.browser`

```js
browser.open(url, { toolbarColor? }) / browser.close() / browser.onClosed(cb)
```

**Example**

Opens a page in an in-app browser tab (Chrome Custom Tabs) - for terms, help pages or a sign-in page - and a new tab on the web.

**Returns:** `open()` → `Promise<boolean>` (`false` when the tab could not open). `close()` → `Promise<void>`. `onClosed(cb)` → an unsubscribe function; `cb` runs when the user closes the tab (in the app only). **Needs:** nothing.

```ts
import { browser } from '@/lib/appmintNative';

async function openHelp() {
  const off = browser.onClosed(() => {
    off();
    void refreshAccount();       // the user may have changed something on the site
  });
  const ok = await browser.open('https://example.com/help', { toolbarColor: '#0B0F1A' });
  if (!ok) { off(); showMessage('Could not open the help page.'); }
}
```

**Notes:** `toolbarColor` defaults to the app's `--bg` token. The tab is a separate browser - it does not share the app's cookies or `localStorage`. `close()` closes the tab from code in the app; on the web it does nothing. `onClosed` never fires on the web.

### `appmintNative.calendar`

```js
calendar.requestPermission / createEvent / listEvents / deleteEvent / openCalendar
```

**Example**

Adds, lists and deletes events in the user's OWN phone calendar (Android's calendar provider), and opens the calendar app at a date.

**Returns:** `requestPermission()` → `Promise<'granted' | 'denied'>`. `createEvent(input)` → `Promise<string | null>` (the event id; `null` when refused or failed). `listEvents({ from, to })` → `Promise<{ id, title, start, end, location?, notes?, allDay? }[]>` (times in epoch ms). `deleteEvent(id)` / `openCalendar(date?)` → `Promise<boolean>`. Nothing rejects. **Needs:** turn on **Calendar** in Step 4 (Access) when you build - the build ticks it for you when it sees `calendar.createEvent(`, `calendar.listEvents(` or `calendar.requestPermission(` in the code.

```ts
import { calendar } from '@/lib/appmintNative';

async function addBooking(b: { name: string; at: Date; minutes: number; place: string }) {
  if ((await calendar.requestPermission()) !== 'granted') {
    showMessage('Allow calendar access to add the booking.');
    return;
  }
  const id = await calendar.createEvent({
    title: `Booking: ${b.name}`,
    start: b.at,
    end: new Date(b.at.getTime() + b.minutes * 60_000),
    location: b.place,
    notes: 'Added by the app',
  });
  if (id) saveEventId(b, id);
}
```

**List this week, delete, open:**

```ts
import { calendar, can } from '@/lib/appmintNative';

if (can('calendarRead') !== 'none') {
  const week = await calendar.listEvents({ from: Date.now(), to: Date.now() + 7 * 86_400_000 });
  if (week[0]) await calendar.deleteEvent(week[0].id);
}
await calendar.openCalendar(new Date());
```

**Notes:** On the web `createEvent` downloads an `.ics` file the user opens in their calendar (returns its UID), `listEvents` returns `[]` (`can('calendarRead')` is `'none'`), and `deleteEvent` / `openCalendar` return `false`. Reminders and repeating events are available on `Capacitor.Plugins.Calendar`, not through this wrapper.

### `appmintNative.camera`

```js
camera.take(opts?) / camera.pick(opts?)
```

**Example**

Takes a photo with the camera or picks photos from the gallery: the Capacitor Camera plugin in the app, a file input with `capture` on the web.

**Returns:** `take()` → `Promise<Photo | null>` with `{ dataUrl, mime }`; `null` on cancel or refused permission. `pick()` → `Promise<Photo[]>`, empty on cancel. **Needs:** **Camera** in Step 4 (Access) - the build turns it on when your code calls `camera.take`.

**Take a profile photo with the front camera.**

```ts
import { camera } from '@/lib/appmintNative';

export async function takeSelfie(img: HTMLImageElement): Promise<boolean> {
  const photo = await camera.take({ source: 'camera', direction: 'front', quality: 70 });
  if (!photo) return false;                 // cancelled or permission refused
  img.src = photo.dataUrl;                  // "data:image/jpeg;base64,..."
  return true;
}
```

**Choose one photo from the gallery with `take`, or several with `pick`.**

```ts
import { camera } from '@/lib/appmintNative';

const one = await camera.take({ source: 'photos' });          // Photo | null
const many = await camera.pick({ limit: 5, quality: 80 });    // Photo[] (limit 0 = no limit)
console.log(one?.mime, many.length);
```

**Upload a photo** by turning the data URL into a Blob.

```ts
import { camera } from '@/lib/appmintNative';

const p = await camera.take();
if (p) {
  const blob = await (await fetch(p.dataUrl)).blob();
  const form = new FormData();
  form.append('file', blob, 'photo.jpg');
  await fetch('https://api.example.com/upload', { method: 'POST', body: form });
}
```

**Notes:** Options: `source` (`'camera'` default, or `'photos'`), `quality` 0-100 (default 80), `direction` (`'rear'` default, or `'front'`), `allowEditing`. Photos are not saved to the gallery. A refused permission resolves `null` / `[]`, never throws - show a short hint so the user knows why nothing happened. Camera is sensitive: declare it in your Play Data safety form.

### `appmintNative.can`

```js
can(cap: Capability): 'native' | 'web' | 'none'
```

**Example**

Tells you where a feature will really run on the current surface, so the app only draws controls that work.

**Returns:** a string, synchronously: `'native'` (the app's own implementation - the shell bridge or a Capacitor plugin baked into the app), `'web'` (a real web standard in this browser) or `'none'` (not available here - do not render the control). **Needs:** nothing.

Hide a control that cannot work on this surface:

```ts
import { can, share } from '@/lib/appmintNative';

export function ShareButton({ url }: { url: string }) {
  if (can('share') === 'none') return null;   // never ship a dead button
  return <button onClick={() => void share({ title: 'Have a look', url })}>Share</button>;
}
```

Change the wording by surface:

```ts
import { can } from '@/lib/appmintNative';

const contacts = can('contactList') !== 'none'; // 'none' on the web
const lockLabel = can('unlock') === 'native' ? 'Unlock with fingerprint' : 'Unlock with a passkey';
```

**Capability names:** `haptic` `share` `clipboardWrite` `clipboardRead` `speak` `fullscreen` `notify` `deviceInfo` · `filePick` `fileSave` `unlock` · `contactPick` `contactList` `contactAdd` · `smsCompose` `smsSend` `smsList` `callLog` · `toast` `dialog` `actionSheet` `keyboardInsets` `orientationLock` `network` `statusBar` · `swipe` `backGesture` · `folderAccess` `secureKeys` `mediaControls` `cast` `bluetooth` · `shortcuts` · `appLaunch` `browser` `camera` `photos` `location` `screenReader` `textZoom` `privacyScreen` `fileViewer` `fileTransfer` `calendarRead` `calendarWrite` `backgroundSync` `motion` · `keepAwake` `brightness` `review` `appUpdate` `badge` `speech` `hapticPatterns` `docScanner` `systemPalette` `liveProgress` `widget` `nfc` `pip` `shareTarget` `notificationActions` · `barcodeScan` `codeScanner` `passkeys`.

**Notes:**
- A Capacitor plugin baked into the app always answers `'native'`; otherwise the shell bridge is checked, then the web standard.
- `toast`, `actionSheet`, `dialog`, `statusBar` and `swipe` have a real web version, so they are not `'none'` in a browser.
- `contactList`, `contactAdd`, `smsSend`, `smsList`, `callLog`, `shortcuts`, `shareTarget` and `widget` are `'none'` on the web.
- Some features exist only when the build switched them on (folder access, secure keys, media controls, SMS, contacts). Ask `can()`, not `isNativeApp()`.
- An unknown name returns `'none'`. For a plain yes/no use `has(cap)`.

### `appmintNative.clearPushInbox`

```js
import { clearPushInbox } from '@/lib/appmintNative'
```

**Example**

Empties the AppMint Push inbox on this phone. Permanent - there is no undo.

**Returns:** `boolean` synchronously - `true` when cleared; always `false` on the web. **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { clearPushInbox, pushInbox } from '@/lib/appmintNative';

export async function onClearAll(confirmDelete: () => Promise<boolean>) {
  if (!(await confirmDelete())) return pushInbox();
  clearPushInbox();
  return pushInbox();   // []
}
```

**Notes:** tray notifications and your send history in AppMint are not touched; new messages still arrive.

### `appmintNative.clearShared`

```js
clearShared(): void
```

**Example**

Forgets the text or link another app shared into this one, once your app has saved it.

**Returns:** nothing. **Needs:** nothing (does nothing on the web).

```ts
import { sharedText, clearShared } from '@/lib/appmintNative';

export async function importPendingShare(addNote: (text: string) => Promise<void>): Promise<boolean> {
  const p = sharedText();
  if (!p) return false;
  await addNote(p.text);
  clearShared();          // otherwise sharedText() returns the same share on the next mount
  return true;
}
```

**Notes:** `sharedText()` keeps a share until this is called, on purpose, so a late-mounting screen still sees it. Call `clearShared()` only AFTER the share is safely saved - if saving fails, leave it so the user does not lose it.

### `appmintNative.click`

```js
click() — @/lib/appmintNative
```

**Example**

Plays the app's tap sound once, for your own buttons.

**Returns:** `void`. **Needs:** **Tap Sound** switched on in Step 3 (Integrate) when you build. On the web, and in apps built without Tap Sound, it does nothing.

```ts
import { click, haptic } from '@/lib/appmintNative';

export function KeypadKey({ label, onPress }: { label: string; onPress: () => void }) {
  return (
    <button
      onClick={() => {
        click();     // sound
        haptic();    // feel
        onPress();
      }}
    >
      {label}
    </button>
  );
}
```

**Notes:** With **Follow phone setting (recommended)** the click is silent when the user turned off "Touch sounds" on the phone. With **Always play on every tap**, every tap already clicks, so extra `click()` calls are not needed. Android only (the shell's `WebToApk.playClick`).

### `appmintNative.Clipboard`

```js
Clipboard.write({ string?, url? }) / Clipboard.read(): Promise<{ value, type }>
```

**Example**

The clipboard in the shape of Capacitor's Clipboard plugin - the same code as `copy()` / `paste()`.

**Returns:** `write()` → `Promise<void>`. `read()` → `Promise<{ value: string; type: 'text/plain' }>` (`value` is `''` when the clipboard is empty). **Needs:** nothing.

```ts
import { Clipboard, can } from '@/lib/appmintNative';

async function copyInvite(code: string) {
  await Clipboard.write({ string: code });
}

async function pasteInvite(): Promise<string> {
  if (can('clipboardRead') === 'none') return '';   // hide the Paste button instead
  try {
    const { value } = await Clipboard.read();
    return value;
  } catch {
    return '';   // the browser denied clipboard access
  }
}
```

**Notes:** In the app it uses the Capacitor Clipboard plugin; in a browser `navigator.clipboard`. `write` sends only `string` (or `url` when `string` is empty) - no images. In a browser, `read()` can throw when the user denies access; in the app an empty clipboard is `''`, not an error. Ask `can('clipboardRead')` before you show a Paste control.

### `appmintNative.collapsingHeader`

```js
collapsingHeader(header: HTMLElement, opts?: { scroller?: HTMLElement | Window; range?: number }): () => void
```

**Example**

Makes a large-title header react to scrolling the way native ones do, by publishing a 0 → 1 value your CSS can use.

**Returns:** an unsubscribe function that also removes the variable and the attribute. **Needs:** nothing.

It sets `--collapse` (from `0` at the top to `1` after `range` px of scroll) on the header, and adds `data-collapsed` once fully collapsed:

```ts
import { useEffect, useRef } from 'react';
import { collapsingHeader } from '@/lib/appmintNative';

export function NotesScreen() {
  const headerRef = useRef<HTMLElement>(null);
  const listRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!headerRef.current || !listRef.current) return;
    return collapsingHeader(headerRef.current, { scroller: listRef.current, range: 80 });
  }, []);

  return (
    <div className="flex h-full flex-col">
      <header ref={headerRef} className="app-header">Notes</header>
      <div ref={listRef} className="flex-1 overflow-y-auto">{/* rows */}</div>
    </div>
  );
}
```

```css
.app-header { font-size: calc(28px - 10px * var(--collapse, 0)); padding-top: var(--safe-top); }
.app-header[data-collapsed] { box-shadow: 0 1px 0 var(--border); }
```

**Notes:**
- Default `scroller` is the window (a page that scrolls as a whole); default `range` is 64 px.
- The scroll listener is passive and updates once per animation frame, so it does not slow scrolling.

### `appmintNative.composeSms`

```js
composeSms(phoneNumber: string, message: string): boolean
```

**Example**

Opens the messaging app with the number and text filled in; the user presses send. Works in the Android app (shell bridge) and on the web (an `sms:` link).

**Returns:** `boolean` right away - `true` when the messaging app was asked to open. **Needs:** no permission.

**A "Text the shop" button.**

```ts
import { composeSms, can } from '@/lib/appmintNative';

export function setUpTextButton(button: HTMLButtonElement) {
  if (can('smsCompose') === 'none') { button.hidden = true; return; }
  button.addEventListener('click', () => {
    const opened = composeSms('+911234567890', 'Hi, is my order ready?');
    if (!opened) alert('Could not open the messaging app.');
  });
}
```

**Notes:** Prefer this over `sendSms()` for anything the user writes or approves - nothing leaves the phone without the user pressing send. On the web it navigates to `sms:…?body=…`. A `composeSms(` call does not switch on **SMS (read & send)** when you build, so it adds no Play-restricted permission.

### `appmintNative.copy`

```js
copy(text: string): Promise<boolean>
```

**Example**

Puts text on the system clipboard.

**Returns:** `Promise<boolean>` - `true` when the text was copied, `false` when the clipboard is not available or the browser refused. **Needs:** nothing.

```ts
import { can, copy, haptic, toast } from '@/lib/appmintNative';

export function CopyLink({ link }: { link: string }) {
  if (can('clipboardWrite') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (await copy(link)) { haptic('success'); toast('Link copied'); }
        else toast('Could not copy the link');
      }}
    >
      Copy link
    </button>
  );
}
```

The Capacitor-shaped form does the same thing:

```ts
import { Clipboard } from '@/lib/appmintNative';

await Clipboard.write({ string: 'ABC-123' });
```

**Notes:** In the app the Capacitor Clipboard plugin writes the text (a plugin error rejects the promise instead of returning `false`). In a browser it uses `navigator.clipboard.writeText`, which needs an https page and usually a user tap.

### `appmintNative.copyImage`

```js
import { copyImage } from '@/lib/appmintNative'
```

**Example**

Copies an image to the clipboard so it pastes into chat and mail apps: the app's clipboard in the Android app, the Async Clipboard API on the web.

**Returns:** `Promise<boolean>` - `true` once copied. **Needs:** nothing; gate the control on `can('clipboardImage')`.

```ts
import { can, copyImage, toast } from '@/lib/appmintNative';

export async function copyQr(canvas: HTMLCanvasElement) {
  if (can('clipboardImage') === 'none') return;
  const png = await new Promise<Blob | null>((r) => canvas.toBlob(r, 'image/png'));
  if (png && (await copyImage(png))) toast('QR code copied');
  else toast('Could not copy the image');
}
```

**Notes:** PNG is the safest type everywhere; the Android app also accepts JPEG and WebP.

### `appmintNative.Device`

```js
Device.getInfo() / Device.getId() / Device.getLanguageCode()
```

**Example**

The `@capacitor/device` shape: phone model, OS version, a stable ID and the language. It uses the Capacitor Device plugin in the app and the browser's answer on the web.

**Returns:** `getInfo()` → `Promise<DeviceInfo & { operatingSystem, isVirtual }>` (with Capacitor also `manufacturer`, `webViewVersion`, …); `getId()` → `Promise<{ identifier }>`; `getLanguageCode()` → `Promise<{ value }>` (two letters, e.g. `"en"`). **Needs:** nothing.

**Show the model and OS version on an About screen.**

```ts
import { Device } from '@/lib/appmintNative';

export async function aboutText(): Promise<string> {
  const info = await Device.getInfo();
  if (info.platform === 'web') return `Browser (${info.language})`;
  return `${info.model ?? 'Unknown model'} · ${info.operatingSystem} ${info.osVersion ?? ''}` +
    (info.isVirtual ? ' · emulator' : '');
}
```

**A stable ID and the language code.**

```ts
import { Device } from '@/lib/appmintNative';

const { identifier } = await Device.getId();       // Capacitor ID in the app, installId() on the web
const { value: lang } = await Device.getLanguageCode();
await fetch('https://api.example.com/hello', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ device: identifier, lang }),
});
```

**Notes:** In the app the sync `deviceInfo()` fills `model` and `osVersion` too; `Device.getInfo()` adds `operatingSystem`, `isVirtual` and the plugin's extra fields. On the web `operatingSystem` is `'unknown'` and `model` is empty. `getId()` on the web is the `localStorage` ID from `installId()` and throws when storage is blocked.

### `appmintNative.deviceInfo`

```js
deviceInfo(): DeviceInfo
```

**Example**

Tells you synchronously where the app runs (`platform`) and the language, plus the Android shell's full device report inside the Android app.

**Returns:** `DeviceInfo` right away: always `{ platform, language }` - `platform` is `'android'` in the app and `'web'` in a browser. In the Android app it also fills `model`, `manufacturer`, `osVersion` (e.g. `"14"`) and `appVersion` (e.g. `"1.4.0"`), and carries every key of `WebToApk.getDeviceInfo()`: `android`, `hardware`, `screen`, `app`, `runtime`, `installId`. On the web it adds `osVersion` (the user agent). **Needs:** nothing.

**Branch on the platform during render.**

```ts
import { deviceInfo } from '@/lib/appmintNative';

const info = deviceInfo();
const isAndroidApp = info.platform === 'android';
const lang = info.language;                         // e.g. "en-IN"
```

**Read Android details.** The common fields are at the top; the rest of the shell's report stays in its nested groups.

```ts
import { deviceInfo } from '@/lib/appmintNative';

const info: any = deviceInfo();
if (info.platform === 'android') {
  console.log(`${info.manufacturer} ${info.model}`);        // "OPPO CPH2269"
  console.log(`Android ${info.osVersion}, app ${info.appVersion}`);
  console.log(`Battery ${info.runtime?.batteryLevel}%`);
}
```

**Notes:** `model` and `appVersion` are filled in the app only. On the web `osVersion` is the browser's user-agent string, not an OS version. It throws if the shell's answer is not valid JSON (a real defect, never expected).

### `appmintNative.Dialog`

```js
Dialog.alert / Dialog.confirm / Dialog.prompt
```

**Example**

Real system dialogs: a message, a yes/no question, a one-line text question. The app uses the Capacitor Dialog plugin; the web uses the browser's own `alert` / `confirm` / `prompt`.

**Returns:** `alert({ title?, message })` → `Promise<void>`. `confirm({ title?, message, okButtonTitle?, cancelButtonTitle? })` → `Promise<{ value: boolean }>`. `prompt({ title?, message, inputPlaceholder?, inputText? })` → `Promise<{ value: string; cancelled: boolean }>`. None of them reject. **Needs:** nothing.

```ts
import { Dialog } from '@/lib/appmintNative';

async function deleteList(id: string) {
  const { value } = await Dialog.confirm({
    title: 'Delete list?',
    message: 'All items in it will be removed.',
    okButtonTitle: 'Delete',
    cancelButtonTitle: 'Keep',
  });
  if (value) await removeList(id);
}
```

**Alert and prompt:**

```ts
import { Dialog } from '@/lib/appmintNative';

await Dialog.alert({ title: 'Offline', message: 'Your changes will sync when you are back online.' });

const r = await Dialog.prompt({ title: 'Rename', message: 'New name', inputText: current, inputPlaceholder: 'List name' });
if (!r.cancelled && r.value.trim()) rename(r.value.trim());
```

**Notes:** Dismissing a confirm gives `{ value: false }`; dismissing a prompt gives `{ value: '', cancelled: true }`. Dialogs block the screen - use them for real decisions, and `toast()` for "Saved"-style messages. `prompt` does not pass custom button titles to the plugin.

### `appmintNative.dismissPushMessage`

```js
import { dismissPushMessage } from '@/lib/appmintNative'
```

**Example**

Hides one message from the AppMint Push inbox (swipe-away). Undo it with `restorePushMessage(id)`.

**Returns:** `boolean` synchronously - `true` if it was hidden. Always `false` on the web. **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { dismissPushMessage, restorePushMessage, pushInbox } from '@/lib/appmintNative';

export function swipeAway(id: string, setItems: (v: ReturnType<typeof pushInbox>) => void,
                          showUndo: (onUndo: () => void) => void) {
  if (!dismissPushMessage(id)) return;
  setItems(pushInbox());
  showUndo(() => {                         // e.g. a 5-second "Undo" snackbar
    restorePushMessage(id);
    setItems(pushInbox());
  });
}
```

**Notes:** only this phone's inbox changes; the tray notification and your send history stay.

### `appmintNative.docScanner`

```js
docScanner.scan({ pageLimit?, pdf?, quality? }): Promise<{ pages, pdfPath? } | null> / docScanner.isSupported(): Promise<boolean>
```

**Example**

Opens the system document scanner - edge detection, straightening, several pages - Google's ML Kit scanner in the Android app. Pages come back as JPEG data URLs.

**Returns:** `Promise<{ pages: string[]; pdfPath?: string } | null>` - `null` when the user cancels, the scanner cannot start, or on the web. `isSupported()` → `Promise<boolean>`: whether this phone can scan (Google Play services); `false` on the web. **Needs:** nothing to switch on (on Android the scanner runs in Google Play services and needs no camera permission).

```ts
import { docScanner, can } from '@/lib/appmintNative';

export function ScanButton({ onPages }: { onPages: (p: string[]) => void }) {
  if (can('docScanner') === 'none') return null;     // no web scanner: hide the button
  return (
    <button onClick={async () => {
      const res = await docScanner.scan({ pageLimit: 3, quality: 0.8 });
      if (res) onPages(res.pages);                   // each a data:image/jpeg;base64,… URL
    }}>
      Scan document
    </button>
  );
}
```

**Notes:** `pageLimit` `0` (default) = no limit; `quality` 0.1-1 (default 0.85). With `pdf: true`, `pdfPath` is a file path in the app's cache, not a URL the page can load. Data URLs are large - resize or upload them rather than keeping many in `localStorage`. For a plain photo use `camera.take()`.

### `appmintNative.fileTransfer`

```js
fileTransfer.download / fileTransfer.upload from '@/lib/appmintNative'
```

**Example**

Downloads or uploads a file with a progress callback. In the app the transfer runs natively (Capacitor FileTransfer); on the web it uses `fetch` / `XMLHttpRequest` with the same callback.

**Returns:** `download(url, fileName, opts?)` → `Promise<{ path?: string; blob?: Blob }>` - `path` (a file in the app's cache) in the app, `blob` on the web (where the file is also saved through the browser). `upload(url, file, opts?)` → `Promise<{ responseCode: number; responseBody: string }>`. Both reject on failure. **Needs:** nothing to switch on.

Download with a progress bar:

```ts
import { fileTransfer, fileViewer } from '@/lib/appmintNative';

async function getReport(bar: HTMLProgressElement) {
  try {
    const r = await fileTransfer.download('https://example.com/report.pdf', 'report.pdf', {
      headers: { Authorization: `Bearer ${token}` },
      onProgress: (p) => { bar.value = p.fraction; },     // {bytes, total, fraction 0..1}
    });
    if (r.path) await fileViewer.open(r.path);
  } catch (e) {
    alert(`Download failed: ${String(e)}`);
  }
}
```

Upload a `File` from an `<input type="file">`:

```ts
import { fileTransfer } from '@/lib/appmintNative';

async function sendPhoto(file: File) {
  const r = await fileTransfer.upload('https://example.com/api/photos', file, {
    fieldName: 'photo',                // form field name, default 'file'
    headers: { Authorization: `Bearer ${token}` },
    onProgress: (p) => console.log(Math.round(p.fraction * 100) + '%'),
  });
  if (r.responseCode >= 200 && r.responseCode < 300) console.log('uploaded', r.responseBody);
  else alert(`Upload failed (${r.responseCode})`);
}
```

**Notes:** `fraction` is 0 while the server sends no length. In the app a download lands in the app's CACHE folder (not the user's Downloads); to give the user a copy, use `saveFile`. For an upload the app first copies the file into its cache, so very large files need that much free space. `method` (default `'POST'`) and `fileName` are optional upload options.

### `appmintNative.fileViewer`

```js
fileViewer.open(target) from '@/lib/appmintNative'
```

**Example**

Opens a document (PDF, image, office file) in the phone's viewer app. On the web it opens the address in a new tab.

**Returns:** `Promise<boolean>` - `true` when the viewer was opened, `false` when it failed (no app for that type, bad path) or the target cannot be opened on this surface. **Needs:** nothing to switch on; it uses the Capacitor FileViewer plugin that ships in the app.

Open a document from the web:

```ts
import { fileViewer } from '@/lib/appmintNative';

async function openManual() {
  const ok = await fileViewer.open('https://example.com/manual.pdf');
  if (!ok) alert('No app on this phone can open this file.');
}
```

Open a file you downloaded first with `fileTransfer.download` (a local path in the app):

```ts
import { fileTransfer, fileViewer } from '@/lib/appmintNative';

const r = await fileTransfer.download('https://example.com/invoice.pdf', 'invoice.pdf');
if (r.path) await fileViewer.open(r.path);            // in the app
else if (r.blob) console.log('saved by the browser'); // on the web
```

**Notes:** An `http(s)` target uses `openDocumentFromUrl`; anything else is treated as a local path (`file://` is stripped). On the web only `http(s):`, `blob:` and `data:` targets open; a local path answers `false`.

### `appmintNative.folder`

```js
import { folder } from '@/lib/appmintNative'
```

**Example**

A whole folder the user grants once and the app keeps across launches, with real file operations inside it (in the app: the shell's folder bridge).

**Returns:** every member is async - `connected()` → `boolean`, `label()` → `string`, `request()` → `boolean` (false on cancel), `disconnect()` → `void`, `list(path)` → `[{ name, uri, isDirectory, length, lastModified }]`, `readText(path)` → `string | null` (null = no such file), `writeText(path, text)` / `mkdir(path)` / `rename(path, newName)` / `remove(path, recursive)` → `boolean`, `stat(path)` → `{ name, size, mime, lastModified, uri, isDirectory, canWrite } | null`. **Needs:** an AI-built app turns the **Native Folder Access (SAF)** switch on by itself when the code uses `folder`; on a hand-made build tick it in Step 4 (Access).

Connect, show the folder, and disconnect:

```ts
import { can, folder } from '@/lib/appmintNative';

export async function setupFolderPanel(el: { name: HTMLElement; connect: HTMLButtonElement; disconnect: HTMLButtonElement }) {
  if (can('folderAccess') === 'none') { el.connect.hidden = true; return; }   // web build, or not shipped

  const refresh = async () => {
    const on = await folder.connected();
    el.name.textContent = on ? `Saving to: ${await folder.label()}` : 'No folder chosen';
    el.connect.hidden = on;
    el.disconnect.hidden = !on;
  };

  el.connect.onclick = async () => { await folder.request(); await refresh(); };   // from a tap only
  el.disconnect.onclick = async () => { await folder.disconnect(); await refresh(); };
  await refresh();
}
```

Read, write, list, rename and delete - every path is relative to the granted folder (`''` is its root):

```ts
import { folder } from '@/lib/appmintNative';

await folder.mkdir('notes');
const saved = await folder.writeText('notes/today.md', '# Today\n');   // parents created
const text = await folder.readText('notes/today.md');                  // null if missing
const entries = await folder.list('notes');
entries.forEach((e) => console.log(e.isDirectory ? 'dir ' : 'file', e.name, e.length));

const info = await folder.stat('notes/today.md');
if (info) console.log(info.size, 'bytes, writable:', info.canWrite);

await folder.rename('notes/today.md', 'journal.md');                   // a new NAME, not a path
await folder.remove('notes', true);                                    // recursive for a non-empty folder
```

**Notes:** `can('folderAccess')` is `'none'` on the web - there is no persistent-folder equivalent in a phone browser, so do not render the feature there (use `pickFile()` / `saveFile()` for single files). Call `request()` only from a user tap. `..` and absolute paths are refused. The granted folder itself can never be removed. `stat()` works for files and sub-folders (a folder has `isDirectory: true` and `size: 0`); `stat('')` - the root - is `null`, use `label()` for it. `request()` can be called again after a cancel.

### `appmintNative.fullscreen`

```js
fullscreen.enter() / fullscreen.exit() / fullscreen.isOn()
```

**Example**

Hides the system bars (in the app) or enters browser fullscreen (on the web), for a video, a game or a reading view.

**Returns:** `enter()` and `exit()` return `Promise<void>` and never reject; `isOn()` returns `boolean` synchronously. **Needs:** nothing.

```ts
import { useState } from 'react';
import { can, fullscreen } from '@/lib/appmintNative';

export function FullscreenToggle() {
  const [on, setOn] = useState(fullscreen.isOn());
  if (can('fullscreen') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (fullscreen.isOn()) await fullscreen.exit(); else await fullscreen.enter();
        setOn(fullscreen.isOn());
      }}
    >
      {on ? 'Exit full screen' : 'Full screen'}
    </button>
  );
}
```

Leave fullscreen when the view closes:

```ts
import { useEffect } from 'react';
import { fullscreen } from '@/lib/appmintNative';

export function useFullscreenWhileOpen() {
  useEffect(() => {
    void fullscreen.enter();
    return () => { void fullscreen.exit(); };
  }, []);
}
```

**Notes:**
- In the app this is the shell's own fullscreen (status and navigation bars hide); it is always available to the page.
- On the web the Fullscreen API works only from a user tap. A refused request is ignored quietly, so read `isOn()` afterwards instead of assuming.

### `appmintNative.Geolocation`

```js
Geolocation.getCurrentPosition / watchPosition / clearWatch / checkPermissions / requestPermissions
```

**Example**

The `@capacitor/geolocation` shape: the Capacitor Geolocation plugin in the app, the standard `navigator.geolocation` on the web. All return the W3C position object.

**Returns:** `getCurrentPosition(opts)` → `Promise<GeolocationPosition>` (rejects on refusal, timeout or no location); `watchPosition(opts, cb)` → `Promise<string>` (the watch id); `clearWatch({ id })` → `Promise<void>`; `checkPermissions()` / `requestPermissions()` → `Promise<{ location: 'granted' | 'denied' | 'prompt' | … }>`. **Needs:** **GPS** / **Location** in Step 4 (Access) - the build turns both on when your code calls `getCurrentPosition` or `watchPosition`.

**Where am I, once.**

```ts
import { Geolocation } from '@/lib/appmintNative';

export async function currentSpot(): Promise<string> {
  try {
    const perm = await Geolocation.checkPermissions();
    if (perm.location !== 'granted') {
      const asked = await Geolocation.requestPermissions();
      if (asked.location !== 'granted') return 'Location is off for this app.';
    }
    const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true, timeout: 15000 });
    return `${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`;
  } catch {
    return 'Could not get your location. Is location turned on?';
  }
}
```

**Track a run, then stop.** The callback gets `null` and an error when a fix fails.

```ts
import { Geolocation } from '@/lib/appmintNative';

let watchId: string | null = null;

export async function startRun(onPoint: (lat: number, lng: number) => void) {
  watchId = await Geolocation.watchPosition({ enableHighAccuracy: true }, (pos, err) => {
    if (!pos) { console.warn('location error', err); return; }
    onPoint(pos.coords.latitude, pos.coords.longitude);
  });
}

export async function stopRun() {
  if (watchId) { await Geolocation.clearWatch({ id: watchId }); watchId = null; }
}
```

**Notes:** Plain `navigator.geolocation` also works in the app; use this when you want the Capacitor shape. On Android 12+ the user may allow only approximate location - you still get a position, just less accurate. On the web `requestPermissions()` makes one real position request to trigger the browser prompt. Location is personal data: declare it in your Play Data safety form.

### `appmintNative.haptic`

```js
haptic(kind?) — @/lib/appmintNative
```

**Example**

A short, native-feeling vibration for taps and results. Call it on every meaningful tap.

**Returns:** `void` (fire and forget). `kind`: `'light'` (default), `'medium'`, `'heavy'`, `'selection'`, `'success'`, `'warning'`, `'error'`. **Needs:** **Vibrate** in Step 4 (Access); AI builds switch it on for you when the code calls `haptic`.

```ts
import { haptic } from '@/lib/appmintNative';

export function SaveButton({ onSave }: { onSave: () => Promise<boolean> }) {
  return (
    <button
      onClick={async () => {
        haptic();                          // light tap on press
        const ok = await onSave();
        haptic(ok ? 'success' : 'error');  // result feedback
      }}
    >
      Save
    </button>
  );
}
```

For tabs, toggles and pickers use `'selection'`; for a delete confirm use `'heavy'`:

```ts
import { haptic } from '@/lib/appmintNative';

function onTabChange(tab: string) { haptic('selection'); setTab(tab); }
function onConfirmDelete() { haptic('heavy'); deleteItem(); }
```

**Notes:** In the app this uses the Capacitor Haptics plugin (`impact`, `notification`, `selectionChanged`). On the web it uses `navigator.vibrate` where the browser has it, and does nothing elsewhere; it never throws. For longer rhythms use `hapticPlay`.

### `appmintNative.hapticPlay`

```js
hapticPlay(pattern) — @/lib/appmintNative
```

**Example**

Plays a rich vibration pattern for a special moment (a win, a streak, a heartbeat). Use `haptic()` for normal taps.

**Returns:** `Promise<boolean>`: `true` when the pattern played, `false` when it could not. **Needs:** **Vibrate** in Step 4 (Access).

Named patterns: `'success-ramp'`, `'heartbeat'`, `'tick-tick-thud'`, `'drum-roll'`, `'nudge'`.

```ts
import { hapticPlay } from '@/lib/appmintNative';

async function onLevelComplete() {
  const felt = await hapticPlay('success-ramp');
  if (!felt) console.log('No vibration on this device');
  showConfetti();
}
```

Your own pattern: a list of `{ at, duration?, intensity?, sharpness? }` (ms, ms, 0-1, 0-1):

```ts
import { hapticPlay } from '@/lib/appmintNative';

await hapticPlay([
  { at: 0,   intensity: 0.4 },               // soft tap
  { at: 150, intensity: 0.4 },               // soft tap
  { at: 320, duration: 120, intensity: 1 },  // strong buzz
]);
```

**Notes:** The app uses the `AppwrightHaptics` Capacitor plugin (strength per beat where the phone supports it). The web gets the timing only. `sharpness` is accepted but has no effect on Android. `haptic.play(pattern)` is the same function.

### `appmintNative.Haptics`

```js
Haptics — @/lib/appmintNative
```

**Example**

The Capacitor-style `Haptics` object: the same names and options as `@capacitor/haptics`, running on the same code as `haptic()`.

**Returns:** every method returns `void` (not a Promise; `await` on it is harmless). **Needs:** **Vibrate** in Step 4 (Access); AI builds switch it on for you when the code uses `Haptics`.

```ts
import { Haptics } from '@/lib/appmintNative';

Haptics.impact({ style: 'LIGHT' });        // 'LIGHT' | 'MEDIUM' | 'HEAVY'  (default 'LIGHT')
Haptics.notification({ type: 'SUCCESS' }); // 'SUCCESS' | 'WARNING' | 'ERROR' (default 'SUCCESS')
Haptics.vibrate({ duration: 300 });        // ms, default 300
```

A picker that ticks as the value changes:

```ts
import { Haptics } from '@/lib/appmintNative';

export function Stepper({ value, onChange }: { value: number; onChange: (v: number) => void }) {
  const step = (d: number) => { Haptics.selectionChanged(); onChange(value + d); };
  return (
    <div onPointerDown={() => Haptics.selectionStart()} onPointerUp={() => Haptics.selectionEnd()}>
      <button aria-label="Less" onClick={() => step(-1)}>-</button>
      <span>{value}</span>
      <button aria-label="More" onClick={() => step(1)}>+</button>
    </div>
  );
}
```

**Notes:** `selectionStart` and `selectionChanged` both give one selection tick; `selectionEnd` does nothing. Works in the app (Capacitor Haptics plugin); on the web it uses `navigator.vibrate` where available and otherwise does nothing.

### `appmintNative.has`

```js
has(cap: Capability): boolean
```

**Example**

The yes/no form of `can()`: true when the feature will do something real on this surface.

**Returns:** `boolean`, synchronously - exactly `can(cap) !== 'none'`. **Needs:** nothing.

```ts
import { has, copy, toast } from '@/lib/appmintNative';

export function CopyCode({ code }: { code: string }) {
  if (!has('clipboardWrite')) return <code>{code}</code>;   // no button where it cannot copy
  return (
    <button onClick={async () => { if (await copy(code)) toast('Copied'); }}>
      Copy {code}
    </button>
  );
}
```

Gate a whole feature:

```ts
import { has, scanCode } from '@/lib/appmintNative';

export async function onScanTap(): Promise<string | null> {
  if (!has('codeScanner')) return null;     // better: do not render the Scan button at all
  const hit = await scanCode({ formats: ['qr_code'] });
  return hit ? hit.value : null;
}
```

**Notes:** It takes the same capability names as `can()` (see that entry for the full list). Use `can()` when you need to know WHERE it runs (`'native'` or `'web'`), for example to change a label.

### `appmintNative.hideKeyboard`

```js
hideKeyboard(): void
```

**Example**

Closes the on-screen keyboard - for example when a form is submitted or a sheet opens over the field.

**Returns:** nothing. **Needs:** nothing.

```ts
import { hideKeyboard, toast } from '@/lib/appmintNative';

export function SearchForm({ onSearch }: { onSearch: (q: string) => void }) {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const q = new FormData(e.currentTarget).get('q') as string;
        hideKeyboard();                       // show the results, not the keyboard
        onSearch(q);
        if (!q) toast('Type something to search');
      }}
    >
      <input name="q" type="search" enterKeyHint="search" aria-label="Search" />
    </form>
  );
}
```

**Notes:** In the app it asks the Capacitor Keyboard plugin to hide the keyboard. On the web it blurs the focused field, which is what closes the keyboard in a browser. It is safe to call when no keyboard is open.

### `appmintNative.installId`

```js
installId(): string
```

**Example**

Gives a stable random ID for this install: the shell's own ID in the Android app, a random ID saved in `localStorage` on the web.

**Returns:** `string` right away. **Needs:** nothing.

**Key a free trial to the device.**

```ts
import { installId } from '@/lib/appmintNative';

export async function claimTrial(): Promise<boolean> {
  let id: string;
  try {
    id = installId();
  } catch {
    return false;          // web with storage blocked (private mode): no stable id exists
  }
  const r = await fetch('https://api.example.com/trial', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ device: id }),
  });
  return r.ok;
}
```

**Notes:** It stays the same across app updates and changes on uninstall or "clear data". It is not a hardware ID. It throws on the web when `localStorage` is blocked, on purpose - a shared blank ID would mix users up.

### `appmintNative.isNativeApp`

```js
isNativeApp(): boolean
```

**Example**

Tells you whether the code is running inside the installed Android app (the shell bridge `window.WebToApk` is present).

**Returns:** `boolean` right away. `false` in a browser. **Needs:** nothing.

**Show an "Android app" badge, and a download link in the browser.**

```ts
import { isNativeApp } from '@/lib/appmintNative';

export function appBadgeText(): string {
  if (isNativeApp()) return 'Android app';
  return 'Get the app on Google Play';
}
```

**Notes:** `platform()` gives the same answer as a string (`'android'` in the app, `'web'` in a browser). To decide whether a feature can be shown, use `can('<capability>')` instead: it knows which platform has which feature.

### `appmintNative.keepAwake`

```js
keepAwake(on: boolean): Promise<boolean>
```

**Example**

Keeps the screen on while a recipe, a route, a QR code or a timer is showing, and lets it sleep again.

**Returns:** `Promise<boolean>` - `true` when the request took effect, `false` when nothing could keep the screen on (or the browser refused). **Needs:** nothing.

```ts
import { keepAwake } from '@/lib/appmintNative';
import { useEffect } from 'react';

function CookingMode() {
  useEffect(() => {
    void keepAwake(true);
    return () => { void keepAwake(false); };   // always release on unmount
  }, []);
  return <Steps />;
}
```

**Notes:** In the app it uses the KeepAwake plugin (the window's keep-screen-on flag); on the web the Screen Wake Lock API, which browsers release by themselves when the tab is hidden - call `keepAwake(true)` again when the page becomes visible if the screen must stay on. It only works while the app is in front.

### `appmintNative.Keyboard`

```js
Keyboard.hide() / Keyboard.addListener(event, handler): { remove() }
```

**Example**

The software keyboard in the shape of Capacitor's Keyboard plugin: close it, and hear when the space it covers changes.

**Returns:** `hide()` → `Promise<void>`. `addListener(event, handler)` → `{ remove(): void }` (returned at once, not a Promise). `event` is `'keyboardWillShow'`, `'keyboardDidShow'`, `'keyboardWillHide'` or `'keyboardDidHide'`; the handler gets `{ keyboardHeight }`. **Needs:** nothing.

**Close the keyboard** when a form is sent or a sheet opens over the field:

```ts
import { Keyboard } from '@/lib/appmintNative';

async function onSubmit(e: React.FormEvent) {
  e.preventDefault();
  await Keyboard.hide();
  await save();
}
```

**Listen** (remove the handle on unmount):

```ts
import { Keyboard } from '@/lib/appmintNative';
import { useEffect } from 'react';

useEffect(() => {
  const h = Keyboard.addListener('keyboardDidShow', ({ keyboardHeight }) => {
    composer.style.paddingBottom = `${keyboardHeight}px`;
  });
  const g = Keyboard.addListener('keyboardDidHide', () => { composer.style.paddingBottom = '0px'; });
  return () => { h.remove(); g.remove(); };
}, []);
```

**Notes:** `keyboardHeight` is the part of the keyboard the page still has to clear, not the raw height: in the Android app the shell resizes the page for the keyboard, so it is `0` there while the keyboard is open (the show event still fires, and fires again when the number changes). The hide events fire only when the keyboard really closes, so they can track "is the keyboard open". "Will" and "did" events fire together. For padding alone, `onKeyboard((px) => …)` is simpler.

### `appmintNative.launchUrl`

```js
launchUrl(): Promise<string>
```

**Example**

Gives the link that launched or resumed the app - a deep link, an icon shortcut, a widget tap, a notification - so the app can open the right screen.

**Returns:** `Promise<string>` - the full URL, or `''` when the app was opened normally. Always `''` on the web, where the address bar already is the route. **Needs:** nothing for shortcuts and widget taps; outside links need the **Deep Linking** option when you build.

Route on it once when the app mounts:

```ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { launchUrl } from '@/lib/appmintNative';

export function useLaunchRoute() {
  const navigate = useNavigate();
  useEffect(() => {
    void launchUrl().then((u) => {
      if (!u) return;
      const { pathname, search } = new URL(u);
      navigate(pathname + search, { replace: true });
    });
  }, [navigate]);
}
```

Pair it with `onDeepLink()` for links that arrive while the app is already open:

```ts
import { onDeepLink } from '@/lib/appmintNative';

const off = onDeepLink((url) => console.log('opened with', url));
```

**Notes:**
- The value is KEPT, not consumed: a component that mounts late still sees it. A newer link replaces it.
- It is read from the shell (`WebToApk.getLaunchUrl()`); never call that directly in an AI-built app. Shortcuts and widget taps arrive as `appmint-shortcut://<package><route>`.
- Parse with `new URL(u)` and navigate to its path; do not load the URL itself.

### `appmintNative.listCallLog`

```js
listCallLog(limit = 100, offset = 0, sinceMillis = 0): Promise<CallLogEntry[]>
```

**Example**

Reads the call history, newest first. Android app only.

**Returns:** `Promise<CallLogEntry[]>` - each `{ number, name, type, date, duration }`; `type` is `incoming`, `outgoing`, `missed`, `voicemail`, `rejected`, `blocked` or `other`, `date` is epoch milliseconds, `duration` is seconds. Empty when Call log is off or permission is refused. **Needs:** the **Call log (read)** switch in Step 4 (Access) - the build turns it on when your code calls this - and the user's permission on first use.

**Missed calls from the last 7 days.**

```ts
import { listCallLog, can } from '@/lib/appmintNative';

export async function missedThisWeek() {
  if (can('callLog') === 'none') return null;          // web: not possible
  const since = Date.now() - 7 * 24 * 60 * 60 * 1000;
  try {
    const calls = await listCallLog(500, 0, since);   // a number here; the wrapper converts it
    return calls.filter((c) => c.type === 'missed');
  } catch {
    return [];                                          // 2-minute timeout
  }
}
```

**Notes:** `limit` is clamped to 2000 by the app. An empty list cannot tell "no calls" from "permission refused". `can('callLog')` is `'none'` on the web. Call log is a Google Play restricted permission: you need an approved Permissions Declaration in Play Console, or the app is rejected.

### `appmintNative.listContacts`

```js
listContacts(limit = 200, offset = 0): Promise<Contact[]>
```

**Example**

Reads the address book, a page at a time. Android app only, through the shell's bridge.

**Returns:** `Promise<Contact[]>`. An empty array when there are no contacts, when access is refused, or when Contacts was not enabled at build time. **Needs:** the **Contacts** switch in Step 4 (Access) - the build turns it on when your code calls this - and the user's permission on first use.

**A contact list screen.** Render it only where it can work. Each contact is `{ id?, name?, phones?: string[], emails?: string[], photoUri? }`.

```ts
import { listContacts, can, type Contact } from '@/lib/appmintNative';

type Row = { id: string; name: string; phone: string };

function toRow(c: Contact): Row {
  return { id: c.id ?? '', name: c.name ?? '', phone: c.phones?.[0] ?? '' };
}

export async function loadPage(page: number): Promise<Row[] | null> {
  if (can('contactList') === 'none') return null;   // web: show "open the app" instead
  try {
    const contacts = await listContacts(100, page * 100);
    return contacts.map(toRow);
  } catch {
    return [];                                        // 2-minute timeout
  }
}
```

**Notes:** An empty result cannot tell "no contacts" from "permission refused" - show a short hint ("Allow contacts in Settings") when the list is empty. Contacts come sorted A to Z by name; `limit` is clamped to 2000. For a single person use `pickContact()`, which needs no permission. Contacts is personal data: declare it in your Play Data safety form and privacy policy.

### `appmintNative.listSms`

```js
listSms(box: 'inbox' | 'sent' | 'draft' = 'inbox', limit = 100, offset = 0): Promise<SmsMessage[]>
```

**Example**

Reads SMS messages from the inbox, the sent box or the drafts, newest first. Android app only.

**Returns:** `Promise<SmsMessage[]>` - each `{ id, address, body, date, read, box }`. Empty when SMS is off, permission is refused, or the box is empty. **Needs:** the **SMS (read & send)** switch in Step 4 (Access) - the build turns it on when your code calls this - and the user's permission on first use.

**Show the latest 20 messages from the inbox.**

```ts
import { listSms, can, type SmsMessage } from '@/lib/appmintNative';

export async function latestInbox(): Promise<SmsMessage[] | null> {
  if (can('smsList') === 'none') return null;          // web: hide the screen
  try {
    const msgs = await listSms('inbox', 20, 0);
    return msgs.map((m) => ({ ...m, body: (m.body ?? '').slice(0, 120) }));
  } catch {
    return [];                                          // 2-minute timeout
  }
}
```

**Notes:** An empty list cannot tell "no messages" from "permission refused". `limit` is clamped to 500 by the app. `can('smsList')` is `'none'` on the web. New incoming messages are not part of this wrapper; hand-written pages get them from the `appmint:sms-received` event (see `listSms` for the raw bridge). SMS is a Google Play restricted permission: you need an approved Permissions Declaration in Play Console, normally only granted to default SMS apps.

### `appmintNative.liveProgress`

```js
import { liveProgress } from '@/lib/appmintNative'
```

**Example**

A sticky, silent status card in the Android notification shade that you update in place - a delivery, an upload, a timer, a workout. `start`, `update`, `end`.

**Returns:** `start` / `update` → `Promise<boolean>` (`false` when not available, when notifications are not allowed, or when the user turned off its "Live status" category); `end` → `Promise<void>`. **Needs:** the Android app, and the notification permission on Android 13+. On the web `can('liveProgress')` is `'none'` - show the same state inside your app there.

```ts
import { liveProgress, can } from '@/lib/appmintNative';

export async function trackUpload(files: File[]) {
  const card = can('liveProgress') !== 'none';
  if (card) await liveProgress.start({ id: 'upload', title: 'Uploading photos', text: 'Starting…', indeterminate: true });

  for (let i = 0; i < files.length; i++) {
    await uploadOne(files[i]);
    if (card) await liveProgress.update({
      id: 'upload', title: 'Uploading photos',
      text: `${i + 1} of ${files.length}`,
      progress: (i + 1) / files.length,       // 0–1
      route: '/uploads',                      // opens this screen when tapped
    });
  }
  if (card) await liveProgress.end('upload'); // removes the card
}
```

**A running timer:** `startedAt` shows a clock counting up from that moment.

```ts
await liveProgress.start({ id: 'run', title: 'Morning run', text: '5 km goal', startedAt: Date.now() });
// ...later
await liveProgress.end('run');
```

**Notes:** the same `id` updates one card; different ids make separate cards (the default id is `'live'`). The user cannot swipe it away, so always call `end`. Ask for notification permission first on Android 13+ (`Notification.requestPermission()`): without it `start` answers `false` and nothing is shown.

### `appmintNative.LocalNotifications`

```js
import { LocalNotifications } from '@/lib/appmintNative'
```

**Example**

The `@capacitor/local-notifications` shape (`LocalNotifications.schedule({ notifications })`) for code written against the Capacitor plugin. It shows each notification immediately through `notify()`.

**Returns:** `Promise<{ notifications }>` - the same list you passed in. It does not tell you whether each one was shown; use `notify()` when you need that. **Needs:** Android: **Notifications** turned on in the Access step (Step 4) when you build.

```ts
import { LocalNotifications } from '@/lib/appmintNative';

export async function announceSync(count: number) {
  await LocalNotifications.schedule({
    notifications: [
      { title: 'Sync complete', body: `${count} items updated`, tag: 'sync' },
    ],
  });
}
```

**Notes:** immediate only - the Capacitor fields `id`, `schedule`, `at`, `actionTypeId` and `extra` are not supported; each item is `{ title, body?, icon?, tag? }`. For a notification at a later time, a hand-written page uses `WebToApk.scheduleNotificationEx` (Android). For buttons use `notifyWithActions`.

### `appmintNative.lockOrientation`

```js
lockOrientation(to: 'portrait' | 'landscape' | 'any'): Promise<boolean>
```

**Example**

Pins the screen one way up for a view that only makes sense that way - a game board, a wide chart, a signature pad. `'any'` releases the lock.

**Returns:** `Promise<boolean>` - `true` when the lock (or release) was applied, `false` when the platform refused. It never rejects. **Needs:** nothing.

Lock while the view is open, release on leave:

```ts
import { useEffect } from 'react';
import { can, lockOrientation } from '@/lib/appmintNative';

export function ChartScreen() {
  useEffect(() => {
    if (can('orientationLock') === 'none') return;
    void lockOrientation('landscape');
    return () => { void lockOrientation('any'); };   // always give the rotation back
  }, []);
  return <div className="h-full w-full">{/* the chart */}</div>;
}
```

Check the answer when the view depends on it:

```ts
import { lockOrientation, toast } from '@/lib/appmintNative';

const ok = await lockOrientation('portrait');
if (!ok) toast('Turn your phone upright for the best view');
```

**Notes:**
- In the app the Capacitor ScreenOrientation plugin applies the lock.
- On the web it uses `screen.orientation.lock()`. Most browsers refuse it outside fullscreen and desktop browsers always refuse it, so expect `false` there.
- Always give the user a way out of a locked view.

### `appmintNative.longPress`

```js
longPress(el: HTMLElement, handler: (e: PointerEvent) => void, opts?: { ms?: number; haptics?: boolean }): () => void
```

**Example**

Calls you when the user holds a finger on an element - for a row's context menu, reactions, or card actions.

**Returns:** an unsubscribe function - call it on unmount. **Needs:** nothing.

**Options:** `ms` - hold time, default `450`; `haptics` - medium haptic when it fires, default `true`.

Open an action sheet on long press, with a visible button for the same menu:

```ts
import { useEffect, useRef } from 'react';
import { actionSheet, longPress } from '@/lib/appmintNative';

export function PhotoTile({ src, onDelete }: { src: string; onDelete(): void }) {
  const ref = useRef<HTMLDivElement>(null);
  const openMenu = async () => {
    const i = await actionSheet('Photo', [{ title: 'Delete', destructive: true }]);
    if (i === 0) onDelete();
  };

  useEffect(() => {
    if (!ref.current) return;
    return longPress(ref.current, () => void openMenu());
  }, []);

  return (
    <div ref={ref} className="relative">
      <img src={src} alt="" />
      <button aria-label="Photo actions" className="absolute right-1 top-1" onClick={() => void openMenu()}>⋯</button>
    </div>
  );
}
```

A slower hold, without the haptic:

```ts
import { longPress } from '@/lib/appmintNative';

declare const card: HTMLElement;
const off = longPress(card, (e) => console.log('held at', e.clientX, e.clientY), { ms: 700, haptics: false });
```

**Notes:**
- It fires once per hold and swallows the click that would follow, so a long press does not also "tap".
- Moving more than 8 px cancels it, so scrolling never triggers it. It also blocks the browser's own long-press menu on that element.
- A long press is invisible. Always keep a visible button for the same action.

### `appmintNative.markPushRead`

```js
import { markPushRead } from '@/lib/appmintNative'
```

**Example**

Marks one AppMint Push inbox message as read, or every message when called with no argument.

**Returns:** `boolean` synchronously - `true` if something changed. Always `false` on the web. **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { pushInbox, markPushRead } from '@/lib/appmintNative';

export function openMessage(id: string) {
  markPushRead(id);
  return pushInbox().find((m) => m.id === id);
}

export function markAllRead() {
  markPushRead();          // no id = all messages
  return pushInbox();
}
```

**Notes:** read state lives only on this phone; the tray notification is not removed.

### `appmintNative.mediaSession`

```js
mediaSession — @/lib/appmintNative
```

**Example**

Lock-screen and notification playback controls for an app that plays audio or video: publish what is playing, receive the Play / Pause / Next / Previous / Stop buttons, and hand a video to a TV.

**Returns:** `setMetadata`, `setPlaybackState`, `clear` → `void`. `onAction(handler)` → an unsubscribe function. `cast(url)` → `Promise<boolean>` (`false` when nothing could be offered). **Needs:** **Media Notification** in Step 4 (Access) when you build; AI builds switch it on for you when the code uses `mediaSession`.

```ts
import { useEffect, useRef } from 'react';
import { mediaSession } from '@/lib/appmintNative';

type Track = { url: string; title: string; artist: string; cover: string };

export function Player({ track, onNext, onPrev }: { track: Track; onNext: () => void; onPrev: () => void }) {
  const audio = useRef<HTMLAudioElement>(null);
  const unsubscribe = useRef<(() => void) | null>(null);

  const publish = () => {
    const a = audio.current;
    if (!a) return;
    mediaSession.setPlaybackState({
      playing: !a.paused,
      positionMs: a.currentTime * 1000,
      durationMs: (a.duration || 0) * 1000,
    });
  };

  const onPlay = () => {
    // Register the buttons when playback starts, not on mount (see Notes).
    if (!unsubscribe.current) {
      unsubscribe.current = mediaSession.onAction((action) => {
        const a = audio.current;
        if (!a) return;
        if (action === 'play') void a.play();
        if (action === 'pause') a.pause();
        if (action === 'next') onNext();
        if (action === 'prev') onPrev();
        if (action === 'stop') { a.pause(); mediaSession.clear(); }
      });
    }
    mediaSession.setMetadata({ title: track.title, artist: track.artist, artworkUrl: track.cover });
    publish();
  };

  useEffect(() => () => { unsubscribe.current?.(); mediaSession.clear(); }, []);

  return <audio ref={audio} src={track.url} controls onPlay={onPlay} onPause={publish} onSeeked={publish} />;
}
```

Send the current video to a TV:

```ts
import { can, mediaSession } from '@/lib/appmintNative';

export function CastButton({ url }: { url: string }) {
  if (can('cast') === 'none') return null;
  return <button onClick={async () => { if (!(await mediaSession.cast(url))) alert('No cast target found'); }}>Cast</button>;
}
```

**Notes:**
- On Android the buttons come through the app's `navigator.mediaSession`, which is installed when the page finishes loading. An `onAction` registered earlier (for example in a mount effect) is lost, so register it on the first play.
- The `seek` action (with `positionMs`) is delivered on the web only; the Android notification has no seek.
- `artworkUrl` must be a full `https://` address on the internet. On Android, `cast(url)` opens the "Cast to..." chooser (a hand-off; it resolves `true` once the chooser is asked for). On the web it uses the Remote Playback API.

### `appmintNative.Motion`

```js
Motion.addListener('accel' | 'orientation', cb): Promise<{ remove() }>
```

**Example**

The `@capacitor/motion` shape over the standard `devicemotion` and `deviceorientation` events: movement and tilt of the phone. The same code runs in the app and in a browser.

**Returns:** `Promise<{ remove(): Promise<void> }>` - call `remove()` to stop. `'accel'` calls back with `{ acceleration: {x,y,z}, accelerationIncludingGravity: {x,y,z}, rotationRate: {alpha,beta,gamma}, interval }`; `'orientation'` with `{ alpha, beta, gamma }` (degrees). **Needs:** nothing.

**Shake to refresh.**

```ts
import { Motion, type AccelEvent } from '@/lib/appmintNative';

let handle: { remove(): Promise<void> } | null = null;

export async function startShakeDetect(onShake: () => void) {
  let last = 0;
  handle = await Motion.addListener('accel', (e) => {
    const g = (e as AccelEvent).accelerationIncludingGravity;
    const force = Math.sqrt(g.x * g.x + g.y * g.y + g.z * g.z);
    if (force > 25 && Date.now() - last > 1000) { last = Date.now(); onShake(); }
  });
}

export async function stopShakeDetect() {
  await handle?.remove();
  handle = null;
}
```

**Tilt a level bubble** with the orientation event.

```ts
import { Motion, type OrientationEvent } from '@/lib/appmintNative';

const tilt = await Motion.addListener('orientation', (e) => {
  const { beta, gamma } = e as OrientationEvent;       // front-back, left-right
  bubble.style.transform = `translate(${gamma * 2}px, ${beta * 2}px)`;
});
// later: await tilt.remove();
```

**Notes:** Some browsers ask for motion access first, so start it from a tap there; if the user refuses, the handle is returned but no events arrive. `can('motion')` is `'none'` where the browser has no `DeviceMotionEvent`. Missing sensor values arrive as `0`. Always `remove()` when the screen closes - motion events fire many times a second.

### `appmintNative.musicVolume`

```js
musicVolume() — @/lib/appmintNative
```

**Example**

Reads the level of the app's looping background music, 0 to 100.

**Returns:** `number`, synchronously: `0`-`100`, or `-1` when there is no music (on the web, or an app built without a music file). **Needs:** a file chosen under **Background Music** in Step 3 (Integrate) when you build.

Draw a music slider only when there is music:

```ts
import { useState } from 'react';
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export function MusicSetting() {
  const [level, setLevel] = useState(() => musicVolume());
  if (level < 0) return null;                  // no music in this app: no control

  return (
    <label>
      Music
      <input
        type="range" min={0} max={100} value={level}
        onChange={(e) => {
          const v = Number(e.target.value);
          setLevel(v);
          setMusicVolume(v);
        }}
      />
    </label>
  );
}
```

**Notes:** Android app only; the web answers `-1`. A level set with `setMusicVolume` is kept for the next launch; until the page sets one, it is the build level.

### `appmintNative.navEnd`

```js
import { navEnd } from '@/lib/appmintNative'
```

**Example**

Documented together with `appmintNative.navStart` - the example there shows this one too.

`navStart()` shows the app's thin top loading bar for a screen change the app cannot see by itself; `navEnd()` ends it.

**Returns:** nothing (both are synchronous). **Needs:** nothing. On the web (no app shell bar) both do nothing.

Use them only for a wait where nothing in the DOM changes until data arrives, and always pair them with `try/finally`:

```ts
import { navStart, navEnd } from '@/lib/appmintNative';

export async function openOrder(id: string, render: (order: unknown) => void) {
  navStart();
  try {
    const res = await fetch(`/api/orders/${id}`);
    render(await res.json());
  } finally {
    navEnd();                // always, also when the fetch failed
  }
}
```

**Notes:** They call `WebToApk.navStarted()` / `navSettled()`. The app already drives the bar when your app swaps screens in the DOM, so most route changes need nothing. Nothing is drawn for a change shorter than about 200 ms, and a bar that is never ended stops by itself after 2.5 seconds. Keep rendering your own spinner or skeleton; the bar is extra.

### `appmintNative.navStart`

```js
navStart() / navEnd() from '@/lib/appmintNative'
```

**Example**

`navStart()` shows the app's thin top loading bar for a screen change the app cannot see by itself; `navEnd()` ends it.

**Returns:** nothing (both are synchronous). **Needs:** nothing. On the web (no app shell bar) both do nothing.

Use them only for a wait where nothing in the DOM changes until data arrives, and always pair them with `try/finally`:

```ts
import { navStart, navEnd } from '@/lib/appmintNative';

export async function openOrder(id: string, render: (order: unknown) => void) {
  navStart();
  try {
    const res = await fetch(`/api/orders/${id}`);
    render(await res.json());
  } finally {
    navEnd();                // always, also when the fetch failed
  }
}
```

**Notes:** They call `WebToApk.navStarted()` / `navSettled()`. The app already drives the bar when your app swaps screens in the DOM, so most route changes need nothing. Nothing is drawn for a change shorter than about 200 ms, and a bar that is never ended stops by itself after 2.5 seconds. Keep rendering your own spinner or skeleton; the bar is extra.

### `appmintNative.Network`

```js
Network.getStatus(): Promise<{ connected, connectionType }> / Network.addListener(event, handler): { remove() }
```

**Example**

Online/offline state in the shape of Capacitor's Network plugin - the same code as `networkStatus()` / `onNetworkChange()`.

**Returns:** `getStatus()` → `Promise<{ connected: boolean; connectionType: string }>`. `addListener(event, handler)` → `{ remove(): void }` at once; the handler gets the same shape on every change. **Needs:** nothing.

```ts
import { Network } from '@/lib/appmintNative';
import { useEffect, useState } from 'react';

function useOnline() {
  const [online, setOnline] = useState(true);
  useEffect(() => {
    void Network.getStatus().then((s) => setOnline(s.connected));
    const h = Network.addListener('networkStatusChange', (s) => setOnline(s.connected));
    return () => h.remove();
  }, []);
  return online;
}
```

**Notes:** The event name is ignored - every listener gets connectivity changes. `connectionType` is `'wifi'`, `'cellular'`, `'none'` or `'unknown'`: in the app `getStatus()` and the listener use the Network plugin, on the web `navigator.onLine` / `navigator.connection.type`. `connected` means the phone has a network, not that your server answers.

### `appmintNative.networkStatus`

```js
networkStatus(): { connected: boolean; type: string }
```

**Example**

Says, right now, whether the device is online.

**Returns:** `{ connected, type }`, synchronously. `type` is `'wifi'`, `'cellular'`, `'none'` (offline) or `'unknown'`. **Needs:** nothing.

Cheap enough to call during render:

```ts
import { networkStatus } from '@/lib/appmintNative';

export function SyncButton({ onSync }: { onSync: () => void }) {
  const { connected } = networkStatus();
  return (
    <button disabled={!connected} onClick={onSync}>
      {connected ? 'Sync now' : 'Offline — will sync later'}
    </button>
  );
}
```

Read the start value, then follow changes:

```ts
import { useEffect, useState } from 'react';
import { networkStatus, onNetworkChange } from '@/lib/appmintNative';

export function useOnline(): boolean {
  const [online, setOnline] = useState(() => networkStatus().connected);
  useEffect(() => onNetworkChange((s) => setOnline(s.connected)), []);
  return online;
}
```

**Notes:**
- In the app it is the Network plugin's answer (kept current in the background, so the very first call may still read the WebView's own value). On the web it is `navigator.onLine` plus `navigator.connection.type` where the browser reports one, else `'unknown'` - a speed class such as `'4g'` is never reported as the type.
- "Connected" means the device has a network, not that your server answers. Handle failed requests anyway.

### `appmintNative.nfc`

```js
import { nfc } from '@/lib/appmintNative'
```

**Example**

One-call helpers over Web NFC (`NDEFReader`): check support, read tags, write a text tag. The Android app uses its own NDEFReader; on the web it is Chrome's own Web NFC (Chrome on Android).

**Returns:** `isSupported(): Promise<boolean>`; `scan(onTag): Promise<() => Promise<void>>` (the stop function), and `onTag` gets `Array<{recordType, text, mediaType?}>` per tag - Web NFC's type names (`'text'`, `'url'`, `'mime'`, …) and the decoded text on every platform; `write(text): Promise<boolean>` (`true` once a tag was written, `false` on any failure). **Needs:** on Android, turn on **Web NFC Support** in Step 4 (Access) when you build. `scan()` throws when there is no `NDEFReader` - check `can('nfc')` first.

```ts
import { useEffect, useRef, useState } from 'react';
import { can, nfc } from '@/lib/appmintNative';

export function TagReader() {
  const [last, setLast] = useState('');
  const stopRef = useRef<null | (() => Promise<void>)>(null);

  useEffect(() => () => { void stopRef.current?.(); }, []);

  // Check at the tap: on Android the app installs NDEFReader while the page loads.
  const start = async () => {
    if (can('nfc') === 'none' || !(await nfc.isSupported())) { setLast('NFC is not available here.'); return; }
    try {
      stopRef.current = await nfc.scan((records) => {
        setLast(records.filter((r) => r.recordType === 'text' || r.recordType === 'url').map((r) => r.text).join(' | '));
      });
      setLast('Hold a tag to the phone.');
    } catch (e) {
      setLast(`NFC error: ${(e as Error).message}`);
    }
  };

  const writeTag = async () => {
    if (can('nfc') === 'none') { setLast('NFC is not available here.'); return; }
    setLast('Tap a tag to write it...');
    setLast((await nfc.write('Hello from my app')) ? 'Tag written.' : 'Write failed or cancelled.');
  };

  return (
    <div>
      <button onClick={start}>Read tags</button>
      <button onClick={writeTag}>Write tag</button>
      <p>{last}</p>
    </div>
  );
}
```

**Notes:** On Android `isSupported()` is `true` whenever the build has NFC - it does not say whether NFC is switched on (`window.AndroidNFC.startScan()` does). After the stop function, `onTag` is never called again. On Android the app itself keeps catching tags while it is in front (so a tap does not open another app), it just stops passing them to you. A text record's `text` has no language prefix; a URL record's `text` is the full URL. `write()` waits until a tag is tapped. For writing URL or mime records, and for locking a tag, use `NDEFReader` directly.

### `appmintNative.notify`

```js
import { notify } from '@/lib/appmintNative'
```

**Example**

Shows one notification right now from an AI-built app, in the Android app and on the web.

**Returns:** `Promise<boolean>` - `true` when posted, `false` when the user refused the notification permission, notifications are switched off for the app, or they are not available here. It asks for the permission itself the first time (Android 13+ and the web). **Needs:** Android: **Notifications** turned on in the Access step (Step 4) when you build.

```ts
import { notify, can } from '@/lib/appmintNative';

export async function onOrderReady(orderId: string) {
  if (can('notify') === 'none') return;          // hide the feature instead
  const ok = await notify({
    title: 'Order ready',
    body: `Order #${orderId} is ready to collect`,
    icon: 'https://example.com/img/order.png',    // shown beside the text
    tag: `order-${orderId}`,                     // same tag replaces the old one
  });
  if (!ok) setBanner('Turn on notifications to get order alerts.');
}
```

**Ask at a good moment:** the first `notify()` shows the permission dialog. To ask from your own "Turn on alerts" button instead - before anything is posted - use the standard API, which the app also supports:

```ts
async function enableAlerts() {
  if (typeof Notification === 'undefined') return false;
  const perm = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission();
  return perm === 'granted';
}
```

**Notes:** Android uses the shell's notification bridge (`icon` becomes the large icon; use an `https://` or `data:` image); the web uses the Notification API. Once the user has refused for good, `notify()` resolves `false` without asking again - point them to the phone's settings. For buttons use `notifyWithActions`; for a sticky progress card use `liveProgress`.

### `appmintNative.notifyWithActions`

```js
import { notifyWithActions } from '@/lib/appmintNative'
```

**Example**

Shows a notification with up to 3 buttons ("Mark done", "Snooze"). The tap comes back through `onNotificationAction`.

**Returns:** `Promise<boolean>` - `true` when posted, `false` when the notification permission was refused (it asks the first time, like `notify()`). **Needs:** Android: **Notifications** turned on in the Access step (Step 4) when you build, and the user's permission on Android 13+. On the web `can('notificationActions')` is `'none'`.

```ts
import { notifyWithActions, onNotificationAction, can } from '@/lib/appmintNative';

export async function remindTask(taskId: string, title: string) {
  if (can('notificationActions') === 'none') return false;   // web: hide the buttons
  return notifyWithActions({
    title,
    body: 'Due now',
    tag: `task-${taskId}`,                       // comes back as e.tag
    actions: [
      { id: 'done', title: 'Mark done' },
      { id: 'snooze', title: 'Snooze' },
    ],
  });
}

// Mount once (e.g. in App.tsx):
export const stopListening = onNotificationAction((e) => {
  // e = { actionId: 'snooze', tag: 'task-42', text: '' }
  const taskId = e.tag.replace('task-', '');
  if (e.actionId === 'done') completeTask(taskId);
  if (e.actionId === 'snooze') snoozeTask(taskId);
});
```

**Notes:** Each button shows its `title`; its `id` is what comes back as `e.actionId`. The `input` option (inline reply box) is not supported in this app - it is ignored, the button has no reply box and `e.text` is always `''`. A tap reaches the page only while the app is running. On the web the wrapper would post a plain notification without buttons, which is why the example checks `can()` first.

### `appmintNative.offlineBanner`

```js
offlineBanner(opts?: { message?: string; onlineMessage?: string }): () => void
```

**Example**

Mounts a ready-made strip that slides in under the status bar when the connection drops, and briefly says "Back online" when it returns.

**Returns:** an unsubscribe function that removes the banner and stops listening. **Needs:** nothing.

Mount it once at app start:

```ts
import { useEffect } from 'react';
import { offlineBanner } from '@/lib/appmintNative';

export function App() {
  useEffect(() => offlineBanner(), []);          // cleanup removes it
  return <main>{/* … */}</main>;
}
```

Your own words (for example in the app's language):

```ts
import { offlineBanner } from '@/lib/appmintNative';

const off = offlineBanner({ message: 'Sin conexión', onlineMessage: 'Conectado de nuevo' });
```

**Notes:**
- Defaults: "You are offline" and "Back online". The online message hides after about 1.8 seconds; the offline one stays until the connection returns.
- If the device is already offline when it mounts, the banner shows at once.
- Built on `onNetworkChange()`, so it is real on every surface. Colours come from the app's tokens (`--ink` / `--bg` offline, `--accent` / `--accent-ink` online). It has `role="status"` for screen readers.
- Mount it once. Two calls draw two banners.

### `appmintNative.onBackGesture`

```js
onBackGesture(handler: () => boolean): () => void
```

**Example**

Lets an open sheet, modal, lightbox or wizard step answer the BACK gesture first - the Android edge swipe / back button in the app, the browser's back button on the web.

**Returns:** an unsubscribe function - call it when the view closes or unmounts. Your handler returns `true` to consume the press (you closed something) or `false` to let it through. **Needs:** nothing.

Register only while the sheet is open:

```ts
import { useEffect, useState } from 'react';
import { onBackGesture } from '@/lib/appmintNative';

export function FilterPanel() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!open) return;
    return onBackGesture(() => { setOpen(false); return true; });   // back closes THIS, not the app
  }, [open]);

  return (
    <>
      <button onClick={() => setOpen(true)}>Filters</button>
      {open && <div role="dialog" aria-label="Filters">{/* … */}</div>}
    </>
  );
}
```

Step back through a wizard, and let the last step's back leave normally:

```ts
import { useEffect } from 'react';
import { onBackGesture } from '@/lib/appmintNative';

export function useWizardBack(step: number, setStep: (n: number) => void) {
  useEffect(() => onBackGesture(() => {
    if (step === 0) return false;       // not ours: normal back navigation happens
    setStep(step - 1);
    return true;
  }), [step, setStep]);
}
```

**Notes:**
- Handlers form a stack: the one registered last (the sheet on top) is asked first. A handler that throws consumes nothing.
- On Android it goes through the shell's back handler. Without it, swiping back with a sheet open closes the whole app.
- On the web it pushes one history entry while any handler is registered and removes it when the last one unsubscribes.
- Still draw a visible back / close control: many users never use the back gesture.
- React Router already handles normal page back; use this only for state the router does not know about. Always unsubscribe, or a closed sheet keeps eating back presses.

### `appmintNative.onDeepLink`

```js
onDeepLink(cb: (url: string) => void): () => void
```

**Example**

Calls you when a link opens the app while it is already running - a deep link, an icon shortcut, a widget tap.

**Returns:** an unsubscribe function - call it on unmount. **Needs:** nothing for shortcuts and widget taps; outside links need the **Deep Linking** option when you build.

```ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { launchUrl, onDeepLink } from '@/lib/appmintNative';

export function DeepLinkRouter() {
  const navigate = useNavigate();
  useEffect(() => {
    const go = (u: string) => { const { pathname, search } = new URL(u); navigate(pathname + search); };
    void launchUrl().then((u) => { if (u) go(u); });   // the link that started the app
    return onDeepLink(go);                             // links while it runs; returns the unsubscribe
  }, [navigate]);
  return null;
}
```

A hand-written page (no React) can listen to the same shell event directly:

```js
window.addEventListener('appmint:deep-link', function (e) {
  var url = e.detail && e.detail.url;
  if (url) console.log('deep link', url);
});
```

**Notes:**
- Android: listens to the shell's `appmint:deep-link` event (`detail.url`); the shell also calls `window.onAppMintDeepLink(detail)` when a page defines it. After a link arrives, `launchUrl()` returns it too.
- Web: never fires (the callback is not called) and the unsubscribe does nothing.

### `appmintNative.onKeyboard`

```js
onKeyboard(cb: (insetPx: number) => void): () => void
```

**Example**

Tells you how many pixels of the page the on-screen keyboard covers, every time that changes, so a bottom bar can ride above it.

**Returns:** an unsubscribe function - call it on unmount. The callback gets a number in CSS pixels: the extra bottom padding you need, `0` when the keyboard is closed. **Needs:** nothing.

```ts
import { useEffect, useState } from 'react';
import { onKeyboard } from '@/lib/appmintNative';

export function Composer() {
  const [kb, setKb] = useState(0);
  useEffect(() => onKeyboard(setKb), []);          // the returned function unsubscribes

  return (
    <form
      className="fixed inset-x-0 bottom-0 flex gap-2 p-3"
      style={{ paddingBottom: `calc(var(--safe-bottom) + 12px + ${kb}px)` }}
    >
      <input className="flex-1" enterKeyHint="send" aria-label="Message" />
      <button type="submit">Send</button>
    </form>
  );
}
```

**Notes:**
- In the app it listens to the Capacitor Keyboard plugin and reports only the part of the keyboard the layout did not already absorb. The shell usually resizes the page for the keyboard, so the value is often `0` there - that is correct; do not add the raw keyboard height yourself.
- In a fullscreen or kiosk app (page not resized) it reports the whole keyboard height.
- On the web it uses `visualViewport`, calls the callback once right away, and treats under 80 px as browser chrome (reported as `0`).
- In the app the first call comes when the keyboard first opens, so start your state at `0`.

### `appmintNative.onNetworkChange`

```js
onNetworkChange(cb: (status: { connected: boolean; type: string }) => void): () => void
```

**Example**

Calls you whenever the connection drops or comes back - for an offline banner, or to retry queued writes.

**Returns:** an unsubscribe function - call it on unmount. **Needs:** nothing.

Retry queued work when the device is back online:

```ts
import { useEffect } from 'react';
import { onNetworkChange, toast } from '@/lib/appmintNative';

export function useRetryWhenOnline(flushQueue: () => Promise<number>) {
  useEffect(() => {
    const off = onNetworkChange(async (s) => {
      if (!s.connected) return;
      const sent = await flushQueue();
      if (sent) toast(`Synced ${sent} change(s)`);
    });
    return off;                                  // unsubscribe on unmount
  }, [flushQueue]);
}
```

**Notes:**
- It does NOT call you with the current state when you subscribe. Read `networkStatus()` for the starting value.
- In the app it listens to the Capacitor Network plugin (`networkStatusChange`); `type` is `'wifi'`, `'cellular'`, `'none'` or `'unknown'`.
- On the web it listens to the `online` / `offline` events and reports `networkStatus()`.
- For a ready-made banner, use `offlineBanner()`, which is built on this.

### `appmintNative.onNotificationAction`

```js
import { onNotificationAction } from '@/lib/appmintNative'
```

**Example**

Listens for taps on notification buttons (from `notifyWithActions`).

**Returns:** a function that stops listening. The callback gets `{ actionId, tag }` - the button's id and the notification's tag. The Android app sends no reply `text` (inline reply is not supported), so do not rely on `e.text`. **Needs:** the same as `notifyWithActions`.

```ts
import { useEffect } from 'react';
import { onNotificationAction } from '@/lib/appmintNative';

export function useNotificationActions(openChat: (chatId: string) => void) {
  useEffect(() => {
    const stop = onNotificationAction(({ actionId, tag }) => {
      if (actionId === 'Open') openChat(tag);
      if (actionId === 'Mute') muteChat(tag);
    });
    return stop;                              // remove on unmount
  }, [openChat]);
}
```

**Raw event in a hand-written page** (Android app):

```js
window.addEventListener('appmint:notification-action', function (e) {
  // e.detail = { actionId: 'Mute', tag: 'chat-7' }
  console.log(e.detail.actionId, e.detail.tag);
});
```

**Notes:** in the Android app this is the `appmint:notification-action` event (also `window.onAppMintNotificationAction(detail)`); on the web it does nothing. A tap is delivered only while the app is running - tapping a button does not open the app.

### `appmintNative.onNotificationClick`

```js
import { onNotificationClick } from '@/lib/appmintNative'
```

**Example**

Runs a callback when the user taps a notification the app posted - including the tap that started the app from cold - so the app can open the right screen.

**Returns:** the unsubscribe function. The callback gets `{ tag, title, body, data }` - `data` is what you passed to `notify({ …, data })`. **Needs:** notifications, like `notify()`. Event behind it: `appmint:notification-click`.

```ts
import { useEffect } from 'react';
import { notify, onNotificationClick } from '@/lib/appmintNative';

export function useOrderAlerts(navigate: (path: string) => void) {
  useEffect(() => onNotificationClick((e) => {
    const d = e.data as { orderId?: number } | null;
    if (d?.orderId) navigate(`/orders/${d.orderId}`);
  }), [navigate]);
}

async function orderShipped(orderId: number) {
  await notify({ title: 'Order shipped', body: `#${orderId} is on its way`, tag: `order-${orderId}`, data: { orderId } });
}
```

**Notes:** The tap travels through the splash and the PIN screen to the page. `data` must be JSON (under 16 KB) and needs a `tag`.

### `appmintNative.onPullToRefresh`

```js
onPullToRefresh(el: HTMLElement, onRefresh: () => void | Promise<void>, opts?: { threshold?: number }): () => void
```

**Example**

Adds the pull-down-to-refresh gesture to a scrolling list: pull down at the top, release, and your data reloads.

**Returns:** an unsubscribe function - call it on unmount. **Needs:** nothing.

`el` is the element that scrolls (or `document.documentElement` when the whole page scrolls). The spinner stays until the promise from `onRefresh` settles:

```ts
import { useEffect, useRef } from 'react';
import { onPullToRefresh, toast } from '@/lib/appmintNative';

export function Inbox({ reload }: { reload: () => Promise<void> }) {
  const listRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!listRef.current) return;
    return onPullToRefresh(listRef.current, async () => {
      try { await reload(); }
      catch { toast('Could not refresh — check your connection'); }
    });
  }, [reload]);

  return <div ref={listRef} className="h-full overflow-y-auto">{/* rows */}</div>;
}
```

A page that scrolls as a whole, with a longer pull:

```ts
import { onPullToRefresh } from '@/lib/appmintNative';

declare function reloadFeed(): Promise<void>;
const off = onPullToRefresh(document.documentElement, reloadFeed, { threshold: 96 });
```

**Notes:**
- It starts only when the list is scrolled to the very top. The default `threshold` is 72 px of pull.
- It draws a round indicator in the app's colours (`--surface`, `--accent`), fires a medium haptic at the commit point, and ignores new pulls while a refresh runs.
- It refreshes your DATA, not the page. The shell's own page-reload pull (**Enable Pull-to-Refresh** in Step 3 (Integrate)) is off by default - keep it off when you use this, so a pull does not also reload the page.
- Built on Pointer Events, so it is the same on every surface. It sets `overscroll-behavior-y: contain` on `el` and restores it on unsubscribe.

### `appmintNative.onShared`

```js
onShared(cb: (p: { text, subject, url, receivedAt }) => void): () => void
```

**Example**

Calls you when another app shares text or a link into this app while it is already open.

**Returns:** an unsubscribe function - call it on unmount. **Needs:** the build's **Receive Shares From Other Apps** option with "Text and links" - switched on automatically when your code calls `onShared(` or `sharedText(`.

Handle both cases - a share that started the app, and one that arrives later:

```ts
import { useEffect } from 'react';
import { can, sharedText, clearShared, onShared, toast } from '@/lib/appmintNative';

export function useIncomingShares(save: (text: string, url: string) => Promise<void>) {
  useEffect(() => {
    if (can('shareTarget') === 'none') return;
    const handle = async (p: { text: string; url: string }) => {
      await save(p.text, p.url);
      clearShared();
      toast('Added');
    };
    const waiting = sharedText();
    if (waiting) void handle(waiting);                    // shared before the app opened
    return onShared((p) => void handle(p));              // shared while open; returns the unsubscribe
  }, [save]);
}
```

A hand-written page can listen to the shell event itself:

```js
window.addEventListener('appmint:shared', function (e) {
  var p = e.detail;                  // { text, subject, url, receivedAt }
  if (p && p.text) console.log('shared:', p.text);
});
```

**Notes:**
- Android: listens to the shell's `appmint:shared` event; the shell also calls `window.onAppMintShared(detail)` when a page defines it. The same payload is also kept for `sharedText()` until `clearShared()`.
- On the web it never fires and returns a no-op unsubscribe.

### `appmintNative.passkeys`

```js
passkeys — available / create / get / origin / assetLinks
```

**Example**

Passkey sign-in and registration with the JSON options your passkey server makes. In the Android app it uses Android's passkey manager; on the web it uses `navigator.credentials`.

**Returns:** `available()` → `'native' | 'web' | 'none'`; `create(optionsJSON)` → Promise of the RegistrationResponseJSON; `get(optionsJSON)` → Promise of the AuthenticationResponseJSON (POST either back to your server). They reject with a `DOMException` (`NotAllowedError`, `InvalidStateError`, `SecurityError`, `NotSupportedError`). `origin()` → string, `assetLinks()` → array (app only; `''` / `[]` on the web). **Needs:** a passkey server; in the app also Android 9+ with Google Play services and your domain's `assetlinks.json`.

```ts
import { passkeys } from '@/lib/appmintNative';

export async function signInWithPasskey(): Promise<boolean> {
  if (passkeys.available() === 'none') return false;          // hide the passkey button
  const options = await (await fetch('https://your-domain.com/passkey/login-options')).json();
  try {
    const answer = await passkeys.get(options);
    const res = await fetch('https://your-domain.com/passkey/login-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer),
    });
    return res.ok;
  } catch (e) {
    if ((e as DOMException).name === 'NotAllowedError') return false;   // cancelled / none here
    throw e;
  }
}

export async function addPasskey() {
  const options = await (await fetch('https://your-domain.com/passkey/register-options')).json();
  const answer = await passkeys.create(options);   // InvalidStateError = already registered
  await fetch('https://your-domain.com/passkey/register-verify', {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer),
  });
}
```

One-time setup values, from the app installed via Google Play:

```ts
import { passkeys } from '@/lib/appmintNative';

console.log(JSON.stringify(passkeys.assetLinks(), null, 2));  // → /.well-known/assetlinks.json
console.log(passkeys.origin());                               // → add to expected origins
```

**Notes:** On the web it needs a secure (`https://`) page. A page bundled in the app has no domain: your server's options must set `rp.id` / `rpId` to your domain. Passkey autofill (`mediation: 'conditional'`) is not available in the app - start from a button.

### `appmintNative.paste`

```js
paste(): Promise<string>
```

**Example**

Reads the text on the system clipboard.

**Returns:** `Promise<string>` - the clipboard text, or `''` when the clipboard is empty. It REJECTS when this browser cannot read the clipboard at all, or when the user denies permission. **Needs:** nothing.

```ts
import { can, paste, toast } from '@/lib/appmintNative';

export function PasteCode({ onCode }: { onCode: (c: string) => void }) {
  if (can('clipboardRead') === 'none') return null;   // e.g. Firefox and Safari
  return (
    <button
      onClick={async () => {
        try {
          const text = (await paste()).trim();
          if (!text) { toast('The clipboard is empty'); return; }
          onCode(text);
        } catch {
          toast('Clipboard access was blocked');       // denied permission, not "empty"
        }
      }}
    >
      Paste code
    </button>
  );
}
```

The Capacitor-shaped form:

```ts
import { Clipboard } from '@/lib/appmintNative';

const { value } = await Clipboard.read();
```

**Notes:** In the app the Clipboard plugin reads the text; an empty clipboard gives `''`, not an error. In a browser it is `navigator.clipboard.readText()` (Chromium only), which asks the user for permission - call it from a tap.

### `appmintNative.pickContact`

```js
pickContact(): Promise<Contact | null>
```

**Example**

Opens the system contact picker so the user chooses one person: Android's picker in the app, the Contact Picker API in Chrome on Android.

**Returns:** `Promise<Contact | null>` - `null` when the user cancels or access is refused. **Needs:** nothing (the system picker needs no permission).

**Pick a person and fill a form.** The contact has the same shape in the app and on the web: `{ id?, name?, phones?: string[], emails?: string[], photoUri? }`.

```ts
import { pickContact, can } from '@/lib/appmintNative';

export async function onPickClick(setName: (s: string) => void, setPhone: (s: string) => void) {
  if (can('contactPick') === 'none') return;   // hide the button instead
  try {
    const c = await pickContact();
    if (!c) return;                            // cancelled
    setName(c.name ?? '');
    setPhone(c.phones?.[0] ?? '');
  } catch {
    // only a 5-minute timeout rejects
  }
}
```

**Notes:** In the app only the tapped number comes back, never emails, and `photoUri` is a `content://` address of the contact photo. `can('contactPick')` is `'none'` in browsers without the Contact Picker API (desktop, Safari) - do not render the button there.

### `appmintNative.pickFile`

```js
pickFile(accept?) from '@/lib/appmintNative'
```

**Example**

Lets the user choose one file and read it as text. In the app it opens Android's document picker; on the web it opens a normal file input.

**Returns:** `Promise<PickedFile | null>` - `{ name, mime, size, text(): Promise<string> }`, or `null` only when the user cancels. It rejects with an `Error` whose `code` is `'no-picker'` (the phone has no document picker app), `'disabled'` or `'failed'` when no picker could open. `text()` reads the whole file and rejects with the bridge's code (for example `'not-found'`) when it cannot. **Needs:** nothing. With **Native Folder Access (SAF)** ticked in Step 4 (Access) the app uses Android's document picker; without it the app uses the page's own file chooser.

```ts
import { pickFile } from '@/lib/appmintNative';

async function importCsv() {
  const f = await pickFile('text/csv');          // default '*/*' = any file
  if (!f) return;                                // cancelled (or picker not enabled in the build)
  if (f.size > 2_000_000) { alert('Please choose a file under 2 MB.'); return; }
  try {
    setRows(parseCsv(await f.text()));
  } catch (e) {
    alert(`Could not read ${f.name}: ${String(e)}`);
  }
}
```

**Notes:** In the app `text()` reads the file in 4 MB pieces until the end and decodes it as UTF-8, so large files are read in full - keep your own size limit for what the screen can handle. A picker that cannot open rejects, so wrap `pickFile()` in try/catch as well. On the web `text()` is the browser's own `File.text()`. For binary files or writing back, hand-written pages use `WebToApkFS`.

### `appmintNative.pip`

```js
pip.enter(video?) / pip.exit(video?)
```

**Example**

Keeps a video playing in a small floating window while the user does something else.

**Returns:** `enter()` returns `Promise<boolean>` - `true` when the floating window opened. `exit()` returns `Promise<void>`. Neither rejects. **Needs:** in the Android app the build's **Picture-in-Picture (PiP)** option in Step 3 (Integrate) - it is switched on automatically when your code calls `pip.enter(`.

A "minimise" button on a player:

```ts
import { useRef } from 'react';
import { can, pip, toast } from '@/lib/appmintNative';

export function Player({ src }: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  return (
    <div>
      <video ref={videoRef} src={src} controls playsInline />
      {can('pip') !== 'none' && (
        <button
          onClick={async () => {
            const ok = await pip.enter(videoRef.current ?? undefined);
            if (!ok) toast('Picture-in-picture is not available here');
          }}
        >
          Minimise
        </button>
      )}
    </div>
  );
}
```

Leave it when the player closes (web):

```ts
import { pip } from '@/lib/appmintNative';

declare const video: HTMLVideoElement;
await pip.exit(video);
```

**Notes:**
- Android (8+): the WHOLE app shrinks into the floating window, and the `video` argument is ignored. It returns `false` on older Android or in a build without the PiP option. The user expands the window to come back; `exit()` does nothing there.
- The web: the `<video>` element's own picture-in-picture. The video must be loaded (usually playing); a call without a `video` returns `false`.
- Call it from a tap.

### `appmintNative.platform`

```js
platform(): 'android' | 'ios' | 'web'
```

**Example**

Tells you which surface the code runs on: the installed Android app, or a web browser.

**Returns:** a string, right away: `'android'` inside the app, `'web'` in a browser. It asks Capacitor first (`Capacitor.getPlatform()`), then checks for the Android shell bridge, else `'web'`. The type also lists `'ios'`, but an AppMint app never gets it - AppMint builds Android apps only. **Needs:** nothing.

**Pick the right link and copy.**

```ts
import { platform } from '@/lib/appmintNative';

const inApp = platform() === 'android';

const rateLink = inApp
  ? 'https://play.google.com/store/apps/details?id=com.example.app'
  : '/feedback';

const backHint = inApp ? 'Press Back to go back' : 'Use the browser\'s back button';
```

**Notes:** `platform()` never answers `'web'` inside the installed app. Do not use it to decide whether a feature exists - many features also depend on build switches; `can('<capability>')` answers that.

### `appmintNative.privacyScreen`

```js
privacyScreen — enable() / disable() / isEnabled()
```

**Example**

Blocks screenshots, screen recording and the app-switcher preview while a screen with secrets is open.

**Returns:** `enable()` → `Promise<boolean>` (`false` where nothing can block it); `disable()` → `Promise<void>`; `isEnabled()` → `Promise<boolean>`. **Needs:** nothing. In the Android app it uses the shell's screenshot block (screenshots, screen recording, casting and the recents thumbnail).

```ts
import { useEffect } from 'react';
import { privacyScreen } from '@/lib/appmintNative';

/** Use in the component that shows the secret (a card number, a statement). */
export function usePrivacyScreen() {
  useEffect(() => {
    void privacyScreen.enable();
    return () => { void privacyScreen.disable(); };   // e.g. a modal closed without a route change
  }, []);
}
```

Tell the user what is protected - only when it really is:

```ts
import { privacyScreen, can } from '@/lib/appmintNative';

export async function protectionLabel(): Promise<string> {
  if (can('privacyScreen') === 'none') return '';           // web: nothing can stop a screenshot
  return (await privacyScreen.isEnabled()) ? 'Screenshots are blocked on this screen' : '';
}
```

**Notes:** In the app the block belongs to the current page and is released when the app navigates to another route, so enable it again when that screen shows again. On the web `can('privacyScreen')` is `'none'` and `enable()` returns `false` - do not claim protection there. Nothing stops a camera pointed at the screen.

### `appmintNative.pushInbox`

```js
import { pushInbox } from '@/lib/appmintNative'
```

**Example**

Returns the AppMint Push messages this install received (newest first, up to 50), for an in-app notifications screen.

**Returns:** `PushMessage[]` synchronously: `{ id, title, body, imageUrl, link, receivedAt, read }`. An empty array on the web and when AppMint Push is off. **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { useEffect, useState } from 'react';
import { pushInbox, markPushRead, type PushMessage } from '@/lib/appmintNative';

export function useInbox() {
  const [items, setItems] = useState<PushMessage[]>(() => pushInbox());
  useEffect(() => {
    const refresh = () => setItems(pushInbox());
    // detail: { type: 'received', message } or { type: 'revoked', id }
    window.addEventListener('appmint:push', refresh);
    return () => window.removeEventListener('appmint:push', refresh);
  }, []);
  const unread = items.filter((m) => !m.read).length;
  const open = (m: PushMessage) => {
    markPushRead(m.id);
    setItems(pushInbox());
  };
  return { items, unread, open };
}
```

**Notes:** the inbox is the saved truth - read it on mount; the `appmint:push` event only fires while the app is on screen. Dismissed messages are left out.

### `appmintNative.readBarcodeFrom`

```js
readBarcodeFrom(source: Blob | CanvasImageSource, formats?: BarcodeFormat[]): Promise<{ value, format }[]>
```

**Example**

Reads barcodes and QR codes from a picture the page already has - a photo file, a Blob, an `<img>`, a `<canvas>` or a video frame. No camera is opened.

**Returns:** `Promise<{ value: string; format: string }[]>` - every code found, in the detector's order; an empty array when there is none, when the image cannot be read, or when barcode reading is not available. It never rejects. **Needs:** nothing.

Read a code from a photo the user picks:

```ts
import { can, readBarcodeFrom, toast } from '@/lib/appmintNative';

export function ReadFromPhoto({ onCode }: { onCode: (v: string) => void }) {
  if (can('barcodeScan') === 'none') return null;
  return (
    <label>
      Read a code from a photo
      <input
        type="file"
        accept="image/*"
        onChange={async (e) => {
          const file = e.currentTarget.files?.[0];
          if (!file) return;
          const codes = await readBarcodeFrom(file, ['qr_code']);
          if (codes.length) onCode(codes[0].value);
          else toast('No QR code found in that photo');
        }}
      />
    </label>
  );
}
```

From an image already on the page:

```ts
import { readBarcodeFrom } from '@/lib/appmintNative';

const img = document.querySelector('img#ticket') as HTMLImageElement;
const [first] = await readBarcodeFrom(img);
if (first) console.log(first.format, first.value);
```

**Notes:**
- Uses the browser engine's `BarcodeDetector` (present in the Android app's WebView; missing in Safari and many desktop browsers - then it returns `[]`).
- A plain `<input type="file" accept="image/*">` opens the Android photo picker and needs no storage permission.
- Narrow `formats` when you know them: it is faster and misreads less. `barcodeFormats()` lists what the device supports.

### `appmintNative.readOtp`

```js
import { readOtp } from '@/lib/appmintNative'
```

**Example**

Waits for the SMS one-time code for this app and returns it - Google's SMS consent sheet in the Android app, WebOTP in Chrome on the web.

**Returns:** `Promise<string | null>` - the code, or `null` when the user declined, five minutes passed, or `signal` aborted. Throws when `can('otp')` is `'none'`. **Needs:** nothing to switch on. The SMS must end with `@<host> #<code>` (in the Android app the host is `appassets.androidplatform.net`).

```ts
import { useEffect, useState } from 'react';
import { can, readOtp } from '@/lib/appmintNative';

export function CodeField({ onDone }: { onDone: (code: string) => void }) {
  const [code, setCode] = useState('');

  useEffect(() => {
    if (can('otp') === 'none') return;
    const ac = new AbortController();
    readOtp({ signal: ac.signal }).then((c) => { if (c) { setCode(c); onDone(c); } });
    return () => ac.abort();          // leaving the screen stops the wait
  }, [onDone]);

  return (
    <input value={code} onChange={(e) => setCode(e.target.value)}
      inputMode="numeric" autoComplete="one-time-code" aria-label="Verification code" />
  );
}
```

**Notes:** it is an accelerator - always keep the field typeable.

### `appmintNative.recognizeText`

```js
import { recognizeText } from '@/lib/appmintNative'
```

**Example**

Reads the text in an image on the device (`can('ocr')`): the Android app's ML Kit recognizer, or Chrome's TextDetector where enabled.

**Returns:** Promise of `{ text, blocks: [{ text, box, lines: [{ text, box }] }] }`, boxes in image pixels. Rejects with `code` `'unavailable'` where `can('ocr')` is `'none'`, or a DOMException when recognition fails. **Needs:** nothing, no permission.

```ts
import { useState } from 'react';
import { can, recognizeText } from '@/lib/appmintNative';

export function ReceiptReader() {
  const [total, setTotal] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  if (can('ocr') === 'none') return null;
  return (
    <label>
      Scan a receipt
      <input type="file" accept="image/*" capture="environment" onChange={async (e) => {
        const file = e.target.files?.[0];
        if (!file) return;
        setBusy(true);
        try {
          const { text } = await recognizeText(file);
          setTotal(/total\s*[:$€£]?\s*([\d.,]+)/i.exec(text)?.[1] ?? 'not found');
        } finally { setBusy(false); }
      }} />
      {busy ? <span role="status">Reading…</span> : total && <strong>Total: {total}</strong>}
    </label>
  );
}
```

**Notes:** Latin scripts only. The first call on a fresh phone can take a few seconds while Play services fetches the model - show progress.

### `appmintNative.remoteConfig`

```js
remoteConfig.start() / fetch() / apply() / manifestUrl() / handledHere() / dismissed() / dismiss()
```

**Example**

Draws the announcement strip and the maintenance screen that the creator publishes from Remote Update - for the page when it runs in a web browser. In the Android app the shell already does this itself, so these calls do nothing there.

**Returns:** `start(manifestUrl?)` → `Promise<void>` (fetch + draw; never rejects); `fetch(url)` → `Promise<RemoteConfig | null>` (never rejects; `null` = nothing applies); `apply(cfg)` → a teardown function; `manifestUrl()` → `string` (the `content` of `<meta name="appmint-remote-manifest">`, `''` when there is none); `handledHere()` → `boolean`; `dismissed(id)` → `boolean`; `dismiss(id)` → `void`. **Needs:** nothing in the app - Remote Update is native there.

Safe to call at startup; in the app it returns at once:

```ts
import { useEffect } from 'react';
import { remoteConfig } from '@/lib/appmintNative';

// Your Remote Update manifest URL. AppMint does not write the meta tag into the page,
// so pass the URL when the same page also runs on the web.
const MANIFEST = 'https://example.com/remote-update/manifest.json';

export function App() {
  useEffect(() => { void remoteConfig.start(MANIFEST); }, []);   // does nothing in the Android app
  return <main>{/* … */}</main>;
}
```

Or read the values and draw your own UI (web only):

```ts
import { remoteConfig, type RemoteConfig } from '@/lib/appmintNative';

export async function loadNotice(url: string): Promise<RemoteConfig | null> {
  if (!remoteConfig.handledHere()) return null;         // the Android app draws its own
  const cfg = await remoteConfig.fetch(url);
  if (!cfg || !cfg.announcementText) return cfg;
  return remoteConfig.dismissed(cfg.announcementId) ? null : cfg;
}
// when the user closes your banner: remoteConfig.dismiss(cfg.announcementId)
```

**Notes:**
- `RemoteConfig` = `{ version, announcementText, announcementId, announcementImageUrl, announcementBgColor, announcementTextColor, maintenanceMode, maintenanceText }`.
- `handledHere()` is `false` in the Android app: the shell reads the manifest natively and draws the announcement itself, so you never see two banners.
- `start()` without an argument uses `manifestUrl()`. An AppMint build does not add that meta tag, so without an argument (or your own `<meta name="appmint-remote-manifest" content="…">`) nothing is fetched.
- `fetch()` returns `null` for a paused or reverted publish, a manifest whose `platforms` list does not include `web`, a newer manifest format, or any network error.
- Maintenance mode wins: `apply()` then covers the whole page with the maintenance text. A dismissed announcement stays hidden until a new `announcementId` is published.

### `appmintNative.requestReview`

```js
requestReview(): Promise<boolean>
```

**Example**

Asks Google Play to show its own rating sheet inside the app (Play In-App Review).

**Returns:** `Promise<boolean>` - `true` when the store was asked, `false` where nothing could be asked (the web, a build without the review plugin, or the request failed). It never tells you whether the sheet appeared or what the user chose. **Needs:** nothing; the sheet only appears for an app installed from Google Play.

```ts
import { requestReview } from '@/lib/appmintNative';

async function onOrderDelivered(count: number) {
  // After a success moment, and rarely — not on launch, not after an error.
  if (count === 3 && !localStorage.getItem('askedReview')) {
    localStorage.setItem('askedReview', '1');
    await requestReview();
  }
}
```

**Notes:** Google Play rate-limits the sheet and often shows nothing - never reward a review and never build a "Rate us" button that expects the sheet to open. In the app it uses the InAppReview plugin. It never leaves the app for the Play listing - for a "Rate us on Play" link, open the listing yourself.

### `appmintNative.restorePushMessage`

```js
import { restorePushMessage } from '@/lib/appmintNative'
```

**Example**

Undoes `dismissPushMessage(id)` - the message returns to the AppMint Push inbox in its old place.

**Returns:** `boolean` synchronously - `true` if it was restored; `false` if the id is unknown or was not dismissed (always `false` on the web). **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { dismissPushMessage, restorePushMessage, pushInbox } from '@/lib/appmintNative';

let lastDismissed: string | null = null;

export function dismiss(id: string) {
  if (dismissPushMessage(id)) lastDismissed = id;
  return pushInbox();
}

export function undo() {
  if (lastDismissed) restorePushMessage(lastDismissed);
  lastDismissed = null;
  return pushInbox();
}
```

**Notes:** messages removed with `clearPushInbox()` or retracted by you cannot be restored.

### `appmintNative.safeArea`

```js
safeArea(): { top: number; right: number; bottom: number; left: number }
```

**Example**

Gives the space taken by the status bar, camera cut-out and navigation bar, in CSS pixels, so fixed bars do not sit under them.

**Returns:** `{ top, right, bottom, left }` numbers, synchronously. **Needs:** nothing.

Most of the time CSS is enough - the runtime puts `--safe-top`, `--safe-right`, `--safe-bottom`, `--safe-left` on `:root`:

```ts
export function BottomNav() {
  return (
    <nav className="fixed inset-x-0 bottom-0" style={{ paddingBottom: 'var(--safe-bottom)' }}>
      {/* tabs */}
    </nav>
  );
}
```

Use the numbers when code must do the maths, for example to place a floating button:

```ts
import { useEffect, useState } from 'react';
import { safeArea } from '@/lib/appmintNative';

export function useBottomInset(): number {
  const [bottom, setBottom] = useState(0);
  useEffect(() => {
    const read = () => setBottom(safeArea().bottom);
    read();
    window.addEventListener('resize', read);        // rotation changes the insets
    return () => window.removeEventListener('resize', read);
  }, []);
  return bottom;
}
```

**Notes:**
- Measured from CSS `env(safe-area-inset-*)`. The value is cached and re-measured after a window resize or rotation.
- Before `document.body` exists (very early at import time) it returns all zeros - call it from an effect.
- On a desktop browser all four are usually `0`.

### `appmintNative.saveFile`

```js
saveFile(fileName, data, mime?) from '@/lib/appmintNative'
```

**Example**

Saves text or a Blob as a file the user keeps, with the name you choose. In the app it opens Android's "Save as…" sheet with that name; in a browser it is a normal download.

**Returns:** (sync) `boolean` - `true` when the download was started, `false` when there is no `document` or creating it threw. It does not tell you whether the user saved or cancelled the sheet. **Needs:** nothing.

```ts
import { saveFile } from '@/lib/appmintNative';

function exportScores(rows: { name: string; score: number }[]) {
  const csv = ['name,score', ...rows.map((r) => `${r.name},${r.score}`)].join('\n');
  if (!saveFile('Scores.csv', csv, 'text/csv')) alert('Could not start the download.');
}
```

A Blob keeps its own type (the third argument is ignored):

```ts
import { saveFile } from '@/lib/appmintNative';

const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
saveFile('Backup.json', blob);
```

**Notes:** It is a plain `<a download>` under the hood, which the app turns into the "Save as…" sheet (see `downloads`). Always pass a name with an extension; without one the file is saved as `download`. The default MIME type for a string is `text/plain`.

### `appmintNative.saveToGallery`

```js
import { saveToGallery } from '@/lib/appmintNative'
```

**Example**

Saves a photo or video into the phone's gallery - `can('gallery')` is `'native'` in the Android app and `'web'` in a browser (the file downloads there).

**Returns:** Promise of the saved item's URI (`''` on the web). Rejects with `code` `'permission-denied'`, `'download-failed'`, `'bad-source'` or `'failed'`. **Needs:** nothing; no permission on Android 10+.

```ts
import { can, saveToGallery, toast } from '@/lib/appmintNative';

export function SaveButton({ canvas }: { canvas: HTMLCanvasElement }) {
  if (can('gallery') === 'none') return null;
  return (
    <button onClick={() => canvas.toBlob(async (blob) => {
      if (!blob) return;
      try {
        await saveToGallery(blob, { album: 'My Cards', fileName: 'card.png' });
        toast('Saved to gallery');
      } catch (e) {
        toast('Could not save');
      }
    }, 'image/png')}>Save to gallery</button>
  );
}
```

**Notes:** `source` may also be a `data:` URL or an http(s) URL; pass `{ kind: 'video' }` for a video URL without a video MIME type.

### `appmintNative.scanBarcode`

```js
scanBarcode(video: HTMLVideoElement, opts?: { signal?, formats?, facingMode? }): Promise<{ value, format } | null>
```

**Example**

Opens the camera in a `<video>` YOU place in your layout, and resolves with the first barcode or QR code it sees.

**Returns:** `Promise<{ value: string; format: string } | null>` - the first code, or `null` when cancelled (the `signal` aborted), when the camera was refused, or when scanning is not available. It never rejects and always stops the camera. **Needs:** camera access - in the Android app turn on **Camera** in Step 4 (Access) when you build.

Use it only when the viewfinder must sit inside your own screen; otherwise `scanCode()` is simpler.

```ts
import { useEffect, useRef, useState } from 'react';
import { can, scanBarcode, haptic } from '@/lib/appmintNative';

export function InlineScanner({ onCode }: { onCode: (v: string) => void }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const video = videoRef.current;
    if (!video || can('barcodeScan') === 'none') return;
    const ctl = new AbortController();
    void scanBarcode(video, { signal: ctl.signal, formats: ['qr_code'] }).then((hit) => {
      if (hit) { haptic('success'); onCode(hit.value); }
      else if (!ctl.signal.aborted) setError('Camera not available. Type the code instead.');
    });
    return () => ctl.abort();          // leaving the screen stops the camera
  }, [onCode]);

  return (
    <div>
      <video ref={videoRef} className="aspect-square w-full object-cover" muted playsInline />
      {error && <p role="alert">{error}</p>}
    </div>
  );
}
```

Use the front camera:

```ts
import { scanBarcode } from '@/lib/appmintNative';

declare const video: HTMLVideoElement;
const hit = await scanBarcode(video, { facingMode: 'user' });
```

**Notes:**
- It calls `getUserMedia` (so the user sees the camera permission prompt) and decodes with `BarcodeDetector` on each frame. `can('barcodeScan')` is `'none'` where either is missing.
- The build scans your code: a `scanBarcode(` call switches **Camera** on for you. If you turned it off by hand, the camera request fails and you get `null`.
- Start it from a user action (a Scan button or opening a scan screen), never on app launch. Handle `null` as a normal outcome with a manual-entry option.

### `appmintNative.scanCode`

```js
scanCode(opts?: { formats?: BarcodeFormat[]; manualInput?: boolean }): Promise<ScanCodeResult | null>
```

**Example**

Opens a full-screen scanner and resolves with the first barcode or QR code - one call, nothing to draw.

**Returns:** `Promise<ScanCodeResult | null>` - `{ value, format, type?, displayValue?, url?, wifi?, phone?, email?, sms?, geo?, contact? }`, or `null` when the user closed the scanner. Anything else rejects with an `Error` whose `code` is `'unavailable'` (no scanner here - Play services missing in the app; no `BarcodeDetector` or camera on the web), `'busy'` (a scan is already open), `'camera-denied'` (web: camera permission refused) or `'failed'`. **Needs:** nothing in the app (Google's scanner needs no camera permission).

From a Scan button:

```ts
import { can, scanCode, haptic, toast } from '@/lib/appmintNative';

export function ScanButton({ onCode }: { onCode: (v: string) => void }) {
  if (can('codeScanner') === 'none') return null;       // nothing can scan here
  return (
    <button
      onClick={async () => {
        try {
          const hit = await scanCode({ formats: ['qr_code'] });
          if (!hit) return;                               // closed: a normal outcome
          haptic('success');
          onCode(hit.value);
        } catch (e) {
          const code = (e as { code?: string }).code;
          toast(code === 'camera-denied' ? 'Allow the camera to scan.' : 'Scanning is not available right now.');
        }
      }}
    >
      Scan QR code
    </button>
  );
}
```

Use the parsed parts the app fills in (Wi-Fi, links, contacts …), with a typed-in fallback in Google's scanner:

```ts
import { scanCode } from '@/lib/appmintNative';

const hit = await scanCode({ manualInput: true });
if (hit?.wifi) console.log('Network', hit.wifi.ssid, hit.wifi.encryption);
else if (hit?.url) window.open(hit.url.url, '_blank');
else if (hit) console.log(hit.type ?? 'text', hit.value);
```

**Notes:**
- In the app this is Google's code scanner (Play services, through the shell's `AppMint.scanCode`): auto-zoom, no camera prompt. `type` is `'url'`, `'wifi'`, `'text'`, `'phone'`, `'email'`, `'sms'`, `'geo'`, `'contact'`, `'calendar'`, `'isbn'`, `'product'` or `'driver_license'`, and the matching field is filled. `manualInput` lets the user type the code.
- On the web it draws its own full-screen camera layer (close button, back gesture closes it) and decodes with `BarcodeDetector`; only `value` and `format` are filled there, and the browser asks for camera permission.
- `null` always means the user closed the scanner; every other outcome rejects with a `code`, so show a message in `catch`.
- Call it from a tap, never on page load. Narrow `formats` when you know them.

### `appmintNative.ScreenOrientation`

```js
ScreenOrientation.lock({ orientation }) / ScreenOrientation.unlock()
```

**Example**

Pins the screen to portrait or landscape for one view, in the shape of Capacitor's ScreenOrientation plugin - the same code as `lockOrientation()`.

**Returns:** `Promise<void>` for both. They never reject; use `lockOrientation()` when you need to know whether the lock took. **Needs:** nothing.

```ts
import { ScreenOrientation } from '@/lib/appmintNative';
import { useEffect } from 'react';

function GameBoard() {
  useEffect(() => {
    void ScreenOrientation.lock({ orientation: 'landscape' });
    return () => { void ScreenOrientation.unlock(); };   // release when leaving
  }, []);
  return <canvas id="board" />;
}
```

**Notes:** Any `orientation` starting with `landscape` locks to landscape; anything else locks to portrait (`'landscape-secondary'` becomes plain landscape). Desktop browsers refuse locks; phone browsers usually allow them only in fullscreen. `lockOrientation(to)` returns `true`/`false`. Give the user a way out of a locked view.

### `appmintNative.screenReader`

```js
screenReader.isEnabled() / screenReader.speak(text, lang?) / screenReader.onChange(cb)
```

**Example**

Tells the app whether TalkBack is on, and announces short messages.

**Returns:** `isEnabled()` → `Promise<boolean>` (`false` on the web - no web API exists). `speak()` → `Promise<void>`. `onChange(cb)` → an unsubscribe function; `cb(enabled)` runs when the screen reader is switched on or off (in the app only). **Needs:** nothing.

```ts
import { screenReader } from '@/lib/appmintNative';
import { useEffect, useState } from 'react';

function useScreenReader() {
  const [on, setOn] = useState(false);
  useEffect(() => {
    void screenReader.isEnabled().then(setOn);
    return screenReader.onChange(setOn);
  }, []);
  return on;   // e.g. skip auto-advancing carousels when true
}

async function onPaid() {
  await screenReader.speak('Payment complete', 'en');
}
```

**Notes:** In the app `speak` goes through the ScreenReader plugin; on the web (or if the plugin fails) it is spoken with text-to-speech. Prefer good markup (`aria-label`, `aria-live` regions) for most announcements; use `speak` for short, important changes.

### `appmintNative.searchContacts`

```js
searchContacts(query: string, limit = 50): Promise<Contact[]>
```

**Example**

Finds contacts that match a search word. App only.

**Returns:** `Promise<Contact[]>` - empty when nothing matches, access is refused, or Contacts is off. **Needs:** the same as `listContacts`: **Contacts** in Step 4 (Access) (turned on by the build when your code calls this), and the user's permission on first use.

**Search box with a small delay while typing.**

```ts
import { searchContacts, can } from '@/lib/appmintNative';

let timer: ReturnType<typeof setTimeout> | undefined;
let latest = 0;

export function onSearchInput(text: string, show: (names: string[]) => void) {
  if (can('contactList') === 'none') return;
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const mine = ++latest;
    const found = await searchContacts(text, 20).catch(() => []);
    if (mine !== latest) return;                     // a newer search already started
    show(found.map((c) => c.name ?? ''));
  }, 250);
}
```

**Notes:** The match is done by the phone's contacts search on names. Each contact is `{ id?, name?, phones?: string[], emails?: string[], photoUri? }`. On the web `can('contactList')` is `'none'` - there is no web API to read the address book. Contacts is personal data: declare it in your Play Data safety form and privacy policy.

### `appmintNative.SecureKeyError`

```js
class SecureKeyError extends Error { code } — from '@/lib/appmintNative'
```

**Example**

The error `secureKeys` throws. Its `code` is one short word you can branch on, the same in the app and on the web.

**Returns:** an `Error` with `name === 'SecureKeyError'` and `code`: `'no-such-key'`, `'auth-required'` (unlock and retry), `'key-invalidated'` (biometrics or the screen lock changed - the data is gone), `'strongbox-unavailable'`, `'disabled'` (Secure Keys not in this build / no Web Crypto), `'bad-alias'`, `'bad-payload'` (not valid base64 or too short), `'too-large'` (over 4 MB in one call on Android), `'foreign-origin'` (a page from another site asked), `'failed'` (anything else). **Needs:** nothing.

```ts
import { secureKeys, SecureKeyError, unlock } from '@/lib/appmintNative';

export async function openSecret(sealed: string): Promise<string | null> {
  try {
    return await secureKeys.decrypt('vault', sealed);
  } catch (e) {
    if (!(e instanceof SecureKeyError)) throw e;
    switch (e.code) {
      case 'auth-required':
        if (await unlock('Open vault')) return secureKeys.decrypt('vault', sealed);
        return null;
      case 'no-such-key':
      case 'key-invalidated':
        alert('This secret can no longer be opened.');
        return null;
      case 'disabled':
        alert('Secure storage is not available in this app.');
        return null;
      default:
        alert('Could not open it: ' + e.code);
        return null;
    }
  }
}
```

**Notes:** `e.message` is `code` or `"code: detail"`; branch on `e.code`, never on the message. The platforms' own error words are turned into these codes, and their original text is kept in the message. A changed or foreign payload is `'bad-payload'` on the web and `'failed'` in the app, which cannot tell it apart from other cipher errors.

### `appmintNative.secureKeys`

```js
secureKeys — generate / encrypt / decrypt / remove / has / list / isHardwareBacked
```

**Example**

Encryption keys the device holds and the app can never export: the Android Keystore in the app, and a non-extractable Web Crypto key on the web. Removing the key is the one real "secure delete".

**Returns:** all Promises. `generate(alias, {requireAuth?, authValiditySeconds?})` → `{hardwareBacked}`; `encrypt(alias, text)` → base64 string; `decrypt(alias, base64)` → the text; `remove(alias)` / `has(alias)` / `isHardwareBacked(alias)` → boolean; `list()` → string[]. `generate`, `encrypt` and `decrypt` throw `SecureKeyError` with a `code`. **Needs:** **Secure Keys (encryption)** in Step 4 (Access) when you build.

```ts
import { secureKeys, SecureKeyError, can } from '@/lib/appmintNative';

export async function saveDiary(text: string) {
  if (can('secureKeys') === 'none') throw new Error('Secure storage is not available here');
  if (!(await secureKeys.has('diary'))) {
    const { hardwareBacked } = await secureKeys.generate('diary', { requireAuth: true, authValiditySeconds: 60 });
    console.log(hardwareBacked ? 'Key in secure hardware' : 'Key in software');
  }
  const sealed = await secureKeys.encrypt('diary', text);    // strings in, base64 out
  localStorage.setItem('diary', sealed);
}

export async function readDiary(): Promise<string | null> {
  const sealed = localStorage.getItem('diary');
  if (!sealed) return null;
  try {
    return await secureKeys.decrypt('diary', sealed);
  } catch (e) {
    if (e instanceof SecureKeyError && e.code === 'key-invalidated') return null;  // data is gone
    throw e;
  }
}
```

Delete for real - nobody can read the sealed data after this:

```ts
import { secureKeys } from '@/lib/appmintNative';

export async function wipeDiary() {
  await secureKeys.remove('diary');
  localStorage.removeItem('diary');
}
```

**Notes:** `can('secureKeys')` is `'native'` in the app and `'web'` in a browser, where `isHardwareBacked()` is always `false` - never say "hardware" there. A `requireAuth` key: in the app `encrypt`/`decrypt` throw `auth-required` without showing anything once the unlock window has passed - call `unlock()` and try again; on the web the runtime shows `unlock()` itself. Encrypt one record at a time (the app caps a call at 4 MB). Aliases: letters, digits, `_ - .`, max 64.

### `appmintNative.sendSms`

```js
sendSms(phoneNumber: string, message: string): Promise<boolean>
```

**Example**

Sends an SMS directly, with no confirmation screen. Android app only.

**Returns:** `Promise<boolean>` - `true` when the phone reports the message as sent, `false` when it failed, when SMS was not enabled, or when the user refused permission. **Needs:** the **SMS (read & send)** switch in Step 4 (Access) - the build turns it on when your code calls this - and the user's permission on first use.

**Send an alert and tell the user what happened.**

```ts
import { sendSms, can } from '@/lib/appmintNative';

export async function sendAlert(to: string, text: string): Promise<string> {
  if (can('smsSend') === 'none') return 'Sending SMS works only in the Android app.';
  try {
    const ok = await sendSms(to, text);
    return ok ? 'Alert sent' : 'The SMS could not be sent';
  } catch {
    return 'No answer from the phone after 3 minutes';
  }
}
```

**Notes:** The wrapper resolves on the FIRST answer, which is `sent` (the message left the phone), not `delivered`. For delivery reports use the raw `WebToApk.sendSms` with the `appmint:sms` event. `can('smsSend')` is `'none'` on the web (no browser can send an SMS). Each SMS may cost the user money. SMS is a Google Play restricted permission: you need an approved Permissions Declaration in Play Console, so prefer `composeSms()` when the user can press send.

### `appmintNative.setMusicVolume`

```js
setMusicVolume(percent) — @/lib/appmintNative
```

**Example**

Sets the level of the app's looping background music, 0 to 100 (rounded and clamped for you).

**Returns:** `void`. **Needs:** a file chosen under **Background Music** in Step 3 (Integrate) when you build. Does nothing when `musicVolume()` is `-1`.

The app keeps the level for the next launch, so a mute toggle is all you need:

```ts
import { musicVolume, setMusicVolume } from '@/lib/appmintNative';

export const hasMusic = () => musicVolume() >= 0;
export const toggleMute = () => setMusicVolume(musicVolume() > 0 ? 0 : 40);   // remembered by the app
```

**Notes:** `0` makes the music silent but it keeps playing. The level is saved on the phone (until the user clears the app's data). Android app only; on the web nothing happens.

### `appmintNative.setPushPresentation`

```js
import { setPushPresentation } from '@/lib/appmintNative'
```

**Example**

Chooses who draws an AppMint Push message you sent with an in-app style (banner, pop-up, bottom sheet or full screen) while the app is open: the app shell (`'auto'`, the default) or your own code (`'none'`).

**Returns:** `boolean` synchronously - `true` when the mode was saved. Always `false` on the web. **Needs:** Android: **AppMint Push** turned on in the Integrate step (Step 3) when you build.

```ts
import { useEffect } from 'react';
import { setPushPresentation, pushInbox } from '@/lib/appmintNative';

// Option A — keep the shell's banner, and never draw a second one on top of it.
export function usePushBanner(show: (title: string) => void) {
  useEffect(() => {
    const onPush = (e: Event) => {
      const d = (e as CustomEvent).detail;
      // { type: 'received', message, presented } — presented = the shell already showed it
      if (d.type === 'received' && !d.presented) show(d.message.title);
    };
    window.addEventListener('appmint:push', onPush);
    return () => window.removeEventListener('appmint:push', onPush);
  }, [show]);
}

// Option B — the app draws every message itself.
export function takeOverPushBanners() {
  setPushPresentation('none');   // saved on the phone; 'auto' gives it back to the shell
  return pushInbox();
}
```

**Notes:** the inbox and the `appmint:push` event work the same in both modes. Setting `'none'` also closes a message the shell is showing right now.

### `appmintNative.setStatusBar`

```js
setStatusBar(backgroundColor: string | null, style?: 'light' | 'dark' | null): void
```

**Example**

Paints the system status bar to match the screen, and picks light or dark icons for it.

**Returns:** nothing. **Needs:** nothing.

`style` is the colour of the ICONS: `'light'` = light icons for a dark bar (the default), `'dark'` = dark icons for a light bar. Call it once at startup with the app's `--bg`, and again on any screen with a different background:

```ts
import { useEffect } from 'react';
import { setStatusBar } from '@/lib/appmintNative';

export function App() {
  useEffect(() => { setStatusBar('#0B0F1A', 'light'); }, []);   // dark app, light icons
  return <main>{/* … */}</main>;
}

export function PhotoViewer() {
  useEffect(() => {
    setStatusBar('#000000', 'light');
    return () => setStatusBar('#0B0F1A', 'light');                // restore on leave
  }, []);
  return null;
}
```

A light screen:

```ts
import { setStatusBar } from '@/lib/appmintNative';

setStatusBar('#FFFFFF', 'dark');
```

The Capacitor-shaped form (the style is named after the BACKGROUND: `'DARK'` = light icons):

```ts
import { StatusBar } from '@/lib/appmintNative';

await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
await StatusBar.setStyle({ style: 'DARK' });
```

**Notes:**
- In the Android app the shell paints the bar itself, which also works on Android 15 edge-to-edge; an unparseable colour is ignored - pass `#RRGGBB`. A style-only call (`backgroundColor = null`) keeps the colour this page last set and flips the icons; before the page set any colour it flips the icons through the StatusBar plugin and the bar keeps the colour chosen at build time.
- On the web it sets `<meta name="theme-color">`; the browser picks the icon colour itself.

### `appmintNative.share`

```js
share(input: { title?, text?, url?, files? }): Promise<boolean>
```

**Example**

Opens the system share sheet in the app, and the Web Share API in a browser.

**Returns:** `Promise<boolean>` - `true` when the sheet opened; `false` when the user dismissed it or sharing is not available. It never rejects. **Needs:** nothing.

```ts
import { can, share, toast } from '@/lib/appmintNative';

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}
```

Text only (no link) is fine too:

```ts
import { share } from '@/lib/appmintNative';

await share({ text: 'My score today: 42' });
```

The Capacitor-shaped form `Share.share(opts)` calls the same code but REJECTS when the user cancels (Capacitor's contract) and resolves `{ activityType: '' }` otherwise; `Share.canShare()` resolves `{ value: can('share') !== 'none' }`:

```ts
import { Share } from '@/lib/appmintNative';

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}
```

**Notes:**
- Pass only the fields you have. In the app, text and links go through the Capacitor Share plugin.
- `files` works in the Android app (the files go to the share sheet as real attachments; a `url` is added to the text) and on the web where the browser can share files (`navigator.canShare({ files })`). Keep files small - they are passed to the app in memory.
- On the web, call it from a tap; browsers refuse a share that no user action started.

### `appmintNative.Share`

```js
import { Share } from '@/lib/appmintNative'
```

**Example**

Documented together with `appmintNative.share` - the example there shows this one too.

Opens the system share sheet in the app, and the Web Share API in a browser.

**Returns:** `Promise<boolean>` - `true` when the sheet opened; `false` when the user dismissed it or sharing is not available. It never rejects. **Needs:** nothing.

```ts
import { can, share, toast } from '@/lib/appmintNative';

export function ShareRecipe({ recipe }: { recipe: { title: string; url: string } }) {
  if (can('share') === 'none') return null;
  return (
    <button
      onClick={async () => {
        const ok = await share({ title: recipe.title, text: 'Try this recipe', url: recipe.url });
        if (ok) toast('Shared');           // false = the user closed the sheet; say nothing
      }}
    >
      Share
    </button>
  );
}
```

Text only (no link) is fine too:

```ts
import { share } from '@/lib/appmintNative';

await share({ text: 'My score today: 42' });
```

The Capacitor-shaped form `Share.share(opts)` calls the same code but REJECTS when the user cancels (Capacitor's contract) and resolves `{ activityType: '' }` otherwise; `Share.canShare()` resolves `{ value: can('share') !== 'none' }`:

```ts
import { Share } from '@/lib/appmintNative';

if ((await Share.canShare()).value) {
  try {
    await Share.share({ title: 'Invite', url: 'https://example.com/join' });
  } catch {
    /* cancelled or unavailable — never report success here */
  }
}
```

**Notes:**
- Pass only the fields you have. In the app, text and links go through the Capacitor Share plugin.
- `files` works in the Android app (the files go to the share sheet as real attachments; a `url` is added to the text) and on the web where the browser can share files (`navigator.canShare({ files })`). Keep files small - they are passed to the app in memory.
- On the web, call it from a tap; browsers refuse a share that no user action started.

### `appmintNative.sharedText`

```js
sharedText(): { text, subject, url, receivedAt } | null
```

**Example**

Gives the text or link another app shared INTO this app ("Share → your app"), for example a link shared from a browser or YouTube.

**Returns:** synchronously, `{ text, subject, url, receivedAt }` or `null` when nothing is waiting. `url` is the first `http(s)` link found in `text` (`''` if none); `receivedAt` is milliseconds since 1970. **Needs:** the build's **Receive Shares From Other Apps** option with "Text and links" - switched on automatically when your code calls `sharedText(` or `onShared(`.

Check on mount, save it, then clear it:

```ts
import { useEffect } from 'react';
import { can, sharedText, clearShared, toast } from '@/lib/appmintNative';

export function useSaveSharedLink(save: (item: { title: string; url: string; text: string }) => Promise<void>) {
  useEffect(() => {
    if (can('shareTarget') === 'none') return;     // on the web
    const p = sharedText();
    if (!p) return;
    void save({ title: p.subject || p.url || 'Shared note', url: p.url, text: p.text })
      .then(() => { clearShared(); toast('Saved'); });
  }, [save]);
}
```

**Notes:**
- The share is KEPT until you call `clearShared()`, so an app that mounts after the share still sees it. Clear it after saving, or the next mount saves it again.
- For a share that arrives while the app is open, use `onShared()`.
- A photo or video shared WITH a caption: the caption arrives here, the file through the opened-file API.
- Android app only. `can('shareTarget')` is `'none'` on the web, and `sharedText()` returns `null` there.

### `appmintNative.sheet`

```js
sheet(opts: { title?, content, dismissible? }): { close(), element, closed }
```

**Example**

Opens a modal bottom sheet with real sheet behaviour - for a filter panel, a composer or a detail preview.

**Returns:** a handle: `close()` closes it, `element` is the body element (fill or update it), `closed` is a `Promise<void>` that resolves when the sheet has closed, however it closed. **Needs:** nothing.

**Options:** `title` (optional heading), `content` - an element, plain text (not HTML), or a function `(host) => { … }` that fills the body; `dismissible` (default `true`: backdrop tap, drag down, Escape and the back gesture close it).

Plain DOM content:

```ts
import { sheet } from '@/lib/appmintNative';

const h = sheet({
  title: 'About this list',
  content: (host) => {
    const p = document.createElement('p');
    p.textContent = 'Items sync to all your devices.';
    host.appendChild(p);
  },
});
await h.closed;
```

React content, rendered into the sheet with a portal:

```ts
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { sheet } from '@/lib/appmintNative';

export function FiltersButton() {
  const [host, setHost] = useState<HTMLElement | null>(null);
  const [close, setClose] = useState<() => void>(() => () => {});

  const open = () => {
    const h = sheet({ title: 'Filters', content: () => {} });
    setHost(h.element);
    setClose(() => h.close);
    void h.closed.then(() => setHost(null));      // unmount the portal after any close
  };

  return (
    <>
      <button onClick={open}>Filters</button>
      {host && createPortal(
        <form onSubmit={(e) => { e.preventDefault(); close(); }}>
          <label><input type="checkbox" name="open" /> Open now</label>
          <button type="submit">Apply</button>
        </form>,
        host,
      )}
    </>
  );
}
```

**Notes:**
- Drawn in the app's tokens (`--surface`, `--ink`, `--muted`, `--border`), above the safe area, with a light haptic on open.
- Drag down closes it when dragged past 120 px or flicked; the body scrolls first when it is not at the top.
- With `dismissible: false` only your own `close()` closes it - give the user a button.
- For a plain list of actions use `actionSheet()`.

### `appmintNative.shortcuts`

```js
shortcuts.set(items: { id?, title, route }[]) / shortcuts.clear()
```

**Example**

Adds shortcuts to the app icon's long-press menu (Android dynamic shortcuts). A tap opens the app at that route.

**Returns:** `set()` returns `Promise<boolean>` - `true` when the shortcuts were handed to the system, `false` where there are none (the web). `clear()` returns `Promise<void>`. **Needs:** nothing.

Set them once at start, and route on the launch link:

```ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { can, shortcuts, launchUrl } from '@/lib/appmintNative';

export function useAppShortcuts() {
  const navigate = useNavigate();
  useEffect(() => {
    if (can('shortcuts') !== 'none') {
      void shortcuts.set([
        { id: 'new', title: 'New note', route: '/new' },
        { id: 'search', title: 'Search', route: '/search' },
      ]);
    }
    void launchUrl().then((u) => { if (u) navigate(new URL(u).pathname); });
  }, [navigate]);
}
```

Remove them (for example after sign-out):

```ts
import { shortcuts } from '@/lib/appmintNative';

await shortcuts.clear();
```

**Notes:**
- At most 4 items; extra items are dropped. A `route` without a leading `/` gets one. `id` is optional.
- Android 7.1+; on older Android the call does nothing. Titles are cut to 25 characters (short label) and 50 (long label).
- A tap opens the app with the launch link `appmint-shortcut://<package><route>`, so `new URL(u).pathname` is your route. Read it with `launchUrl()` on mount and `onDeepLink()` while the app runs.
- `can('shortcuts')` is `'none'` on the web: a browser has no app icon.

### `appmintNative.speak`

```js
speak(text, opts?) — @/lib/appmintNative
```

**Example**

Reads text aloud with the phone's text-to-speech engine.

**Returns:** `void` - it starts speaking (or queues after the current speech) and gives no "finished" signal. `opts`: `{ lang?: string; rate?: number; pitch?: number }` (defaults: engine language, 1, 1). **Needs:** nothing.

```ts
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function ReadAloud({ text }: { text: string }) {
  return (
    <div>
      <button onClick={() => speak(text, { lang: 'en-IN', rate: 0.9 })}>Read aloud</button>
      <button onClick={() => stopSpeaking()}>Stop</button>
    </div>
  );
}
```

Different languages and speeds:

```ts
import { speak } from '@/lib/appmintNative';

speak('வணக்கம்', { lang: 'ta-IN' });          // Tamil
speak('Slow and clear', { rate: 0.7 });       // rate 0.1..4, 1 = normal
speak('High voice', { pitch: 1.5 });          // pitch 0.1..4, 1 = normal
```

**Notes:** In the Android app: the app's native engine. On the web: `speechSynthesis`. Several `speak()` calls play one after another. If you need to know when speech ends (to highlight words or move on), use `window.speechSynthesis` with an utterance's `onend` directly; it works in the Android app too.

### `appmintNative.speech`

```js
speech — @/lib/appmintNative
```

**Example**

Dictation: turns the user's speech into text. Android's speech recognizer in the Android app, the Web Speech API in Chrome.

**Returns:** `speech.isAvailable()` → `Promise<boolean>`. `speech.start(onResult, opts?)` → `Promise<boolean>`: `true` when listening started, `false` when the microphone permission was refused or dictation is not available. `onResult(text, isFinal)` is called with the text. `speech.stop()` → `Promise<void>`. `opts`: `{ lang?: string; partial?: boolean; prompt?: string }`. **Needs:** **Mic** in Step 4 (Access); AI builds switch it on for you when the code uses speech recognition.

```ts
import { useEffect, useState } from 'react';
import { speech } from '@/lib/appmintNative';

export function VoiceNote() {
  const [ok, setOk] = useState(false);
  const [text, setText] = useState('');
  const [msg, setMsg] = useState('');

  useEffect(() => { void speech.isAvailable().then(setOk); }, []);
  if (!ok) return null;                      // no dictation here: hide the mic button

  const listen = async () => {
    setMsg('Listening...');
    const started = await speech.start(
      (t, isFinal) => { setText(t); if (isFinal) setMsg(''); },
      { lang: 'en-US' },                     // partial results by default
    );
    if (!started) setMsg('Microphone permission is needed for voice input.');
  };

  return (
    <div>
      <button aria-label="Speak" onClick={listen}>🎤</button>
      <button onClick={() => { void speech.stop(); setMsg(''); }}>Stop</button>
      <p>{text}</p>
      <p role="status">{msg}</p>
    </div>
  );
}
```

**Notes:**
- With `partial` on (the default) `onResult(text, false)` arrives as words come in, then once `onResult(text, true)` with the final text. With `partial: false` you get only the final one, when the user stops talking.
- The permission prompt is shown on the first `start()`.
- If the user says nothing, `onResult` is not called; listening just ends. Keep a Stop button or a timeout in your UI.
- Listening stops by itself after a pause. `lang` defaults to the phone's language.

### `appmintNative.splash`

```js
splash.hold() / splash.ready()
```

**Example**

Keeps the launch splash on screen until the first real screen is ready, instead of showing "splash, then a spinner".

**Returns:** nothing (both are synchronous). **Needs:** nothing.

By default the splash hides by itself at the app's first paint - you need nothing. When the first screen needs data, hold the splash at module top and release it when that screen is on:

```ts
// main.tsx
import { splash } from '@/lib/appmintNative';

splash.hold();   // at import time, before the first render
```

```ts
// Home.tsx
import { useEffect, useState } from 'react';
import { splash } from '@/lib/appmintNative';

export function Home({ load }: { load: () => Promise<string[]> }) {
  const [items, setItems] = useState<string[] | null>(null);

  useEffect(() => {
    load()
      .then(setItems)
      .catch(() => setItems([]))
      .finally(() => splash.ready());     // ALWAYS release, also when loading failed
  }, [load]);

  if (!items) return null;
  return <ul>{items.map((t) => <li key={t}>{t}</li>)}</ul>;
}
```

**Notes:**
- In the Android app `hold()` tells the shell to keep its splash; `ready()` hides it (the creator's minimum splash time still applies).
- On the web both do nothing - there is no splash.
- The shell caps the hold at about 6 seconds, so a page that never calls `ready()` still opens.

### `appmintNative.StatusBar`

```js
StatusBar.setBackgroundColor({ color }) / StatusBar.setStyle({ style })
```

**Example**

The status bar in the shape of Capacitor's StatusBar plugin, built on `setStatusBar()`.

**Returns:** `Promise<void>` for both. **Needs:** nothing.

```ts
import { StatusBar } from '@/lib/appmintNative';

// Colour only — the icon style is left alone (the shell picks readable icons on Android).
await StatusBar.setBackgroundColor({ color: '#0B0F1A' });
```

**Colour and icons together** - prefer `setStatusBar()`, which sets both in one call:

```ts
import { setStatusBar } from '@/lib/appmintNative';

setStatusBar('#0B0F1A', 'light');   // dark bar, light icons
setStatusBar('#FFFFFF', 'dark');    // light bar, dark icons
```

**Notes:** `style` follows Capacitor: `'DARK'` means a dark background (light icons), anything else means dark icons. In the Android app the shell bridge paints the bar (the plugin's colour call does nothing on Android 15+), and `setStyle()` flips the icons on the bar's current colour. On the web the colour becomes `<meta name="theme-color">`.

### `appmintNative.stopSpeaking`

```js
stopSpeaking() — @/lib/appmintNative
```

**Example**

Stops text-to-speech now and drops anything still waiting to be spoken.

**Returns:** `void`. **Needs:** nothing.

```ts
import { useEffect } from 'react';
import { speak, stopSpeaking } from '@/lib/appmintNative';

export function StoryPage({ story }: { story: string }) {
  // Stop reading when the user leaves the page.
  useEffect(() => () => stopSpeaking(), []);

  return (
    <article>
      <p>{story}</p>
      <button onClick={() => { stopSpeaking(); speak(story); }}>Read from the start</button>
      <button onClick={stopSpeaking}>Stop</button>
    </article>
  );
}
```

**Notes:** Safe to call when nothing is speaking. In the Android app: the native engine's stop. On the web: `speechSynthesis.cancel()`.

### `appmintNative.swipeable`

```js
swipeable(el: HTMLElement, opts: SwipeOptions): () => void
```

**Example**

Adds swipe gestures to an element - swipe-to-delete rows, paging, dismissing a card.

**Returns:** an unsubscribe function that also restores the element's original `touch-action`. **Needs:** nothing.

**Options:** `onLeft`, `onRight`, `onUp`, `onDown` (at least one), `threshold` (px of travel, default `56`), `edgeGuard` (px from the left/right screen edge where horizontal swipes are ignored, default `24`), `haptics` (selection haptic on a swipe, default `true`).

Swipe left to delete, with a visible button for the same action:

```ts
import { useEffect, useRef } from 'react';
import { swipeable, toast } from '@/lib/appmintNative';

export function MessageRow({ id, text, onDelete, onUndo }: {
  id: string; text: string; onDelete(id: string): void; onUndo(id: string): void;
}) {
  const ref = useRef<HTMLLIElement>(null);
  useEffect(() => {
    if (!ref.current) return;
    return swipeable(ref.current, {
      onLeft: () => { onDelete(id); toast('Deleted'); },
    });
  }, [id, onDelete]);

  return (
    <li ref={ref} className="flex items-center justify-between p-4">
      <span>{text}</span>
      <button onClick={() => onDelete(id)}>Delete</button>
      {/* swipe is never the only way to an action */}
    </li>
  );
}
```

Page between tabs with left / right:

```ts
import { swipeable } from '@/lib/appmintNative';

declare const panel: HTMLElement;
declare let tab: number;
const off = swipeable(panel, { onLeft: () => { tab += 1; }, onRight: () => { tab -= 1; }, threshold: 72 });
// later: off();
```

**Notes:**
- Built on Pointer Events, so the same code works with touch, pen and a mouse drag.
- It sets `touch-action` for you: `pan-y` for horizontal handlers (vertical scrolling still works), `pan-x` for vertical ones, `none` for both.
- A swipe must be clearly one direction (1.5 times more on one axis); diagonal scrolls do not fire. A second finger cancels the gesture.
- The edge guard exists because Android's system back gesture owns the screen edges.

### `appmintNative.systemPalette`

```js
systemPalette(): Promise<SystemPalette>
```

**Example**

Reads the phone's Material You colours (built from the wallpaper on Android 12+), so the app's accent can follow the user's wallpaper like the system apps.

**Returns:** `Promise<{ available: boolean; accent1?, accent2?, accent3?, neutral1?, neutral2? }>` - each swatch maps a tone (`'0'`, `'10'`, `'50'`, `'100'` … `'900'`, `'1000'`) to a hex colour. `{ available: false }` below Android 12 and on the web. **Needs:** nothing.

```ts
import { systemPalette } from '@/lib/appmintNative';

export async function applyWallpaperAccent() {
  const p = await systemPalette();
  if (!p.available || !p.accent1) return;          // keep the app's own palette
  const root = document.documentElement.style;
  root.setProperty('--accent', p.accent1['600']);
  root.setProperty('--accent-soft', p.accent1['100']);
}
```

**Notes:** Check contrast before you use a wallpaper tone for text or buttons - `--accent` must still reach 3:1 on your background. Offer it as an option ("Match my wallpaper"), not a forced theme. Call it again when the app resumes; the user may have changed the wallpaper.

### `appmintNative.TextToSpeech`

```js
TextToSpeech — @/lib/appmintNative
```

**Example**

The Capacitor-style `TextToSpeech` object (`speak` / `stop`), running on the same code as `speak()` and `stopSpeaking()`.

**Returns:** `speak({ text, lang?, rate?, pitch? })` → `Promise<void>` that resolves when the text is **queued**, not when it has been spoken. `stop()` → `Promise<void>`. **Needs:** nothing.

```ts
import { TextToSpeech } from '@/lib/appmintNative';

async function announce(orderNo: number) {
  await TextToSpeech.speak({ text: `Order ${orderNo} is ready`, lang: 'en-US', rate: 1, pitch: 1 });
  // Here the text is queued; it may still be speaking.
}

async function silence() {
  await TextToSpeech.stop();
}
```

**Notes:** Because `speak` resolves early, do not chain "the next step after speaking" on it. For a real end signal use `window.speechSynthesis` with an utterance's `onend`. In the app: the shell's native Android text-to-speech engine; on the web: `speechSynthesis`.

### `appmintNative.textZoom`

```js
textZoom.get() / textZoom.getPreferred() / textZoom.set(value)
```

**Example**

An in-app text-size control. In the app it changes the page's text zoom (`1` = 100%); on the web it changes the root font size, which scales every `rem`-based layout.

**Returns:** `get()` → `Promise<number>`. `getPreferred()` → `Promise<number>` - the size the user chose in the phone's settings (`1` on the web). `set(value)` → `Promise<void>`; the value is clamped to 0.5-3. **Needs:** nothing.

```ts
import { textZoom } from '@/lib/appmintNative';

async function bigger() { await textZoom.set((await textZoom.get()) + 0.1); }
async function smaller() { await textZoom.set((await textZoom.get()) - 0.1); }
async function reset() { await textZoom.set(await textZoom.getPreferred()); }
```

**Remember the choice** across launches:

```ts
import { textZoom } from '@/lib/appmintNative';

const saved = Number(localStorage.getItem('textZoom'));
if (saved) await textZoom.set(saved);

export async function setSize(v: number) {
  await textZoom.set(v);
  localStorage.setItem('textZoom', String(v));
}
```

**Notes:** The app already follows the phone's font-size setting; this is an extra, in-app control. Test the layout at large sizes - fixed heights on text controls clip.

### `appmintNative.toast`

```js
toast(message: string, opts?: { duration?: 'short' | 'long' }): void
```

**Example**

Shows a brief message that needs no answer - "Copied", "Saved", "Added to list".

**Returns:** nothing (fire and forget). **Needs:** nothing.

```ts
import { toast, haptic } from '@/lib/appmintNative';

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await onSave();
        haptic('success');
        toast('Saved');                              // 'short' ≈ 2 s
      }}
    >
      Save
    </button>
  );
}
```

A longer message stays about 3.5 seconds:

```ts
import { toast } from '@/lib/appmintNative';

toast('Your export will be ready in a minute', { duration: 'long' });
```

The Capacitor-shaped form `Toast.show({ text, duration? })` does the same and resolves `Promise<void>`:

```ts
import { Toast } from '@/lib/appmintNative';

await Toast.show({ text: 'Added to list' });
```

For a message the user MUST see, use a dialog instead:

```ts
import { Dialog } from '@/lib/appmintNative';

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });
```

**Notes:**
- In the Android app the Capacitor Toast plugin shows it at the bottom of the screen.
- Everywhere else it is a small snackbar drawn over the page in the app's own colours (`--ink` on `--bg`), above the safe-area inset, with `role="status"` so screen readers announce it.
- Only one is shown at a time; a new toast replaces the old one.
- A toast is easy to miss by design. Never use it for an error the user has to act on.

### `appmintNative.Toast`

```js
import { Toast } from '@/lib/appmintNative'
```

**Example**

Documented together with `appmintNative.toast` - the example there shows this one too.

Shows a brief message that needs no answer - "Copied", "Saved", "Added to list".

**Returns:** nothing (fire and forget). **Needs:** nothing.

```ts
import { toast, haptic } from '@/lib/appmintNative';

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
  return (
    <button
      onClick={async () => {
        await onSave();
        haptic('success');
        toast('Saved');                              // 'short' ≈ 2 s
      }}
    >
      Save
    </button>
  );
}
```

A longer message stays about 3.5 seconds:

```ts
import { toast } from '@/lib/appmintNative';

toast('Your export will be ready in a minute', { duration: 'long' });
```

The Capacitor-shaped form `Toast.show({ text, duration? })` does the same and resolves `Promise<void>`:

```ts
import { Toast } from '@/lib/appmintNative';

await Toast.show({ text: 'Added to list' });
```

For a message the user MUST see, use a dialog instead:

```ts
import { Dialog } from '@/lib/appmintNative';

await Dialog.alert({ title: 'Payment failed', message: 'Your card was declined.' });
```

**Notes:**
- In the Android app the Capacitor Toast plugin shows it at the bottom of the screen.
- Everywhere else it is a small snackbar drawn over the page in the app's own colours (`--ink` on `--bg`), above the safe-area inset, with `role="status"` so screen readers announce it.
- Only one is shown at a time; a new toast replaces the old one.
- A toast is easy to miss by design. Never use it for an error the user has to act on.

### `appmintNative.torch`

```js
import { torch } from '@/lib/appmintNative'
```

**Example**

The phone's flashlight: `torch.on()`, `off()`, `toggle()`, `isOn()`, `isAvailable()`. `can('torch')` is `'none'` on the web.

**Returns:** `on()`/`off()` → Promise (rejects with `code` `'unavailable'` or `'CAMERA_IN_USE'`); `toggle()` → the new state; `isOn()` / `isAvailable()` → boolean. **Needs:** nothing, no permission.

```ts
import { useEffect, useState } from 'react';
import { can, torch } from '@/lib/appmintNative';

export function LightButton() {
  const [on, setOn] = useState(false);
  const [ok, setOk] = useState(false);
  useEffect(() => { if (can('torch') !== 'none') torch.isAvailable().then(setOk); }, []);
  if (!ok) return null;
  return (
    <button aria-pressed={on} aria-label="Flashlight" onClick={async () => setOn(await torch.toggle())}>
      {on ? 'Light off' : 'Light on'}
    </button>
  );
}
```

**Notes:** switched off when the app closes. While the app's own camera stream is open, light it with the track's `torch` constraint instead.

### `appmintNative.transition`

```js
transition(update: () => void | Promise<void>, opts?: { type?: 'push' | 'pop' | 'fade' }): Promise<void>
```

**Example**

Runs a screen change with a native-feeling animation: `push` slides the new screen in from the right, `pop` slides back, `fade` cross-fades.

**Returns:** `Promise<void>` that settles after the change and its animation. `update` is your state change (a router `navigate`, a `setState`) and is always applied. **Needs:** nothing.

```ts
import { useNavigate } from 'react-router-dom';
import { transition, haptic } from '@/lib/appmintNative';

export function NoteRow({ id, title }: { id: string; title: string }) {
  const navigate = useNavigate();
  return (
    <button
      onClick={() => { haptic('light'); void transition(() => navigate(`/note/${id}`)); }}   // push
    >
      {title}
    </button>
  );
}

export function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => void transition(() => navigate(-1), { type: 'pop' })}>Back</button>;
}
```

Fade between two tabs:

```ts
import { transition } from '@/lib/appmintNative';

declare function setTab(t: 'list' | 'map'): void;
await transition(() => setTab('map'), { type: 'fade' });
```

**Notes:**
- It uses the View Transitions API when the WebView has it. Otherwise it applies the update and plays a short enter animation on `#root` (or the first child of `<body>`).
- When the user asked the system for reduced motion, it only applies the update - no animation.
- Animations are about 240 ms.

### `appmintNative.unlock`

```js
unlock(title?, subtitle?) — from '@/lib/appmintNative'
```

**Example**

Asks the phone's owner to prove they are present - fingerprint, face, or the device PIN/passcode - and tells you whether they did.

**Returns:** `Promise<boolean>` - `true` when unlocked; `false` when cancelled, failed, locked out, or nothing can check on this device. It never throws. **Needs:** in the app, **Fingerprint** in Step 4 (Access) when you build. On the web: the browser's platform authenticator (WebAuthn).

```ts
import { unlock, can } from '@/lib/appmintNative';

export async function openPrivateNotes(show: () => void) {
  if (can('unlock') === 'none') {
    alert('This device has no fingerprint, face or PIN check.');
    return;
  }
  const ok = await unlock('Unlock notes', 'Use your fingerprint or PIN');
  if (ok) show();
}
```

Hide the button where there is nothing to unlock with:

```ts
import { can } from '@/lib/appmintNative';

const showLockButton = can('unlock') !== 'none';   // 'native' | 'web' | 'none'
```

**Notes:** In the app the device PIN, pattern or password is allowed as the second way in. On the web, the first call enrols the browser's platform authenticator and counts as unlocked. This proves the device owner is present; it is NOT a login - never use it as the only guard on data that lives on a server. On Android 11 and newer the prompt opens on any phone with a screen lock - a PIN-only phone gets it too, and unlocks with the PIN; on Android 10 and older a fingerprint must be saved first, otherwise it answers `false` without a prompt.

### `appmintNative.updates`

```js
updates.supported() / download() / stage() / confirm() / revertIfUnconfirmed() / current()
```

**Example**

A page-side updater for web-bundle updates. Page code does not need it: in an AppMint app, Remote Update is done natively by the Android app, and `updates.supported()` returns `false` everywhere.

**Returns:** `supported()` → `boolean` (currently always `false`); `download(url, sha256, version, binaryVersion)` → `Promise<boolean>` (`false` while unsupported); `stage(dir, version, binaryVersion)` → `Promise<boolean>`; `confirm()` → `void`; `revertIfUnconfirmed(binaryVersion)` → `Promise<boolean>`; `current()` → `{ path, version, binaryVersion } | null`. **Needs:** nothing for your app - updates are published from Remote Update in AppMint.

The only safe use in app code is to check, and do nothing when it is off:

```ts
import { updates } from '@/lib/appmintNative';

if (updates.supported()) {
  // Not reached today: the Android app's own updater owns the whole update lane.
  const staged = await updates.download(
    'https://example.com/bundle-1.2.0.zip',
    'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',  // sha256, required
    '1.2.0',
    '1',
  );
  console.log('update staged:', staged);
}
```

Show which bundle is running (a settings "About" row):

```ts
import { updates } from '@/lib/appmintNative';

const active = updates.current();          // null = no page-staged bundle (always, today)
const label = active ? `Content ${active.version}` : 'Built-in content';
```

**Notes:**
- In the Android app the shell's Remote Update manager downloads, verifies and serves updates, and rolls back a bundle that fails to load. The page is served by the shell itself, so a bundle staged from page code would never be shown - that is why `supported()` is `false`.
- On the web there is nothing to update; the page is whatever the server sends.
- Updates can change HTML, CSS and JavaScript only; a new native feature still needs a new app build.

### `appmintNative.watchIdle`

```js
import { watchIdle } from '@/lib/appmintNative'
```

**Example**

Tells the app when the user goes idle or locks the screen, and when they are back (`can('idle')`). Start it from a tap the first time - the permission request needs one.

**Returns:** Promise of the stop function. The callback gets `{ user: 'active' | 'idle', screen: 'locked' | 'unlocked' }` at start and on every change. Rejects with `code` `'permission-denied'` or `'unavailable'`. **Needs:** nothing, no Android permission.

```ts
import { useRef } from 'react';
import { can, watchIdle } from '@/lib/appmintNative';

export function AwayStatus({ setAway }: { setAway: (away: boolean) => void }) {
  const stop = useRef<(() => void) | null>(null);
  if (can('idle') === 'none') return null;
  return (
    <button onClick={async () => {
      stop.current?.();
      stop.current = await watchIdle((s) => setAway(s.user === 'idle' || s.screen === 'locked'), { thresholdMs: 5 * 60000 });
    }}>Show me as away when idle</button>
  );
}
```

**Notes:** the threshold is at least one minute (the spec's floor).

### `appmintNative.widget`

```js
widget.set(card) / widget.clear() / widget.placedCount() / widget.requestPin()
```

**Example**

Fills the app's home-screen widget - one card with a title, a big value, up to six lines, and a tap that opens the app at a route.

**Returns:** `set(card)` → `Promise<boolean>` (`true` when saved); `clear()` → `Promise<void>`; `placedCount()` → `Promise<number>` (how many copies the user placed); `requestPin()` → `Promise<boolean>` (`true` when the launcher's "add widget" flow opened). None of them reject. **Needs:** nothing in the Android app (the widget is always part of the build).

Update the card after every change the user would want to see on the home screen:

```ts
import { can, widget } from '@/lib/appmintNative';

export async function updateTodayWidget(tasks: { title: string; done: boolean }[]) {
  if (can('widget') === 'none') return;          // on the web
  const open = tasks.filter((t) => !t.done);
  await widget.set({
    title: 'Today',
    value: `${open.length} left`,
    lines: open.slice(0, 6).map((t) => '• ' + t.title),
    route: '/today',                              // a tap opens the app here
  });
}
```

An "Add to home screen" button:

```ts
import { can, widget, toast } from '@/lib/appmintNative';

export function AddWidgetButton() {
  if (can('widget') === 'none') return null;
  return (
    <button
      onClick={async () => {
        if (!(await widget.requestPin())) toast('Long-press your home screen, tap Widgets, and pick this app');
      }}
    >
      Add widget to home screen
    </button>
  );
}
```

**Notes:**
- The card is saved on the device, survives a reboot, and every placed copy is refreshed. `placedCount()` of `0` means no copy is on the home screen yet.
- Card fields: `title` (empty = the app name), `value` (shown large in `accent`, hidden when empty), `lines` (max 6), `route` (default `/`), and `bg` / `ink` / `accent`. The colours default to the CSS tokens `--bg`, `--ink`, `--accent`; they must be colours Android can read, such as `#RRGGBB` - an `hsl()`/`oklch()` value falls back to the default colours.
- A tap opens the app with `appmint-shortcut://<package><route>`; read it with `launchUrl()` / `onDeepLink()`.
- `requestPin()` needs Android 8+ and a launcher that supports it. `clear()` shows the empty "Nothing to show yet" card.
- Android app only: `can('widget')` is `'none'` on the web, where `set()` and `requestPin()` return `false`.

