# Capacitor plugins - JavaScript bridge API

> The Capacitor plugins the app hosts. A hand-written page reaches them as window.Capacitor.Plugins.<Name>; an AI-built app uses the @/lib/appmintNative wrapper each example names.

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

### `Capacitor.ActionSheet`

```js
Capacitor.Plugins.ActionSheet — showActions({ title, message, options })
```

**Example**

Shows a native bottom sheet with a list of choices and tells you which one was tapped (`@capacitor/action-sheet` 8.1.1).

**Returns:** Promise `{ index, canceled }` - `index` is the tapped option; when the sheet is dismissed `canceled` is `true` and `index` is outside the list (`-1`). **Needs:** nothing - every app hosts the plugin.

Each option is `{ title, style? }` with `style` `'DEFAULT'`, `'DESTRUCTIVE'` or `'CANCEL'`.

```js
var ActionSheet = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ActionSheet;

async function photoMenu() {
  if (!ActionSheet) return;
  var opts = [
    { title: 'Share', style: 'DEFAULT' },
    { title: 'Edit', style: 'DEFAULT' },
    { title: 'Delete', style: 'DESTRUCTIVE' }
  ];
  var r = await ActionSheet.showActions({ title: 'Photo', message: 'Choose an action', options: opts });
  if (r.canceled || r.index < 0 || r.index >= opts.length) return;   // dismissed
  if (opts[r.index].title === 'Delete') deletePhoto();
}
```

**In an AI-built app** the Capacitor-shaped wrapper returns `{ index: -1 }` on dismiss; the short form `actionSheet()` returns `null`:

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

const { index } = await ActionSheet.showActions({
  title: 'Photo',
  options: [{ title: 'Share' }, { title: 'Delete', destructive: true }],
});
if (index === 1) deletePhoto();

const i = await actionSheet('Sort by', [{ title: 'Name' }, { title: 'Date' }]);   // number | null
```

**Delete confirmation that also works in a plain browser:**

```js
async function confirmDelete(name) {
  const AS = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ActionSheet;
  if (!AS) return window.confirm('Delete ' + name + '?');
  const r = await AS.showActions({
    title: 'Delete ' + name + '?',
    options: [{ title: 'Delete', style: 'DESTRUCTIVE' }, { title: 'Keep it', style: 'CANCEL' }]
  });
  return !r.canceled && r.index === 0;
}
```

**Notes:** Always check the index bound - never treat any index as a real choice. The wrapper's option shape is `{ title, destructive? }`; it maps `destructive` to the plugin's `'DESTRUCTIVE'` style.

### `Capacitor.App`

```js
Capacitor.Plugins.App — getInfo(), getState(), exitApp(), minimizeApp(), addListener(...)
```

**Example**

App identity and lifecycle: name, package id, version, whether the app is in front, and pause/resume events (`@capacitor/app` 8.1.0).

**Returns:** `getInfo()` → `{ name, id, build, version }`; `getState()` → `{ isActive }`; `getLaunchUrl()` → `{ url }` or nothing; `getAppLanguage()` → `{ value }`; `exitApp()` / `minimizeApp()` → `void`. `addListener()` → Promise of a handle with `remove()`. **Needs:** nothing - every app hosts the plugin.

```js
var App = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.App;
if (App) {
  App.getInfo().then(function (i) {
    document.getElementById('about').textContent = i.name + ' ' + i.version + ' (' + i.build + ')';
  });
}
```

**Pause and resume** - save a draft when the user leaves, refresh when they come back:

```js
if (App) {
  App.addListener('appStateChange', function (s) {
    if (!s.isActive) saveDraft();      // went to the background
    else refreshInbox();               // back in front
  });
  App.addListener('pause', function () { console.log('paused'); });
  App.addListener('resume', function () { console.log('resumed'); });
}
```

**In an AI-built app:**

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

const info = await App.getInfo();   // { name, id, build, version }
App.exitApp();                      // closes the app on Android
```

**Notes:** The `backButton` event never fires: the shell keeps the back button (the plugin runs with `disableBackButtonHandler`). Handle back with `AppMint.setBackHandler(fn)` or the `onBackGesture` wrapper. For deep links use `WebToApk.getLaunchUrl()` / the `appmint:deep-link` event (or `launchUrl()` / `onDeepLink()` in AI-built apps). `minimizeApp()` is Android-only. Never make an Exit button the only way forward.

### `Capacitor.AppLauncher`

```js
Capacitor.Plugins.AppLauncher — canOpenUrl({ url }), openUrl({ url })
```

**Example**

Opens another app by link (`whatsapp://send?text=…`, `tel:`, `geo:`, an https link) and asks whether one can handle it (`@capacitor/app-launcher` 8.0.1).

**Returns:** `canOpenUrl()` → Promise `{ value: boolean }`. `openUrl()` → Promise `{ completed: boolean }` (`false` when no app took the link). **Needs:** nothing - every app hosts the plugin.

```js
var AL = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppLauncher;

async function chatOnWhatsApp(text) {
  if (!AL) return;
  var url = 'whatsapp://send?text=' + encodeURIComponent(text);
  var can = await AL.canOpenUrl({ url: url });
  if (!can.value) { alert('WhatsApp is not installed'); return; }
  var r = await AL.openUrl({ url: url });
  if (!r.completed) alert('Could not open WhatsApp');
}
```

**In an AI-built app:**

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

if (await appLauncher.canOpen('tel:+15551234567')) await appLauncher.open('tel:+15551234567');
```

**Open a map link without asking first:**

```js
async function openMap(query) {
  const AL = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppLauncher;
  if (!AL) { window.open('https://maps.google.com/?q=' + encodeURIComponent(query)); return; }
  const r = await AL.openUrl({ url: 'geo:0,0?q=' + encodeURIComponent(query) });
  if (!r.completed) alert('No maps app could open this');
}
```

**Notes:** On Android 11+ an app can only "see" other apps it declares. Generated apps declare `https`, `geo`, `google.navigation`, `tel`, `mailto`, `sms`, `whatsapp`, `intent` and `upi` links, so `canOpenUrl` answers `false` for other schemes (for example `spotify:`) even when the app is installed. `openUrl` still works for them - call it and check `completed`. On Android `canOpenUrl` also accepts a package name (`'com.whatsapp'`), with the same visibility limit.

### `Capacitor.AppUpdate`

```js
Capacitor.Plugins.AppUpdate — getAppUpdateInfo(), performImmediateUpdate(), startFlexibleUpdate(), completeFlexibleUpdate(), openAppStore()
```

**Example**

Checks Google Play for a newer version of the app and starts Play's in-app update. Vendored `@capawesome/capacitor-app-update` (`template/plugins/app-update`).

**Returns:** `getAppUpdateInfo()` → `{ currentVersionName, currentVersionCode, availableVersionCode, updateAvailability, updatePriority, immediateUpdateAllowed, flexibleUpdateAllowed, clientVersionStalenessDays?, installStatus }` (version codes are strings; `updateAvailability` `2` = an update is available). `performImmediateUpdate()` / `startFlexibleUpdate()` → `{ code }`: `0` OK, `1` cancelled, `2` failed, `3` not available, `4` not allowed, `5` call `getAppUpdateInfo()` first. `openAppStore()` → `void`. **Needs:** nothing to switch on; the app must be installed from Google Play.

```js
var AU = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppUpdate;

async function checkForUpdate() {
  if (!AU) return;
  try {
    var info = await AU.getAppUpdateInfo();          // must come first
    if (info.updateAvailability !== 2) return;       // up to date
    if (info.immediateUpdateAllowed) {
      var r = await AU.performImmediateUpdate();     // Play's full-screen update
      if (r.code === 1) console.log('user said not now');
    } else {
      await AU.openAppStore();                       // the Play listing
    }
  } catch (e) {
    console.log('Play update check failed:', e.message);  // e.g. no Play services
  }
}
```

**Flexible update** - download in the background, then restart when the user agrees:

```js
async function flexibleUpdate() {
  if (!AU) return;
  var info = await AU.getAppUpdateInfo();
  if (info.updateAvailability !== 2 || !info.flexibleUpdateAllowed) return;
  await AU.addListener('onFlexibleUpdateStateChange', function (s) {
    if (s.installStatus === 2) console.log(s.bytesDownloaded + ' / ' + s.totalBytesToDownload);
    if (s.installStatus === 11) showRestartButton();   // 11 = DOWNLOADED
  });
  await AU.startFlexibleUpdate();
}
function restartNow() { if (AU) AU.completeFlexibleUpdate(); }
```

**In an AI-built app:**

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

const info = await appUpdate.check();          // null on the web / without Play
if (info?.inPlace) await appUpdate.start();    // Android: immediate update
```

**Notes:** Use `updateAvailability === 2` as the only "update available" test. The wrapper's `check()` compares `currentVersionName` with `availableVersionCode`, which are never equal on Android, so its `updateAvailable` is not reliable there - test `inPlace` instead. `start()` resolves `true` even when the user cancels.

### `Capacitor.AppwrightDocScanner`

```js
Capacitor.Plugins.AppwrightDocScanner — scan({ pageLimit, pdf, quality }), isSupported()
```

**Example**

Opens the Google ML Kit document scanner: the user photographs one or more pages (or imports them), the edges are found and straightened, and the pages come back as JPEG images. Appwright's own plugin (`AppwrightDocScannerPlugin.kt`).

**Returns:** `scan()` → Promise that always RESOLVES: `{ ok: true, pages: string[] (JPEG data URLs), pdfDataUrl?, pdfPath?, pdfError? }` on success, `{ ok: false, cancelled: true }` when the user backs out, `{ ok: false, error }` when the scanner cannot start. `isSupported()` → `{ value: boolean }` (Google Play services present). **Needs:** nothing to switch on and no camera permission (the scanner runs inside Play services, which downloads its model on first use).

```js
var Scanner = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightDocScanner;

async function scanReceipt() {
  if (!Scanner) { alert('Scanning is not available here'); return; }
  var s = await Scanner.isSupported();
  if (!s.value) { alert('This phone has no Google Play services'); return; }
  var r = await Scanner.scan({ pageLimit: 1, pdf: false, quality: 0.8 });
  if (r.cancelled) return;
  if (!r.ok) { alert('Scanner error: ' + r.error); return; }
  document.getElementById('preview').src = r.pages[0];
}
```

`pageLimit` `0` = no limit. `quality` is 0.1-1 for the JPEG. With `pdf: true` the scanner also writes a PDF: `pdfDataUrl` is it as a `data:application/pdf;base64,…` URL the page can download, upload or show, and `pdfPath` is its copy in the app's cache folder. If the PDF could not be read, the pages still return and `pdfError` says why.

**In an AI-built app** (`null` on cancel, error, or the web):

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

if (can('docScanner') !== 'none') {
  const res = await docScanner.scan({ pageLimit: 5, pdf: true });
  if (res) setPages(res.pages);          // res.pdfPath when pdf: true
}
```

**Scan several pages and offer the PDF:**

```js
async function scanContract() {
  const Scanner = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightDocScanner;
  if (!Scanner) return;
  const r = await Scanner.scan({ pageLimit: 0, pdf: true, quality: 0.9 });
  if (!r.ok) { if (!r.cancelled) alert('Scanner error: ' + r.error); return; }
  if (r.pdfDataUrl) {
    const a = document.createElement('a');
    a.href = r.pdfDataUrl; a.download = 'contract.pdf'; a.click();
  } else if (r.pdfError) console.warn('PDF not made:', r.pdfError);
  console.log(r.pages.length + ' pages scanned');
}
```

**Notes:** Pages are full data URLs and can be large - shrink them before saving many in `localStorage`. Use `pdfDataUrl`, not `pdfPath` - a device file path is not a URL the page can load. A page that cannot be read is skipped; the rest still return.

### `Capacitor.AppwrightGallery`

```js
Capacitor.Plugins.AppwrightGallery — save({ source, fileName, album, kind })
```

**Example**

Saves a photo or a video into the phone's gallery (Pictures/<album> or Movies/<album>). Appwright's own plugin (`AppwrightGalleryPlugin.kt`).

**Returns:** Promise of `{ uri }` of the saved item. Rejects with a `code`: `bad-source`, `download-failed`, `permission-denied` (Android 7-9, storage refused) or `failed`. **Needs:** nothing to switch on; no permission on Android 10+ (Android 7-9 asks for storage the first time).

```js
const Gallery = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightGallery;

async function saveCard(canvas) {
  const dataUrl = canvas.toDataURL('image/png');
  if (!Gallery) { const a = document.createElement('a'); a.href = dataUrl; a.download = 'card.png'; a.click(); return; }
  try {
    await Gallery.save({ source: dataUrl, fileName: 'birthday-card.png', album: 'Cards' });
    toast('Saved to your gallery');
  } catch (e) {
    toast('Could not save: ' + e.message);
  }
}
```

Save a video from your server into its own album, with a message for each failure code:

```js
const Gallery = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightGallery;

async function saveClip(url) {
  if (!Gallery) { window.open(url, '_blank'); return; }
  try {
    await Gallery.save({ source: url, fileName: 'highlight.mp4', album: 'Highlights', kind: 'video' });
    toast('Video saved to Movies/Highlights');
  } catch (e) {
    if (e.code === 'download-failed') toast('Could not download the video. Check your connection.');
    else if (e.code === 'permission-denied') toast('Allow storage access to save videos.');
    else toast('Could not save: ' + e.message);
  }
}
```

Save a photo the user edited as a Blob (for example from `canvas.toBlob`) by turning it into a `data:` URL first:

```js
const Gallery = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightGallery;

function blobToDataUrl(blob) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(r.result);
    r.onerror = () => reject(r.error);
    r.readAsDataURL(blob);
  });
}

async function saveEditedPhoto(canvas) {
  if (!Gallery) return;
  const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.92));
  const { uri } = await Gallery.save({ source: await blobToDataUrl(blob), fileName: 'edit-' + Date.now() + '.jpg', album: 'My Edits' });
  console.log('saved as', uri);
}
```

**Notes:** `source` is a `data:` URL, an http(s) URL or a `content://`/`file://` URI; `kind` (`'image'`/`'video'`) is read from the MIME type when omitted. AI-built apps use `saveToGallery()` from `@/lib/appmintNative`.

### `Capacitor.AppwrightHaptics`

```js
Capacitor.Plugins.AppwrightHaptics
```

**Example**

Appwright's own plugin for rich vibration patterns: a list of beats, each with its own start time, length and strength.

**Returns:** `play({ events })` resolves `{ ok: true }` or `{ ok: false, error }` (`'unsupported'` = no vibrator, `'empty'` = no events, or the system's message). It never rejects. `isSupported()` resolves `{ value: true/false }`. **Needs:** turn on **Vibrate** in Step 4 (Access) when you build. `window.Capacitor` exists only inside the app.

Each event is `{ at, duration, intensity, sharpness }`: `at` = start in ms, `duration` = length in ms (under 20 becomes a 30 ms tap), `intensity` = 0-1 (default 0.8). `sharpness` is used on iOS only.

```js
var AH = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins.AppwrightHaptics : null;

async function heartbeat() {
  if (!AH) return false;
  var r = await AH.play({
    events: [
      { at: 0,   intensity: 0.8 },
      { at: 140, intensity: 0.5 },
      { at: 700, intensity: 0.8 },
      { at: 840, intensity: 0.5 }
    ]
  });
  if (!r.ok) console.log('pattern not played:', r.error);
  return r.ok;
}

document.getElementById('like').addEventListener('click', heartbeat);
```

Hide the button on phones with no vibrator:

```js
(async function () {
  var btn = document.getElementById('feel-it');
  var ok = false;
  if (AH) { try { ok = (await AH.isSupported()).value; } catch (e) {} }
  btn.hidden = !ok;
})();
```

Play a quick double tap on a successful action, and report when it cannot play:

```js
async function successBuzz() {
  const AH = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightHaptics;
  if (!AH) return;
  const r = await AH.play({ events: [{ at: 0, duration: 40, intensity: 1 }, { at: 120, duration: 40, intensity: 0.6 }] });
  if (!r.ok && r.error === 'unsupported') console.log('this phone has no vibrator');
}
```

**Notes:** Strength works only on phones with amplitude control; other phones keep the timing at one fixed strength. Without the Vibrate permission, `play` answers `{ ok: false, error: '...' }`. In AI-built apps use `hapticPlay` from `@/lib/appmintNative`, which also has named patterns.

### `Capacitor.AppwrightLiveProgress`

```js
Capacitor.Plugins.AppwrightLiveProgress — start / update / end
```

**Example**

Appwright's own Android plugin for a sticky, silent, live-updating status notification (progress bar, spinner or running clock) that opens a route when tapped.

**Returns:** `start(opts)` / `update(opts)` → `Promise<{ ok: true, id }>` or `{ ok: false, error }` with nothing posted - `'permission'` when notifications are not allowed (Android 13+: not granted yet, or switched off for the app), `'channel_disabled'` when the user turned off the "Live status" category; `end({ id })` → `Promise<void>`. **Needs:** Android and the notification permission (Android 13+). Not on iOS.

**In an AI-built app** use the wrapper `liveProgress` from `@/lib/appmintNative`:

```ts
import { liveProgress } from '@/lib/appmintNative';
await liveProgress.start({ id: 'order', title: 'Order #42', text: 'Being prepared', indeterminate: true });
await liveProgress.update({ id: 'order', title: 'Order #42', text: 'On the way', progress: 0.6, route: '/orders/42' });
await liveProgress.end('order');
```

**In a hand-written page:**

```js
async function showDelivery(step, total) {
  const P = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightLiveProgress;
  if (!P) return;                                   // not in the Android app
  const r = await P.update({
    id: 'delivery',
    title: 'Your delivery',
    text: 'Stop ' + step + ' of ' + total,
    progress: step / total,                         // 0–1; or indeterminate: true
    route: '/track'                                 // opened when the card is tapped
  });
  if (!r.ok && r.error === 'permission') {
    await Notification.requestPermission();         // then try again
  }
}

async function deliveryDone() {
  const P = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightLiveProgress;
  if (P) await P.end({ id: 'delivery' });
}
```

A workout timer: `startedAt` makes the card count up on its own, so it is posted once and ended when the workout stops:

```js
const Live = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightLiveProgress;
let workoutStart = 0;

async function startWorkout(name) {
  workoutStart = Date.now();
  if (!Live) return;
  const r = await Live.start({ id: 'workout', title: name, text: 'Workout in progress', startedAt: workoutStart, route: '/workout' });
  if (!r.ok && r.error === 'channel_disabled') {
    showHint('Turn on "Live status" in this app\'s notification settings to see the timer.');
  }
}

async function stopWorkout() {
  if (Live) await Live.end({ id: 'workout' });
  saveWorkout({ durationMs: Date.now() - workoutStart });
}
```

**Options:** `id` (default `'live'`), `title`, `text`, `progress` (0-1, omit for no bar), `indeterminate`, `startedAt` (epoch ms, shows a running clock), `route`.

**Notes:** `start` and `update` do the same thing (post or replace the card with that id). The card cannot be swiped away - always call `end`.

### `Capacitor.AppwrightSystemPalette`

```js
Capacitor.Plugins.AppwrightSystemPalette — get()
```

**Example**

Returns the phone's Material You colours (the palette Android 12+ builds from the wallpaper), so the app can match the system apps. Appwright's own plugin (`AppwrightSystemPalettePlugin.kt`).

**Returns:** Promise `{ available: false }` below Android 12, else `{ available: true, accent1, accent2, accent3, neutral1, neutral2 }` - each an object from tone to hex colour, with tones `'0'`, `'10'`, `'50'`, `'100'` … `'900'`, `'1000'` (e.g. `accent1['600'] === '#3F5F90'`). **Needs:** nothing - every app hosts the plugin.

```js
var Palette = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightSystemPalette;

async function useWallpaperColours() {
  if (!Palette) return;
  var p = await Palette.get();
  if (!p.available) return;                 // keep the app's own colours
  var root = document.documentElement.style;
  root.setProperty('--accent', p.accent1['600']);
  root.setProperty('--surface', p.neutral1['50']);
}
useWallpaperColours();
```

**In an AI-built app:**

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

const p = await systemPalette();
if (p.available && p.accent1) document.documentElement.style.setProperty('--accent', p.accent1['600']);
```

Pick light or dark tones to match the system theme, and read the palette again when the user comes back from changing the wallpaper:

```js
var Palette = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightSystemPalette;
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

async function applyPalette() {
  if (!Palette) return;
  var p = await Palette.get();
  if (!p.available) return;
  var dark = darkQuery.matches;
  var s = document.documentElement.style;
  s.setProperty('--accent', p.accent1[dark ? '200' : '600']);
  s.setProperty('--on-accent', p.accent1[dark ? '800' : '0']);
  s.setProperty('--bg', p.neutral1[dark ? '900' : '10']);
  s.setProperty('--text', p.neutral1[dark ? '100' : '900']);
}

darkQuery.addEventListener('change', applyPalette);
document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'visible') applyPalette();
});
applyPalette();
```

**Notes:** Check text contrast before you apply palette colours - a wallpaper tone is not guaranteed to read well on your background. iOS and browsers expose no wallpaper colours: the answer is `{ available: false }`. The colours change when the user changes the wallpaper; call `get()` again on resume.

### `Capacitor.AppwrightWidget`

```js
Capacitor.Plugins.AppwrightWidget — set(card), clear(), placedCount(), requestPin()
```

**Example**

Fills the app's home-screen widget: one card with a title, a big value, up to six lines, your colours and a tap route. The card is saved on the phone and shown even after a reboot. Appwright's own plugin (`AppwrightWidgetPlugin.kt`).

**Returns:** `set({ title?, value?, lines?, route?, bg?, ink?, accent? })` → Promise `{ ok: true, placed }` (`placed` = widgets currently on the home screen). `clear()` → `void`. `placedCount()` → `{ value }`. `requestPin()` → `{ value: boolean }` (`true` when the launcher's "add widget" sheet was opened). **Needs:** nothing - every app declares the widget.

```js
var Widget = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AppwrightWidget;

async function updateWidget(tasks) {
  if (!Widget) return;
  await Widget.set({
    title: 'Today',
    value: tasks.length + ' tasks',
    lines: tasks.slice(0, 6).map(function (t) { return t.title; }),
    route: '/today',                 // opened when the widget is tapped
    bg: '#0B0F1A', ink: '#FFFFFF', accent: '#22D3EE'
  });
}
```

**Offer to place it** (only when none is placed yet):

```js
async function offerWidget() {
  if (!Widget) return;
  var n = (await Widget.placedCount()).value;
  if (n > 0) return;
  var r = await Widget.requestPin();
  if (!r.value) alert('Long-press the home screen, tap Widgets and pick this app.');
}
```

**In an AI-built app:**

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

await widget.set({ title: 'Today', value: '3 tasks', lines: ['Gym', 'Call mum'], route: '/today' });
if ((await widget.placedCount()) === 0) await widget.requestPin();
```

**Notes:** Colours are hex strings; empty ones use a dark card with a cyan value. The tap opens the app at `route` as a deep link (read it with `WebToApk.getLaunchUrl()` or the `appmint:deep-link` event). Before any `set()` the widget says "Nothing to show yet"; `clear()` returns it to that state. Android only - iOS has no widget target, so `can('widget')` is `'none'` there.

### `Capacitor.Badge`

```js
Capacitor.Plugins.Badge — the launcher-icon count
```

**Example**

The Badge plugin (capawesome) that sets the number on the app's launcher icon.

**Returns:** Promises: `set({ count })`, `clear()`, `increase()`, `decrease()` → `void`; `get()` → `{ count }`; `isSupported()` → `{ isSupported }`. (iOS also has `checkPermissions()` / `requestPermissions()`; the Android plugin does not.) **Needs:** nothing on Android.

**In an AI-built app** use the wrapper `badge` from `@/lib/appmintNative`:

```ts
import { badge } from '@/lib/appmintNative';
await badge.set(3);
await badge.clear();
```

**In a hand-written page:**

```js
async function setUnread(count) {
  const B = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Badge;
  if (!B) return false;                       // not inside the app
  try {
    const s = await B.isSupported();
    if (!s.isSupported) return false;         // this launcher shows no badges
    if (count > 0) await B.set({ count: count });
    else await B.clear();
    return true;
  } catch (e) {
    return false;
  }
}

window.addEventListener('appmint:push', function () {
  const inbox = JSON.parse(window.WebToApk ? WebToApk.getPushInbox() : '[]');
  setUnread(inbox.filter(function (m) { return !m.read; }).length);
});
```

**Count up and down as items arrive and are read:**

```js
const B = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Badge;
async function onNewMessage() { if (B) await B.increase(); }
async function onMessageRead() { if (B) await B.decrease(); }
async function showCurrent() {
  if (!B) return;
  const { count } = await B.get();
  console.log('badge shows', count);
}
```

**Notes:** on Android the look depends on the launcher (a number, a dot, or nothing). `navigator.setAppBadge(n)` in the app writes to the same badge.

### `Capacitor.Browser`

```js
Capacitor.Plugins.Browser — open({ url, toolbarColor }), close(), addListener('browserFinished')
```

**Example**

Opens a web page in an in-app browser tab (Chrome Custom Tabs), so the user reads terms, docs or a sign-in page and comes back with one gesture (`@capacitor/browser` 8.0.3).

**Returns:** `open()` / `close()` → Promise `void`. Events: `browserFinished` (the user closed the tab) and `browserPageLoaded`. **Needs:** nothing - every app hosts the plugin (a browser that supports Custom Tabs, such as Chrome, must be installed).

```js
var Browser = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Browser;

async function openTerms() {
  if (!Browser) return;
  var h = await Browser.addListener('browserFinished', function () {
    console.log('user closed the terms');
    h.remove();
  });
  await Browser.open({ url: 'https://example.com/terms', toolbarColor: '#0B0F1A' });
}
```

**In an AI-built app** (a new tab on the web):

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

const off = browser.onClosed(() => refreshAccount());
await browser.open('https://example.com/terms', { toolbarColor: '#0B0F1A' });
// call off() when you no longer care
```

Open a link, and close the tab when a sign-in return reaches the page:

```js
var Browser = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Browser;

async function openHelp() {
  if (!Browser) { window.open('https://example.com/help', '_blank'); return; }
  try {
    await Browser.open({ url: 'https://example.com/help' });
  } catch (e) {
    alert('Could not open the page: ' + e.message);
  }
}

window.addEventListener('appmint:deep-link', function () {
  if (Browser) Browser.close().catch(function () {});
});
```

**Notes:** `url` must not be empty. `toolbarColor` is a hex colour; an invalid one is ignored. `close()` also works on Android in this plugin version (it finishes the tab's host activity) - useful after a sign-in link returns to the app. The tab is a separate browser: it does not share the app's cookies or `localStorage`.

### `Capacitor.Calendar`

```js
Capacitor.Plugins.Calendar — createEvent, findEvents, deleteEvent, listCalendars, openCalendar
```

**Example**

Adds, finds and deletes events in the phone's own calendar (Android CalendarContract), and opens the calendar app at a date. Vendored `@capacitor/calendar` (`template/plugins/calendar`).

**Returns:** `createEvent()` → `{ id }`; `findEvents()` → `{ events: [{ id, title, location?, notes?, startDate, endDate, isAllDay, calendarId, calendarName?, attendees? }] }`; `listCalendars()` → `{ calendars: [{ id, name, displayName?, isPrimary }] }`; `deleteEvent()`, `openCalendar()` → `void`. Dates are epoch milliseconds. **Needs:** turn on **Calendar (read & write events)** in the permissions step when you build (the build ticks it for you when it sees a calendar call - check it). The plugin asks the user for calendar access on first use.

```js
var Cal = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Calendar;

async function addDentist() {
  if (!Cal) return;
  var start = new Date('2026-10-02T10:00:00').getTime();
  try {
    var r = await Cal.createEvent({
      title: 'Dentist', startDate: start, endDate: start + 60 * 60 * 1000,
      location: 'Main St 5', notes: 'Bring the card', isAllDay: false,
      firstReminderMinutes: 30
    });
    console.log('event id', r.id);
  } catch (e) {
    // e.code: OS-PLUG-CLDR-0020 permission denied, -0001 bad arguments
    alert('Could not add the event: ' + e.message);
  }
}
```

**Read this week, delete one, open the calendar:**

```js
async function thisWeek() {
  if (!Cal) return;
  var now = Date.now();
  var found = await Cal.findEvents({ startDate: now, endDate: now + 7 * 864e5 });
  found.events.forEach(function (ev) { console.log(ev.title, new Date(ev.startDate)); });
  if (found.events.length) await Cal.deleteEvent({ id: found.events[0].id });
  await Cal.openCalendar({ date: now });
}
```

**In an AI-built app** (on the web `createEvent` downloads an `.ics` file; reading returns `[]`):

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

if ((await calendar.requestPermission()) === 'granted') {
  const id = await calendar.createEvent({ title: 'Dentist', start: new Date('2026-10-02T10:00'), end: new Date('2026-10-02T11:00') });
  const week = await calendar.listEvents({ from: Date.now(), to: Date.now() + 7 * 864e5 });
  if (id) await calendar.deleteEvent(id);
  await calendar.openCalendar(new Date());
}
```

**Notes:** Without the Calendar permission in the build, no dialog appears and every event call rejects (missing permission); a user who says no gets `OS-PLUG-CLDR-0020`. `recurrence: { frequency: 'daily' | 'weekly' | 'monthly' | 'yearly', interval?, count?, endDate? }` makes a repeating event. `createEventInteractively({ title, startDate, endDate })` opens the calendar app's own editor instead (no id comes back). The wrapper returns `null` / `[]` / `false` instead of throwing.

### `Capacitor.Camera`

```js
Capacitor.Plugins.Camera
```

**Example**

The official Capacitor Camera plugin, built into the app: take a photo with the camera or choose photos from the gallery.

**Returns:** Promises. `getPhoto(opts)` → `{ dataUrl | base64String | webPath, format, saved }` (which field depends on `resultType`), and REJECTS when the user cancels; `pickImages(opts)` → `{ photos: [{ webPath, format }] }`; `checkPermissions()` / `requestPermissions({ permissions })` → `{ camera, photos }`. **Needs:** **Camera** in Step 4 (Access) - the build turns it on when your code calls `Camera.getPhoto(`. `window.Capacitor` exists only inside the app.

**Take a photo and show it.**

```js
async function takePhoto() {
  var cam = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Camera;
  if (!cam) { showMessage('Open the app to use the camera.'); return; }
  try {
    var perm = await cam.requestPermissions({ permissions: ['camera'] });
    if (perm.camera !== 'granted') { showMessage('Camera permission was refused.'); return; }

    var photo = await cam.getPhoto({
      source: 'CAMERA',          // 'CAMERA' | 'PHOTOS' | 'PROMPT'
      resultType: 'dataUrl',     // 'dataUrl' | 'base64' | 'uri'
      quality: 80,
      direction: 'REAR',         // 'REAR' | 'FRONT'
      correctOrientation: true,
      saveToGallery: false
    });
    document.getElementById('photo').src = photo.dataUrl;
  } catch (err) {
    // The user closed the camera (or no camera app). Nothing to do.
  }
}
```

**Choose up to 5 photos from the gallery.**

```js
async function choosePhotos() {
  var cam = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Camera;
  if (!cam) return;
  try {
    var result = await cam.pickImages({ quality: 80, limit: 5 });
    result.photos.forEach(function (p) {
      var img = document.createElement('img');
      img.src = p.webPath;       // a URL the page can load
      document.getElementById('gallery').appendChild(img);
    });
  } catch (err) { /* cancelled */ }
}
```

**In an AI-built app** use the wrapper, which also works on the web (and resolves `null` instead of throwing on cancel):

```ts
import { camera } from '@/lib/appmintNative';
const photo = await camera.take({ source: 'camera' });
```

**Notes:** If **Camera** was off at build time, the camera permission is refused with no prompt. An `<input type="file" accept="image/*" capture>` also opens the camera. Camera is sensitive: declare it in your Play Data safety form.

### `Capacitor.Clipboard`

```js
Capacitor.Plugins.Clipboard — write(options), read()
```

**Example**

Copies text (or a link) to the system clipboard and reads it back (`@capacitor/clipboard` 8.0.1).

**Returns:** `write({ string?, url?, image?, label? })` → Promise `void`. `read()` → Promise `{ value, type }` (`type` is `'text/plain'` for text). **Needs:** nothing - every app hosts the plugin.

```js
async function copyCode(code) {
  var Clipboard = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Clipboard;
  if (!Clipboard) { alert('Copy is not available here'); return; }
  await Clipboard.write({ string: code, label: 'Invite code' });
}

async function pasteCode(input) {
  var Clipboard = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Clipboard;
  if (!Clipboard) return;
  try {
    var r = await Clipboard.read();
    input.value = r.value;
  } catch (e) {
    // An EMPTY clipboard is a rejection: "There is no data on the clipboard".
    if (/no data/i.test(e.message)) input.value = '';
    else alert('Could not read the clipboard: ' + e.message);
  }
}
```

**In an AI-built app** the Capacitor-shaped wrapper turns an empty clipboard into `''`:

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

await Clipboard.write({ string: 'ABC-123' });
const { value } = await Clipboard.read();   // '' when the clipboard is empty
```

Copy a link and confirm it with a tap of feedback:

```js
async function copyLink(url) {
  var P = window.Capacitor && window.Capacitor.Plugins;
  if (!P || !P.Clipboard) { alert('Copy is not available here'); return; }
  await P.Clipboard.write({ url: url });
  if (P.Haptics) P.Haptics.impact({ style: 'LIGHT' });
  copiedToast.hidden = false;
}
```

**Notes:** `write` needs one of `string`, `url` or `image` (a data URL), else it rejects "No data provided". Android 12+ shows its own "pasted from…" notice on read - that is the OS, not the app. The wrapper's `write` sends only `string || url`.

### `Capacitor.Device`

```js
Capacitor.Plugins.Device
```

**Example**

The official Capacitor Device plugin, built into the app: phone model, OS version, battery, a stable ID and the language.

**Returns:** Promises. `getInfo()` → `{ model, platform, operatingSystem, osVersion, androidSDKVersion, manufacturer, isVirtual, webViewVersion, … }`; `getId()` → `{ identifier }`; `getBatteryInfo()` → `{ batteryLevel (0-1), isCharging }`; `getLanguageCode()` → `{ value }`. **Needs:** nothing. `window.Capacitor` exists only inside the app.

**Show the phone and battery.**

```js
async function showDevice() {
  var cap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Device;
  if (!cap) { document.getElementById('out').textContent = navigator.userAgent; return; }
  try {
    var info = await cap.getInfo();
    var battery = await cap.getBatteryInfo();
    document.getElementById('out').textContent =
      info.manufacturer + ' ' + info.model + ' - ' + info.operatingSystem + ' ' + info.osVersion +
      ' - battery ' + Math.round(battery.batteryLevel * 100) + '%' + (battery.isCharging ? ' (charging)' : '');
  } catch (err) {
    document.getElementById('out').textContent = 'Could not read device info';
  }
}
```

**A stable ID and the language.**

```js
async function helloServer() {
  var cap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Device;
  if (!cap) return;
  var id = (await cap.getId()).identifier;
  var lang = (await cap.getLanguageCode()).value;     // e.g. "en"
  await fetch('https://api.example.com/hello?lang=' + encodeURIComponent(lang), {
    method: 'POST', body: JSON.stringify({ device: id })
  });
}
```

**In an AI-built app** use the wrapper, which also works on the web:

```ts
import { Device } from '@/lib/appmintNative';
const info = await Device.getInfo();
```

**Notes:** For the Android build ID, security patch, screen, memory and network, the shell's `WebToApk.getDeviceInfo()` gives more detail. Plugin ids are not hardware ids; they can change after a factory reset.

### `Capacitor.Dialog`

```js
Capacitor.Plugins.Dialog — alert, confirm, prompt
```

**Example**

Shows a real Android system dialog: a message, a yes/no question, or a one-line text question (`@capacitor/dialog` 8.0.1).

**Returns:** `alert({ title?, message, buttonTitle? })` → Promise `void`. `confirm({ title?, message, okButtonTitle?, cancelButtonTitle? })` → `{ value: boolean }`. `prompt({ title?, message, okButtonTitle?, cancelButtonTitle?, inputPlaceholder?, inputText? })` → `{ value: string, cancelled: boolean }`. **Needs:** nothing - every app hosts the plugin.

```js
var Dialog = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Dialog;

async function deleteNote(id) {
  if (!Dialog) return;
  var r = await Dialog.confirm({
    title: 'Delete note?',
    message: 'This cannot be undone.',
    okButtonTitle: 'Delete',
    cancelButtonTitle: 'Keep'
  });
  if (r.value) removeNote(id);
}
```

**Alert and prompt:**

```js
async function renameFlow() {
  if (!Dialog) return;
  await Dialog.alert({ title: 'Saved', message: 'Your changes are stored.', buttonTitle: 'OK' });

  var p = await Dialog.prompt({ title: 'Rename', message: 'New name', inputText: 'Groceries', inputPlaceholder: 'List name' });
  if (!p.cancelled && p.value.trim()) renameList(p.value.trim());
}
```

**In an AI-built app** the wrapper never rejects: dismissal is `{ value: false }` / `{ cancelled: true }`, and on the web it uses the browser's own dialogs:

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

const { value } = await Dialog.confirm({ title: 'Delete note?', message: 'This cannot be undone.' });
if (value) removeNote(id);
```

**Notes:** `message` is required - without it the plugin rejects "Please provide a message for the dialog". Dialogs are modal; use them only for decisions that must block.

### `Capacitor.Filesystem`

```js
Capacitor.Plugins.Filesystem
```

**Example**

The stock Capacitor Filesystem plugin: read and write files in the app's OWN storage (not a folder the user picked - for that use `WebToApkFS`).

**Returns:** every method returns a Promise and rejects on failure (for example reading a file that does not exist). `readFile` → `{ data }`, `writeFile` → `{ uri }`, `readdir` → `{ files: [{ name, type, size, mtime, uri }] }`, `mkdir` / `deleteFile` → nothing. **Needs:** nothing to switch on; `'DATA'` and `'CACHE'` need no permission.

A page has no `Directory` / `Encoding` imports, so pass the plain strings `'DATA'` (private app files, kept until uninstall) or `'CACHE'` (the system may clear it), and `'utf8'` for text:

```js
async function saveAndReadBack() {
  var Fs = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Filesystem;
  if (!Fs) { console.log('Filesystem plugin not available (not inside the app)'); return; }

  try {
    await Fs.mkdir({ path: 'notes', directory: 'DATA', recursive: true });
    await Fs.writeFile({ path: 'notes/today.txt', data: 'Hello', directory: 'DATA', encoding: 'utf8', recursive: true });

    var r = await Fs.readFile({ path: 'notes/today.txt', directory: 'DATA', encoding: 'utf8' });
    console.log('text:', r.data);

    var dir = await Fs.readdir({ path: 'notes', directory: 'DATA' });
    dir.files.forEach(function (f) { console.log(f.type, f.name, f.size); });   // type: 'file' | 'directory'

    await Fs.deleteFile({ path: 'notes/today.txt', directory: 'DATA' });
  } catch (e) {
    console.log('File error:', e.message);   // e.g. the file does not exist
  }
}
```

Leave out `encoding` to write or read binary data as base64:

```js
var Fs = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Filesystem;
var base64Data = btoa('raw bytes here');   // any base64 string, e.g. from a canvas or FileReader
if (Fs) {
  Fs.writeFile({ path: 'blobs/data.bin', data: base64Data, directory: 'CACHE', recursive: true })
    .then(function (w) { console.log('written to', w.uri); })
    .catch(function (e) { console.log('write failed:', e.message); });
}
```

In AI-built apps, the `@/lib/appmintNative` wrapper that uses this plugin is `fileTransfer` (it stages downloads and uploads in `'CACHE'`):

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

const r = await fileTransfer.download('https://example.com/report.pdf', 'report.pdf', {
  onProgress: (p) => console.log(Math.round(p.fraction * 100) + '%'),
});
if (r.path) console.log('saved in the app at', r.path);   // on the web the browser saves it instead
```

**Notes:** `window.Capacitor` exists only inside the app - always feature-detect. Files in `'DATA'` are private to the app and deleted on uninstall; to put files where the user can see them, use `WebToApkFS` (a folder the user grants) or a download.

### `Capacitor.FileTransfer`

```js
Capacitor.Plugins.FileTransfer
```

**Example**

The official Capacitor File Transfer plugin: downloads a URL to a file on the phone, or uploads a file from the phone, natively, with progress events.

**Returns:** `downloadFile({url, path, headers?, progress?})` → `Promise<{path}>`; `uploadFile({url, path, fileKey?, mimeType?, method?, headers?, progress?})` → `Promise<{bytesSent, responseCode, response, headers}>`. Progress arrives as `'progress'` listener events `{type, url, bytes, contentLength, lengthComputable}`. Failures reject with `{message, code}`. **Needs:** nothing to switch on. `window.Capacitor` exists only in pages the app itself serves - always feature-detect.

Download into the app's cache with progress:

```js
async function download(url, name, onPct) {
  const P = window.Capacitor && window.Capacitor.Plugins;
  if (!P || !P.FileTransfer || !P.Filesystem) return null;          // not in the app
  const target = await P.Filesystem.getUri({ path: name, directory: 'CACHE' });
  const listener = await P.FileTransfer.addListener('progress', function (p) {
    if (p.url === url && p.lengthComputable) onPct(Math.round(p.bytes / p.contentLength * 100));
  });
  try {
    const r = await P.FileTransfer.downloadFile({ url: url, path: target.uri, progress: true });
    return r.path;
  } catch (e) {
    alert('Download failed: ' + e.message);
    return null;
  } finally {
    listener.remove();
  }
}
```

Upload a file that is already on the phone:

```js
const P = window.Capacitor && window.Capacitor.Plugins;
if (P && P.FileTransfer) {
  P.FileTransfer.uploadFile({
    url: 'https://example.com/api/upload', path: localFileUri,
    fileKey: 'file', mimeType: 'image/jpeg', headers: { Authorization: 'Bearer ' + token }
  }).then(function (r) { console.log('server said', r.responseCode, r.response); })
    .catch(function (e) { alert('Upload failed: ' + e.message); });
}
```

Report a failed transfer by its code:

```js
async function safeDownload(url, target) {
  const P = window.Capacitor && window.Capacitor.Plugins;
  if (!P || !P.FileTransfer) { alert('Downloads need the app'); return null; }
  try {
    const r = await P.FileTransfer.downloadFile({ url: url, path: target });
    return r.path;
  } catch (e) {
    console.log('transfer failed', e.code, e.message);
    alert('Could not download the file.');
    return null;
  }
}
```

**Notes:** The path must be a file path or `file://` URI, not a Blob - write a Blob to the cache with `Filesystem.writeFile` first. On Android 10 and older, a path in public storage asks for storage permission. In AI-built apps prefer `fileTransfer` from `@/lib/appmintNative`, which does all of this and also works on the web.

### `Capacitor.FileViewer`

```js
Capacitor.Plugins.FileViewer
```

**Example**

The official Capacitor File Viewer plugin: opens a document or media file in the phone's viewer app, from a web address, a local file path, or a file bundled in the app.

**Returns:** each method returns a `Promise<void>` that rejects with `{message, code}` when nothing can open the file. **Needs:** nothing to switch on. `window.Capacitor` exists only in pages the app itself serves - always feature-detect.

```js
async function openPdf(url) {
  const FV = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.FileViewer;
  if (!FV) { window.open(url, '_blank'); return; }          // a normal browser: open a tab
  try {
    await FV.openDocumentFromUrl({ url: url });
  } catch (e) {
    alert('No app on this phone can open this file.');
  }
}
```

A file on the phone, for example one written with `Capacitor.Plugins.Filesystem`:

```js
async function openSaved(name) {
  const P = window.Capacitor && window.Capacitor.Plugins;
  if (!P || !P.FileViewer || !P.Filesystem) return;
  const f = await P.Filesystem.getUri({ path: name, directory: 'CACHE' });
  try {
    await P.FileViewer.openDocumentFromLocalPath({ path: f.uri.replace(/^file:\/\//, '') });
  } catch (e) {
    alert('Cannot open ' + name + ': ' + e.message);
  }
}
```

`openDocumentFromResources({path})` opens a file packed in the APK's own assets folder. The same three methods also exist as `previewMediaContentFromUrl`, `previewMediaContentFromLocalPath` and `previewMediaContentFromResources` for video and audio.

Download a file and open it when it arrives:

```js
async function downloadAndOpen(url, name) {
  const P = window.Capacitor && window.Capacitor.Plugins;
  if (!P || !P.FileTransfer || !P.Filesystem || !P.FileViewer) return;
  const f = await P.Filesystem.getUri({ path: name, directory: 'CACHE' });
  const r = await P.FileTransfer.downloadFile({ url: url, path: f.uri });
  await P.FileViewer.openDocumentFromLocalPath({ path: r.path.replace(/^file:\/\//, '') });
}
```

**Notes:** In AI-built apps prefer `fileViewer.open(target)` from `@/lib/appmintNative`, which picks the right method and also works on the web. A local path is a plain path (no `file://`).

### `Capacitor.Geolocation`

```js
Capacitor.Plugins.Geolocation
```

**Example**

The official Capacitor Geolocation plugin, built into the app: the phone's position once, or continuous updates.

**Returns:** Promises. `getCurrentPosition(opts)` → `{ timestamp, coords: { latitude, longitude, accuracy, altitude, speed, heading, … } }`; `watchPosition(opts, callback)` → the watch id (a string); `clearWatch({ id })`; `checkPermissions()` / `requestPermissions()` → `{ location, coarseLocation }` (`'granted' | 'denied' | 'prompt' | …`). **Needs:** **GPS** and/or **Location** in Step 4 (Access) - the build turns them on when your code calls `getCurrentPosition` or `watchPosition`. `window.Capacitor` exists only inside the app.

**Position once, asking first.**

```js
async function whereAmI() {
  var geo = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Geolocation;
  if (!geo) { showMessage('Open the app to use location.'); return; }
  try {
    var perm = await geo.checkPermissions();
    if (perm.location !== 'granted' && perm.coarseLocation !== 'granted') {
      perm = await geo.requestPermissions();
      if (perm.location !== 'granted' && perm.coarseLocation !== 'granted') {
        showMessage('Location is off for this app.');
        return;
      }
    }
    var pos = await geo.getCurrentPosition({ enableHighAccuracy: true, timeout: 15000 });
    showMessage(pos.coords.latitude.toFixed(5) + ', ' + pos.coords.longitude.toFixed(5));
  } catch (err) {
    showMessage('No location: ' + err.message);   // location off in settings, timeout, …
  }
}
```

**Continuous updates.** The callback gets `(position, error)`.

```js
var watchId = null;

async function startTracking() {
  var geo = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Geolocation;
  if (!geo) return;
  watchId = await geo.watchPosition({ enableHighAccuracy: true }, function (pos, err) {
    if (err || !pos) return;
    addPointToMap(pos.coords.latitude, pos.coords.longitude);
  });
}

async function stopTracking() {
  var geo = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Geolocation;
  if (geo && watchId) { await geo.clearWatch({ id: watchId }); watchId = null; }
}
```

**In an AI-built app** use the wrapper, which also works on the web:

```ts
import { Geolocation } from '@/lib/appmintNative';
const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true });
```

**Notes:** Plain `navigator.geolocation` works in the app too. If the location switches were off at build time, the permission is refused with no prompt. On Android 12+ the user may allow only approximate location (`coarseLocation` granted). Location is personal data: declare it in your Play Data safety form.

### `Capacitor.Haptics`

```js
Capacitor.Plugins.Haptics
```

**Example**

The official Capacitor Haptics plugin (`@capacitor/haptics`), built into the app: light/medium/heavy taps, success/warning/error feedback, selection ticks and plain vibration.

**Returns:** every method returns a Promise that resolves with nothing. **Needs:** turn on **Vibrate** in Step 4 (Access) when you build; the plugin cannot vibrate without that permission. `window.Capacitor` exists only inside the app, so feature-detect.

```js
function haptics() {
  return window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins.Haptics : null;
}

async function tap(style) {            // 'LIGHT' | 'MEDIUM' | 'HEAVY'
  var h = haptics();
  if (!h) return;
  try { await h.impact({ style: style }); } catch (e) { console.log('haptics failed', e); }
}

async function result(type) {          // 'SUCCESS' | 'WARNING' | 'ERROR'
  var h = haptics();
  if (h) { try { await h.notification({ type: type }); } catch (e) {} }
}

document.getElementById('add').addEventListener('click', function () { tap('LIGHT'); });
document.getElementById('pay').addEventListener('click', function () { result('SUCCESS'); });
```

Selection ticks for a picker or slider, and a plain vibration:

```js
var h = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins.Haptics : null;
var wheel = document.getElementById('wheel');

wheel.addEventListener('pointerdown', function () { if (h) h.selectionStart(); });
wheel.addEventListener('change', function () { if (h) h.selectionChanged(); });
wheel.addEventListener('pointerup', function () { if (h) h.selectionEnd(); });

document.getElementById('buzz').addEventListener('click', function () {
  if (h) h.vibrate({ duration: 300 });   // milliseconds
});
```

Buzz on an error and pair it with a visible message:

```js
var h = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics;

function rejectInput(field) {
  field.classList.add('invalid');
  if (h) h.notification({ type: 'ERROR' }).catch(function () {});
}
```

**Notes:** The strings are upper case (`'HEAVY'`, not `'heavy'`). In AI-built apps, `import { Haptics } from '@/lib/appmintNative'` gives the same names. For custom rhythms with strength per beat, use `Capacitor.Plugins.AppwrightHaptics`.

### `Capacitor.InAppReview`

```js
Capacitor.Plugins.InAppReview — requestReview()
```

**Example**

Asks Google Play to show its own "rate this app" sheet inside the app. The `@capacitor-community/in-app-review` API, answered by the app's own review code (the same as `AppMint.requestReview()`).

**Returns:** a Promise that resolves once with `{result, openedStore, message?}`: `result` is `'shown'` (Play put its sheet up - it never says whether the user rated), `'not_from_play'` (a sideloaded or test install, where Play never shows the sheet), `'quota'` (Play chose not to show it now) or `'error'`. `{openStore: true}` opens the Play listing when the sheet did not show. Code that ignores the value keeps working. **Needs:** nothing to switch on; the app must be installed from Google Play for the sheet to appear.

```js
var Review = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.InAppReview;

async function afterThirdOrder() {
  if (!Review) return;
  var r = await Review.requestReview();
  console.log('review:', r.result);   // 'shown' | 'not_from_play' | 'quota' | 'error'
  // continue the app flow either way — you cannot know if the user rated
}
```

**In an AI-built app:**

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

// after a success moment, not on launch
const r = await askForReview();   // r.result: 'shown' | 'not_from_play' | 'quota' | 'error' | 'unavailable' (web)
```

Ask once, after a success moment, and remember that you asked:

```js
var Review = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.InAppReview;

async function maybeAskForReview() {
  if (!Review || localStorage.getItem('review_asked')) return;
  localStorage.setItem('review_asked', '1');
  try { await Review.requestReview(); } catch (e) { console.log('review not shown:', e.message); }
}
```

**Notes:** Play limits how often the sheet appears and does not show it for every call - never tie a reward to it and never ask from a "Rate us" button expecting it to open (open the store listing for that). Ask after a success moment, not at startup. A sideloaded or debug install always answers `not_from_play`.

### `Capacitor.KeepAwake`

```js
Capacitor.Plugins.KeepAwake — keepAwake(), allowSleep(), isKeptAwake(), isSupported()
```

**Example**

Stops the screen from dimming and locking while something must stay visible - a recipe, a route, a QR code, a timer. Vendored `@capacitor-community/keep-awake` (`template/plugins/keep-awake`).

**Returns:** `keepAwake()` / `allowSleep()` → Promise `void`. `isKeptAwake()` → `{ isKeptAwake: boolean }`. `isSupported()` → `{ isSupported: true }`. **Needs:** nothing - every app hosts the plugin; no permission.

```js
var KA = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.KeepAwake;

async function startCooking() {
  if (KA) await KA.keepAwake();
  showSteps();
}

async function stopCooking() {
  if (KA) await KA.allowSleep();      // always release when the view closes
}

if (KA) KA.isKeptAwake().then(function (r) { console.log('awake?', r.isKeptAwake); });
```

**In an AI-built app** (the Screen Wake Lock API on the web):

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

useEffect(() => {
  void keepAwake(true);
  return () => { void keepAwake(false); };
}, []);
```

Keep the screen on only while a timer runs:

```js
var KA = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.KeepAwake;

async function runTimer(seconds) {
  if (KA) await KA.keepAwake();
  await new Promise(function (done) { setTimeout(done, seconds * 1000); });
  if (KA) await KA.allowSleep();
  alert('Time is up');
}
```

**Notes:** The flag belongs to the app window: it only works while the app is in front, and it stays on until `allowSleep()` - release it on every exit from the screen. `keepAwake()` returns `false` when nothing could keep the screen on.

### `Capacitor.Keyboard`

```js
Capacitor.Plugins.Keyboard — show(), hide(), addListener('keyboardDidShow' | ...)
```

**Example**

Opens or closes the software keyboard and tells the page when it appears and how tall it is (`@capacitor/keyboard` 8.0.5).

**Returns:** `show()` / `hide()` → Promise `void`. `addListener(event, fn)` → Promise of a handle with `remove()`; show events pass `{ keyboardHeight }` in CSS pixels. Events: `keyboardWillShow`, `keyboardDidShow`, `keyboardWillHide`, `keyboardDidHide`. **Needs:** nothing - every app hosts the plugin.

```js
var Keyboard = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Keyboard;

if (Keyboard) {
  Keyboard.addListener('keyboardDidShow', function (info) {
    console.log('keyboard height', info.keyboardHeight);
    document.body.classList.add('typing');       // e.g. hide the bottom tab bar
  });
  Keyboard.addListener('keyboardDidHide', function () {
    document.body.classList.remove('typing');
  });
}

function onSend() {
  if (Keyboard) Keyboard.hide().catch(function () {});   // rejects if no field is focused
}
```

**In an AI-built app** use `onKeyboard()` for layout - it reports only the part of the keyboard the resized page does not already clear - and `hideKeyboard()` to close it:

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

const off = onKeyboard((px) => { composer.style.paddingBottom = `${px}px`; });  // call off() on unmount
hideKeyboard();

const h = Keyboard.addListener('keyboardWillShow', ({ keyboardHeight }) => console.log(keyboardHeight));
h.remove();
```

Show the keyboard for a field after a button tap:

```js
var Keyboard = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Keyboard;

searchBtn.addEventListener('click', function () {
  searchInput.focus();
  if (Keyboard) Keyboard.show().catch(function () {});
});
```

**Notes:** The shell already resizes the page for the keyboard, so do NOT add the full `keyboardHeight` as padding - a bottom bar is already above the keys. `setAccessoryBarVisible`, `setStyle`, `setResizeMode` and `setScroll` are iOS-only. `hide()` rejects "Can't close keyboard, not currently focused" when nothing has focus.

### `Capacitor.Network`

```js
Capacitor.Plugins.Network — getStatus(), addListener('networkStatusChange')
```

**Example**

Tells the page whether the phone is online and on which kind of connection, and when that changes (`@capacitor/network` 8.0.1).

**Returns:** `getStatus()` → Promise `{ connected: boolean, connectionType: 'wifi' | 'cellular' | 'none' | 'unknown' }`. The `networkStatusChange` event passes the same shape. **Needs:** nothing - every app hosts the plugin.

```js
var Network = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Network;

function showBanner(s) {
  document.getElementById('offline').hidden = s.connected;
}

if (Network) {
  Network.getStatus().then(showBanner);
  Network.addListener('networkStatusChange', function (s) {
    showBanner(s);
    if (s.connected) retryQueuedWrites();
    if (s.connectionType === 'cellular') console.log('on mobile data');
  });
}
```

**In an AI-built app:**

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

const { connected, connectionType } = await Network.getStatus();
const now = networkStatus();                        // sync: { connected, type }
const off = onNetworkChange((s) => setOnline(s.connected));   // call off() on unmount
```

Pause a large upload on mobile data:

```js
var Network = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Network;

async function canUploadVideo() {
  if (!Network) return navigator.onLine;
  var s = await Network.getStatus();
  if (!s.connected) return false;
  return s.connectionType === 'wifi' || confirm('Upload over mobile data?');
}
```

**Notes:** `connected` means the phone has a network, not that your server answers - still handle failed requests. The wrapper's `Network.getStatus()` and `networkStatus()` read `navigator.onLine` / `navigator.connection`, not the plugin, so their type can be `'unknown'` or a speed class like `'4g'`; `onNetworkChange()` does use the plugin's event in the app.

### `Capacitor.Preferences`

```js
Capacitor.Plugins.Preferences — get / set / remove / keys / clear
```

**Example**

A small native key-value store (Android SharedPreferences) for strings (`@capacitor/preferences` 8.0.1).

**Returns:** `set({ key, value })`, `remove({ key })`, `clear()` → Promise `void`; `get({ key })` → `{ value }` (`null` when the key is missing); `keys()` → `{ keys: string[] }`. **Needs:** nothing - every app hosts the plugin.

```js
var Prefs = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Preferences;

async function saveTheme(name) {
  if (!Prefs) return false;          // not inside the app
  await Prefs.set({ key: 'theme', value: name });
  return true;
}

async function loadTheme() {
  if (!Prefs) return null;
  var r = await Prefs.get({ key: 'theme' });
  return r.value;            // null if never saved
}
```

Objects must be turned into a string first:

```js
async function saveUser() {
  if (!Prefs) return;
  await Prefs.set({ key: 'user', value: JSON.stringify({ id: 7, name: 'Asha' }) });
  var u = JSON.parse((await Prefs.get({ key: 'user' })).value || 'null');
  var all = (await Prefs.keys()).keys;
  await Prefs.remove({ key: 'user' });
}
```

List and clear the saved keys from a settings screen:

```js
async function resetSettings() {
  if (!Prefs) return;
  var k = (await Prefs.keys()).keys;
  console.log('clearing', k.length, 'saved values');
  await Prefs.clear();
}
```

**Notes:** `@/lib/appmintNative` has NO Preferences wrapper, on purpose: `localStorage` is already real and persistent in the app, and a second store would split one app's data across two places. AI-built apps should keep using `localStorage` (or the data runtime). Use Preferences in a hand-written page only if you want all of your settings there, and pick one store per value. `configure({ group })` changes the store name (default `CapacitorStorage`).

### `Capacitor.PrivacyScreen`

```js
Capacitor.Plugins.PrivacyScreen — enable(), disable(), isEnabled()
```

**Example**

The official `@capacitor/privacy-screen` plugin: blocks screenshots and screen recording and hides the app's content in the recent-apps view. Baked so imported Capacitor projects that call it work.

**Returns:** `enable({android})` → `{success}`; `disable()` → `{success}`; `isEnabled()` → `{enabled}`. **Needs:** nothing to switch on.

```js
const Privacy = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.PrivacyScreen;

async function openVault() {
  if (Privacy) await Privacy.enable({ android: { privacyModeOnActivityHidden: 'dim' } });
  showVault();
}
async function closeVault() {
  if (Privacy) await Privacy.disable();
}
```

Check the current state before toggling:

```js
async function toggleProtection() {
  if (!Privacy) return;
  var s = await Privacy.isEnabled();
  if (s.enabled) await Privacy.disable();
  else await Privacy.enable();
}
```

Protect the screen only while a sensitive view is open:

```js
async function showCardNumber() {
  if (Privacy) await Privacy.enable({ android: { privacyModeOnActivityHidden: 'splash' } });
  cardBox.hidden = false;
}
async function hideCardNumber() {
  cardBox.hidden = true;
  if (Privacy) await Privacy.disable();
}
```

**Notes:** `privacyModeOnActivityHidden`: `'none'`, `'dim'` or `'splash'`. Code written for the app itself should use `WebToApk.setSecureScreen(true)` / `privacyScreen.enable()` from `@/lib/appmintNative`, which follows the page (released when the app leaves it); mixing both, the last call wins.

### `Capacitor.ScreenBrightness`

```js
Capacitor.Plugins.ScreenBrightness — getBrightness(), setBrightness({ brightness })
```

**Example**

Sets the app window's screen brightness - full brightness for a boarding pass or QR code, then back. Vendored `@capacitor-community/screen-brightness` (`template/plugins/screen-brightness`).

**Returns:** `getBrightness()` → Promise `{ brightness }` (0-1, or `-1` when the app has not overridden it and the screen follows the system). `setBrightness({ brightness })` → Promise `void`. **Needs:** nothing - every app hosts the plugin; no permission (it changes only this app's window, not the system setting).

```js
var SB = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenBrightness;

async function showTicket() {
  if (SB) await SB.setBrightness({ brightness: 1 });     // full brightness
  document.getElementById('qr').hidden = false;
}

async function hideTicket() {
  if (SB) await SB.setBrightness({ brightness: -1 });    // -1 = follow the system again
  document.getElementById('qr').hidden = true;
}
```

**In an AI-built app:**

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

await brightness.set(1);        // clamped to 0–1; false where not available (web)
// ... on leaving the screen
await brightness.restore();
const level = await brightness.get();   // -1 = following the system / not available
```

Read the current level first and restore exactly that:

```js
var SB = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenBrightness;
var before = -1;

async function boost() {
  if (!SB) return;
  before = (await SB.getBrightness()).brightness;
  await SB.setBrightness({ brightness: 1 });
}
async function undo() {
  if (SB) await SB.setBrightness({ brightness: before });
}
```

**Notes:** Always pass a number - `setBrightness({})` has no value to apply. Restore with `-1` when the view closes; the override also ends when the app closes. A web page cannot drive the backlight, so `can('brightness')` is `'none'` in a browser.

### `Capacitor.ScreenOrientation`

```js
Capacitor.Plugins.ScreenOrientation — orientation(), lock({ orientation }), unlock()
```

**Example**

Reads the screen orientation, pins it for one view (a game, a chart, a signature pad) and releases it (`@capacitor/screen-orientation` 8.0.1).

**Returns:** `orientation()` → Promise `{ type }` (e.g. `'portrait-primary'`). `lock()` / `unlock()` → Promise `void`. Event `screenOrientationChange` passes `{ type }`. **Needs:** nothing - every app hosts the plugin.

`orientation` values: `'portrait'`, `'landscape'`, `'portrait-primary'`, `'portrait-secondary'`, `'landscape-primary'`, `'landscape-secondary'`, `'natural'`, `'any'`.

```js
var SO = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenOrientation;

async function openChart() {
  if (SO) await SO.lock({ orientation: 'landscape' });
  showChart();
}

async function closeChart() {
  if (SO) await SO.unlock();          // always release when leaving the view
  hideChart();
}

if (SO) {
  SO.orientation().then(function (o) { console.log('now', o.type); });
  SO.addListener('screenOrientationChange', function (o) { console.log('rotated to', o.type); });
}
```

**In an AI-built app:**

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

const ok = await lockOrientation('landscape');   // false when refused (always on desktop)
await lockOrientation('any');                    // release

await ScreenOrientation.lock({ orientation: 'portrait' });
await ScreenOrientation.unlock();
```

**Handle a refused lock and tell the user:**

```js
var SO = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenOrientation;
var Toast = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Toast;

async function tryLandscape() {
  if (!SO) { document.getElementById('rotate-hint').hidden = false; return; }   // ask the user to rotate by hand
  try {
    await SO.lock({ orientation: 'landscape' });
  } catch (e) {
    if (Toast) Toast.show({ text: 'Could not rotate the screen', duration: 'short' });
  }
}
```

**Notes:** The wrappers only know `portrait` / `landscape` / `any`: `ScreenOrientation.lock({ orientation: 'landscape-secondary' })` locks to plain `landscape`. Give the user a way out of a locked view.

### `Capacitor.ScreenReader`

```js
Capacitor.Plugins.ScreenReader — isEnabled(), speak({ value, language }), addListener('stateChange')
```

**Example**

Tells the page whether TalkBack is on, and speaks an announcement (`@capacitor/screen-reader` 8.0.1).

**Returns:** `isEnabled()` → Promise `{ value: boolean }`. `speak()` → Promise `void`. The `stateChange` event passes `{ value: boolean }` when TalkBack is switched on or off. **Needs:** nothing - every app hosts the plugin.

```js
var SR = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenReader;

function setCalmMode(on) {
  document.documentElement.classList.toggle('no-motion', on);   // shorten animations
}

if (SR) {
  SR.isEnabled().then(function (r) { setCalmMode(r.value); });
  SR.addListener('stateChange', function (s) { setCalmMode(s.value); });
}

async function announceSaved() {
  if (SR) await SR.speak({ value: 'Order placed', language: 'en' });
}
```

**In an AI-built app** (`speak` uses text-to-speech on the web; `isEnabled()` is `false` there):

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

const on = await screenReader.isEnabled();
const off = screenReader.onChange((enabled) => setCalmMode(enabled));
await screenReader.speak('Order placed', 'en');
```

**Feature-detect and keep the page usable without the plugin:**

```js
var SR = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.ScreenReader;

async function say(message) {
  if (SR) { await SR.speak({ value: message }); return; }
  document.getElementById('live-region').textContent = message;   // an aria-live="polite" element
}
```

**Notes:** `language` defaults to `'en'`. Prefer normal accessible markup (labels, `aria-live` regions) for most announcements; use `speak` for short, important state changes. There is no web API to detect a screen reader, so `can('screenReader')` is `'none'` in a browser.

### `Capacitor.Share`

```js
Capacitor.Plugins.Share — share(options), canShare()
```

**Example**

Opens the Android share sheet with a title, text and/or a link (`@capacitor/share` 8.0.1).

**Returns:** `share()` → Promise `{ activityType }` (the chosen app's component name, may be `''`); it REJECTS when the user closes the sheet. `canShare()` → Promise `{ value: boolean }`. **Needs:** nothing - every app hosts the plugin.

```js
async function shareLink() {
  var Share = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Share;
  if (!Share) { alert('Sharing is not available here'); return; }
  // Only pass the fields you have. A present but empty `url` is rejected ("Unsupported url").
  var opts = { title: 'My list', text: 'Milk, eggs, bread' };
  var link = 'https://example.com/list/42';
  if (link) opts.url = link;
  try {
    var r = await Share.share(opts);
    console.log('shared to', r.activityType);
  } catch (e) {
    console.log('share cancelled or failed:', e.message);   // "Share canceled" on dismiss
  }
}
```

**In an AI-built app** the wrapper has the same shape and also rejects on cancel:

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

if (can('share') !== 'none') {
  try {
    await Share.share({ title: 'My list', text: 'Milk, eggs, bread', url: 'https://example.com/list/42' });
  } catch {
    // the user closed the sheet — not an error to show
  }
}
```

**Check `canShare()` first and hide the button when sharing is not available:**

```js
var Share = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Share;
var btn = document.getElementById('share');

btn.hidden = true;
if (Share) {
  Share.canShare().then(function (r) { btn.hidden = !r.value; });
}
```

**Notes:** `url` must be `http(s)://` or `file://`. `dialogTitle` sets the chooser heading. Sharing files needs `file://` paths the app owns; from a page, share files with `navigator.share({ files })` instead. Only one share can be open at a time ("Can't share while sharing is in progress").

### `Capacitor.SpeechRecognition`

```js
Capacitor.Plugins.SpeechRecognition
```

**Example**

Turns the user's speech into text with Android's speech recognizer (the community `@capacitor-community/speech-recognition` plugin, built into the app). Android WebView has no Web Speech recognition of its own, so this is the way to do dictation.

**Returns:** Promises. `available()` → `{ available }`. `checkPermissions()` / `requestPermissions()` → `{ speechRecognition: 'granted' | 'denied' | 'prompt' }`. `start({ partialResults: false })` → `{ status: 'success', matches: [...] }` after the user stops talking; it rejects on errors such as no speech or no match. **Needs:** turn on **Mic** in Step 4 (Access) when you build, and the user must allow the microphone.

One-shot dictation (the same way `@/lib/appmintNative` `speech` uses it):

```js
var SR = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins.SpeechRecognition : null;

async function dictate() {
  var out = document.getElementById('text');
  if (!SR || !(await SR.available()).available) { out.textContent = 'Voice input is not available on this phone.'; return; }

  var p = (await SR.checkPermissions()).speechRecognition;
  if (p !== 'granted') p = (await SR.requestPermissions()).speechRecognition;
  if (p !== 'granted') { out.textContent = 'Microphone permission was not given.'; return; }

  try {
    var r = await SR.start({ language: 'en-US', maxResults: 3, partialResults: false, popup: false });
    out.textContent = r.matches && r.matches.length ? r.matches[0] : '';
  } catch (e) {
    out.textContent = 'Did not catch that. Try again.';
  }
}

document.getElementById('mic').addEventListener('click', dictate);
```

Live text while the user speaks (`partialResults: true`), and the listening state:

```js
// Add the listeners ONCE, when the page starts.
if (SR) {
  SR.addListener('partialResults', function (d) {
    // Words as they come, and then the final text, all arrive here.
    if (d.matches && d.matches.length) document.getElementById('text').textContent = d.matches[0];
  });
  SR.addListener('listeningState', function (d) {
    // 'started' = the user began speaking, 'stopped' = the user stopped speaking
    document.body.classList.toggle('listening', d.status === 'started');
  });
}

document.getElementById('mic-live').addEventListener('click', async function () {
  if (!SR) return;
  try {
    await SR.start({ language: 'en-US', partialResults: true, popup: false });   // resolves at once, no matches
  } catch (e) {
    document.getElementById('text').textContent = 'Could not start: ' + e.message;
  }
});

document.getElementById('stop').addEventListener('click', function () { if (SR) SR.stop(); });
```

**Show the mic button only when dictation can work, and stop listening when the page is hidden:**

```js
var SR = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins.SpeechRecognition : null;
var mic = document.getElementById('mic');

mic.hidden = true;
if (SR) {
  SR.available().then(function (r) { mic.hidden = !r.available; });
}

document.addEventListener('visibilitychange', function () {
  if (document.hidden && SR) SR.stop();
});
```

**Notes:**
- In live mode (`partialResults: true`), the final text arrives **after** the `'stopped'` state, so do not remove the `partialResults` listener on `'stopped'`. An error after the start (no speech, no match) is not reported in this mode.
- `language` is a tag like `'en-US'` or `'ta-IN'` (default: the phone's language). `popup: true` shows Google's own listening dialog instead. `getSupportedLanguages()` → `{ languages }` and `isListening()` → `{ listening }` also exist.
- `start()` rejects with `'Missing permission'` if you did not ask first. Listening stops by itself after a pause in speech.

### `Capacitor.SplashScreen`

```js
Capacitor.Plugins.SplashScreen — show(), hide()
```

**Example**

The `@capacitor/splash-screen` API over the app's own branded launch splash, so imported Capacitor / AI Studio projects that call `SplashScreen.hide()` once their first screen is ready work in the Android app.

**Returns:** `hide()` → Promise (the splash goes once the creator's minimum splash time has passed); `show({showDuration, autoHide})` → Promise (the splash comes back over the page and, with `autoHide` - the default - goes after `showDuration` ms, default 3000). **Needs:** nothing to switch on.

```js
const Splash = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.SplashScreen;

async function boot() {
  await loadFirstScreen();
  if (Splash) await Splash.hide();     // otherwise the shell hides it at first paint
}
boot();
```

Cover a slow account switch with the splash, kept up until the new data is on screen:

```js
const Splash = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.SplashScreen;

async function switchAccount(accountId) {
  if (Splash) await Splash.show({ autoHide: false });   // stays until hide()
  try {
    await loadAccount(accountId);
    renderHome();
  } finally {
    if (Splash) await Splash.hide();
  }
}
```

Show the branded splash for a moment after signing out; it goes by itself after `showDuration`:

```js
const Splash = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.SplashScreen;

async function signOut() {
  await clearSession();
  showSignInScreen();
  if (Splash) {
    try {
      await Splash.show({ showDuration: 1200 });
    } catch (e) {
      console.warn('splash not shown:', e.code);
    }
  }
}
```

**Notes:** without a `hide()` call the splash still goes at the first paint, like every app. AI-built apps use `splash.hold()` / `splash.ready()` from `@/lib/appmintNative`.

### `Capacitor.StatusBar`

```js
Capacitor.Plugins.StatusBar — setStyle, setBackgroundColor, show, hide, getInfo
```

**Example**

Changes the status bar's icon style, colour and visibility (`@capacitor/status-bar` 8.0.2).

**Returns:** every method → Promise `void`, except `getInfo()` → `{ visible, style, color, overlays, height }`. **Needs:** nothing - every app hosts the plugin.

`style` names the BACKGROUND it suits: `'DARK'` = light icons (for a dark bar), `'LIGHT'` = dark icons, `'DEFAULT'` = follow the system.

```js
var StatusBar = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.StatusBar;
if (StatusBar) {
  StatusBar.setStyle({ style: 'DARK' });          // light icons
  StatusBar.hide();                               // e.g. while a video plays
  // later
  StatusBar.show();
}
```

**Colour the bar with the shell bridge, not the plugin.** On Android 15+ the plugin's `setBackgroundColor` does nothing (edge-to-edge ignores it). The shell paints the strip itself and takes the icon style in the same call:

```js
if (window.WebToApk && window.WebToApk.setStatusBarColor) {
  window.WebToApk.setStatusBarColor('#0B0F1A', 'light');   // dark bar, light icons
} else if (StatusBar) {
  StatusBar.setBackgroundColor({ color: '#0B0F1A' });      // hex only, e.g. #RRGGBB
}
```

**In an AI-built app** use `setStatusBar(color, contentStyle)`; it prefers the shell bridge and sets colour and icons in one call:

```ts
import { setStatusBar, StatusBar } from '@/lib/appmintNative';

setStatusBar('#0B0F1A', 'light');                       // dark bar, light icons
await StatusBar.setBackgroundColor({ color: '#FFFFFF' }); // colour only; icons picked from luminance
```

**Notes:** `setBackgroundColor` rejects a colour that is not a hex string. In fullscreen / kiosk builds the bars are hidden by the shell. In the Android app the wrapper's `StatusBar.setStyle()` alone changes nothing (the bridge needs a colour to paint) - pass the style to `setStatusBar()` together with a colour.

### `Capacitor.TextToSpeech`

```js
Capacitor.Plugins.TextToSpeech — speak(), stop(), getSupportedLanguages(), getSupportedVoices()
```

**Example**

The community `@capacitor-community/text-to-speech` plugin over the phone's text-to-speech engine. Baked so imported Capacitor projects that call it work.

**Returns:** `speak({text, lang, rate, pitch, volume, voice})` → Promise that resolves when the sentence has been spoken; `stop()`; `getSupportedLanguages()` → `{languages}`; `getSupportedVoices()` → `{voices}`; `isLanguageSupported({lang})` → `{supported}`. **Needs:** nothing to switch on.

```js
const Tts = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.TextToSpeech;

async function readAloud(text) {
  if (!Tts) { speechSynthesis.speak(new SpeechSynthesisUtterance(text)); return; }
  await Tts.speak({ text: text, lang: 'en-US', rate: 1.0, pitch: 1.0, volume: 1.0 });
}
document.getElementById('stop').onclick = () => Tts && Tts.stop();
```

**Check the language before speaking and show a message when it is not installed:**

```js
const Tts = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.TextToSpeech;

async function speakIn(lang, text) {
  if (!Tts) return false;
  const { supported } = await Tts.isLanguageSupported({ lang: lang });
  if (!supported) { showMessage('This phone has no ' + lang + ' voice installed.'); return false; }
  await Tts.speak({ text: text, lang: lang });
  return true;
}
```

**Read a paragraph aloud with a stop button, and handle a speak error:**

```js
const Tts = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.TextToSpeech;

async function readParagraph(el, btn) {
  if (!Tts) return;
  btn.textContent = 'Stop';
  btn.onclick = () => Tts.stop();
  try {
    await Tts.speak({ text: el.textContent, lang: 'en-US', rate: 0.9 });   // resolves when finished
  } catch (e) {
    console.warn('could not speak:', e.message);
  } finally {
    btn.textContent = 'Listen';
    btn.onclick = () => readParagraph(el, btn);
  }
}
```

**Notes:** `window.speechSynthesis` works in the app too (the shell's own engine) and is what AI-built apps use through `speak()`; this plugin is the Capacitor-shaped path.

### `Capacitor.TextZoom`

```js
Capacitor.Plugins.TextZoom — get(), getPreferred(), set({ value })
```

**Example**

Reads and changes the text size of the app's page, for an in-app "text size" control (`@capacitor/text-zoom` 8.0.1).

**Returns:** `get()` → Promise `{ value }` (current zoom, `1` = 100%). `getPreferred()` → `{ value }` (the size the user picked in Android settings). `set({ value })` → Promise `void`. **Needs:** nothing - every app hosts the plugin.

```js
var TZ = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.TextZoom;

async function biggerText() {
  if (!TZ) return;
  var now = (await TZ.get()).value;
  await TZ.set({ value: Math.min(2, now + 0.1) });
}

async function resetText() {
  if (!TZ) return;
  var pref = (await TZ.getPreferred()).value;   // e.g. 1.15 when the phone uses large text
  await TZ.set({ value: pref });
}
```

**In an AI-built app** (on the web it scales the root font size instead):

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

const size = await textZoom.get();
await textZoom.set(size + 0.1);                     // clamped to 0.5–3
await textZoom.set(await textZoom.getPreferred());  // back to the system size
```

**A text size slider that remembers the choice:**

```js
var TZ = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.TextZoom;
var slider = document.getElementById('text-size');   // <input type="range" min="0.8" max="1.6" step="0.1">

if (TZ) {
  var saved = parseFloat(localStorage.getItem('textZoom'));
  if (saved) TZ.set({ value: saved });
  slider.addEventListener('change', function () {
    TZ.set({ value: parseFloat(slider.value) });
    localStorage.setItem('textZoom', slider.value);
  });
} else {
  slider.hidden = true;
}
```

**Notes:** The app already follows the system text size; this is for an extra in-app control. Test your layout at large sizes - fixed heights on text controls will clip.

### `Capacitor.Toast`

```js
Capacitor.Plugins.Toast — show({ text, duration, position })
```

**Example**

Shows a short Android system toast such as "Saved" or "Copied" (`@capacitor/toast` 8.0.1).

**Returns:** Promise `void`. **Needs:** nothing - every app hosts the plugin.

```js
var Toast = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Toast;

function saved() {
  if (Toast) Toast.show({ text: 'Saved', duration: 'short', position: 'bottom' });
}
```

`duration` is `'short'` (about 2 s) or `'long'` (about 3.5 s). `position` is `'top'`, `'center'` or `'bottom'`.

**In an AI-built app** use `toast()` or the Capacitor-shaped `Toast`; on the web they draw a snackbar in the app's colours:

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

await Toast.show({ text: 'Added to list', duration: 'long' });
toast('Copied');   // short form, same result
```

**Feature-detect and show an in-page message when the plugin is missing:**

```js
function notify(message) {
  var Toast = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Toast;
  if (Toast) { Toast.show({ text: message, duration: 'short' }); return; }
  var el = document.getElementById('snackbar');   // a plain in-page message
  el.textContent = message;
  el.hidden = false;
  setTimeout(function () { el.hidden = true; }, 2000);
}
```

**Notes:** `text` is required ("Must provide text"). The system draws the toast in the phone maker's style, and newer Android versions ignore `position` for text toasts. A toast is easy to miss - never use it for an error the user must act on; use `Dialog.alert` for that.

### `Capacitor.Torch`

```js
Capacitor.Plugins.Torch — enable(), disable(), toggle(), isEnabled(), isAvailable()
```

**Example**

The phone's flashlight, with the `@capawesome/capacitor-torch` API. Appwright's own plugin (`TorchPlugin.kt`).

**Returns:** `enable()` / `disable()` / `toggle()` → Promise (rejects `CAMERA_IN_USE` while the app's own camera stream is open, `UNAVAILABLE` without a flash); `isEnabled()` → `{enabled}`; `isAvailable()` → `{available}`. **Needs:** nothing to switch on and no permission.

```js
const Torch = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Torch;

async function setupLightButton() {
  if (!Torch || !(await Torch.isAvailable()).available) { lightBtn.hidden = true; return; }
  lightBtn.onclick = async () => {
    try {
      await Torch.toggle();
      lightBtn.classList.toggle('on', (await Torch.isEnabled()).enabled);
    } catch (e) {
      alert(e.message);
    }
  };
}
```

Flash an SOS signal from a button, and switch the light off if the camera takes the flash midway:

```js
const Torch = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Torch;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

sosBtn.onclick = async () => {
  if (!Torch || !(await Torch.isAvailable()).available) return;
  const flashes = [200, 200, 200, 600, 600, 600, 200, 200, 200];   // S O S, in ms
  try {
    for (const ms of flashes) {
      await Torch.enable();  await wait(ms);
      await Torch.disable(); await wait(200);
    }
  } catch (e) {
    await Torch.disable().catch(() => {});
    alert(e.code === 'CAMERA_IN_USE' ? 'Close the camera first.' : e.message);
  }
};
```

Keep a light button in step with the system, since the user can switch the flashlight from the quick-settings tile while the app is in the background:

```js
const Torch = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Torch;

async function syncLightButton() {
  if (!Torch) return;
  const { enabled } = await Torch.isEnabled();
  lightBtn.classList.toggle('on', enabled);
  lightBtn.setAttribute('aria-pressed', String(enabled));
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') syncLightButton();
});
syncLightButton();
```

**Notes:** the light follows the system state (turning it off from the quick-settings tile reads back as off) and is switched off when the app closes. While a `getUserMedia` stream is open, light it through the track: `track.applyConstraints({ advanced: [{ torch: true }] })`. AI-built apps use `torch` from `@/lib/appmintNative`.

