# JavaScript bridge API

> Every native method a page inside an AppMint or Appwright app can call: 138 methods.

- **Applies to:** AppMint and Appwright - one bridge, identical in both.
- **Source:** extracted from the app runtime (`WebViewActivity.kt`); this page is generated from it.
- **HTML:** https://freewebtoapk.com/docs/api
- **Using AppMint:** where a description says "Appwright", read "AppMint".

The bridge exists only inside a generated app: in a browser `window.WebToApk` is undefined, so guard every call. Anything asynchronous answers with a DOM event (`appmint:*`) rather than a return value; each description names the event it fires.

## Purchases and entitlements

Selling from your own page, and asking what this buyer already owns. See the in-app purchases guide for the code around these.

### `isPremium`

```js
window.WebToApk.isPremium(): Boolean
```

True when the user owns "Remove Ads" (Play Billing entitlement, auto-restored on every launch). Matches the reason 'ad_free' in 'appmint:ad-unavailable'.

### `startRemoveAdsPurchase`

```js
window.WebToApk.startRemoveAdsPurchase()
```

Launch the Google Play "Remove Ads" purchase flow from the page (user-initiated, e.g. the app's own "Remove ads" button). Without this the ONLY entry point was the native side-menu item - unreachable in apps built without the side menu. Outcome reaches the page via 'appmint:premium' {premium:true}; already-owned re-notifies immediately; flow errors surface as a native toast.

### `isOwned`

```js
window.WebToApk.isOwned(productId: String): Boolean
```

Instant on-device ownership check (one-time product owned OR sub active).

### `getOwnedProducts`

```js
window.WebToApk.getOwnedProducts(): String
```

JSON array of every owned product ID - for gating whole screens at startup.

### `getProducts`

```js
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
```

Asks Play for live details of the given product IDs (JSON array of strings). Result arrives as 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. IDs Play doesn't know are absent from the result - the page hides those items.

### `purchase`

```js
window.WebToApk.purchase(productId: String)
```

Opens Google's purchase sheet for [productId] (one-time product or subscription - resolved automatically). Grant ONLY on 'appmint:purchase' {productId, owned:true}; failures fire 'appmint:purchase-failed' {reason}. Already-owned re-fires the purchase event so the page can idempotently unlock.

## Ads

Showing an interstitial or a rewarded ad on your own cue, and telling the shell when a navigation starts and settles so it does not interrupt one.

### `isRewardedAdReady`

```js
window.WebToApk.isRewardedAdReady(): Boolean
```

True when a rewarded ad is loaded and can be shown right now. Let the page enable/disable its "Watch ad" button off this (poll or call before showing).

### `isInterstitialAdReady`

```js
window.WebToApk.isInterstitialAdReady(): Boolean
```

True when an interstitial is loaded AND past its cooldown (matches exactly what showInterstitialAd() will accept - no "ready" that then refuses to show).

### `showRewardedAd`

```js
window.WebToApk.showRewardedAd()
```

Show a rewarded ad NOW (user-initiated - the only policy-compliant way to show rewarded). Pair it with trigger mode "On Demand" in the wizard to get rewarded ads with NO automatic app-open/navigation ads at all. The reward is delivered back to the page via deliverRewardToPage(): window.addEventListener('appmint:reward', e => { /* e.detail = {type,amount} */ }) When the ad closes, 'appmint:ad-closed' fires with {adType:'rewarded', earned} - earned is false if the user backed out early, so grant nothing in that case. Not showable right now → skipped + 'appmint:ad-unavailable' fired with the reason.

### `showInterstitialAd`

```js
window.WebToApk.showInterstitialAd()
```

Show an interstitial NOW (e.g. between levels / on a deliberate action). Not showable right now → skipped + 'appmint:ad-unavailable' with the reason.

### `notifyUserAction`

```js
window.WebToApk.notifyUserAction()
```

Internal (called by the injected tap counter, not creator code): one meaningful user interaction happened in the page. Counts toward the "interstitial every N actions" cadence ONLY while no URL-based navigation has ever been observed - single-HTML apps that switch screens via DOM show/hide never change the URL, so without this their navigation counter stayed at 0 forever and configured ads never showed (TicTrek complaint class, 2026-07-26). URL/SPA apps are unaffected: their first real navigation permanently switches counting back to URL signals.

### `navStarted`

```js
window.WebToApk.navStarted()
```

Internal (called by the injected navigation observer, not creator code): the page is changing view without changing its URL. This is the ONLY signal for it - a single-HTML app that swaps DOM nodes fires no onPageStarted, no onPageFinished, no doUpdateVisitedHistory, and leaves onProgressChanged pinned at 100, so from native the tap and the new screen are completely invisible. Ignored while a real document load is running: that load owns the bar, and its own callbacks decide when it ends.

### `navSettled`

```js
window.WebToApk.navSettled()
```

Companion to [navStarted]: the swapped-in view has painted.

## Notifications on the device

Posting and scheduling without a server, the POST_NOTIFICATIONS grant, exact alarms, and custom sounds.

### `showNotification`

```js
window.WebToApk.showNotification(title: String, body: String, icon: String, tag: String): Boolean
```

The ORIGINAL four-argument form, kept verbatim as an adapter so every app already in the wild and every guide snippet keeps working. It now forwards to the rich builder - which means the `icon` argument, silently discarded by the old implementation, finally does something.

### `notify`

```js
window.WebToApk.notify(optionsJson: String): Boolean
```

Rich notification. See NotificationBridge.show for the full option list: channel (urgent|default|quiet|ongoing), image, largeIcon, bigText, ongoing, actions, progress, sound, silent, group, color. Image fetching and decoding happen off the main thread, so a remote image can never block the page.

### `getNotificationPermission`

```js
window.WebToApk.getNotificationPermission(): String
```

The CURRENT notification permission, read live from the OS. The polyfill used to hardcode `Notification.permission = 'default'` on every page load, so the overwhelmingly common page pattern if (Notification.permission === 'granted') new Notification(...) never fired - not even for a user who had already granted it, and not on Android 12 and below where notifications need no runtime grant at all.

### `requestNotificationPermission`

```js
window.WebToApk.requestNotificationPermission(): String
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

### `scheduleNotification`

```js
window.WebToApk.scheduleNotification(id: String, title: String, body: String, triggerAtMillis: String, repeat: String): Boolean
```

===== Tier-2: SCHEDULED local notifications (reminders) ===== Fire a notification at a future time even when the app is closed - backed by AlarmManager + persistence so it survives reboot. Dormant unless called. JS contract (all args strings; triggerAtMillis = epoch millis): window.WebToApk.scheduleNotification(id, title, body, triggerAtMillis, repeat) repeat ∈ "none" | "minutely" | "hourly" | "daily" | "weekly" window.WebToApk.cancelNotification(id) window.WebToApk.cancelAllNotifications() window.WebToApk.getScheduledNotifications()  -> JSON array string

### `cancelNotification`

```js
window.WebToApk.cancelNotification(id: String): Boolean
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

### `cancelAllNotifications`

```js
window.WebToApk.cancelAllNotifications(): Boolean
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

### `getScheduledNotifications`

```js
window.WebToApk.getScheduledNotifications(): String
```

Reading and requesting the POST_NOTIFICATIONS grant, and managing notifications the page already scheduled. Posting and scheduling are documented on their own methods.

_Described by its group, Notification permission, cancelling and listing, rather than on its own._

### `scheduleNotificationEx`

```js
window.WebToApk.scheduleNotificationEx(id: String, optionsJson: String): Boolean
```

The full form of [scheduleNotification]: everything `notify()` accepts, fired at a future time with the app closed. WebToApk.scheduleNotificationEx('dose-1', JSON.stringify({ at: Date.now() + 3600000,   // epoch millis (alias: triggerAt) repeat: 'daily',            // none|minutely|hourly|daily|weekly title: 'Time for your dose', body: 'Vitamin D - 1 tablet', sound: '/audio/chime.mp3',  // a file in YOUR bundle; 'silent' for none openApp: true               // open the app itself, over the lock screen })); A bundled sound is copied into place during this call, so a wrong file name is reported now (false) rather than turning into the default sound days later.

### `getAlarmPrecision`

```js
window.WebToApk.getAlarmPrecision(): String
```

"exact" - reminders fire on the minute. "inexact" - the user (or the OEM) turned exact alarms off for this app, so Doze may batch them by minutes. Ask [requestExactAlarms] to fix it. A page that must be on time should check this instead of assuming.

### `requestExactAlarms`

```js
window.WebToApk.requestExactAlarms(): Boolean
```

Opens the system screen where exact alarms are granted. Android 12+.

### `canOpenAppAtTime`

```js
window.WebToApk.canOpenAppAtTime(): String
```

Whether a scheduled notification may put the APP ITSELF on screen (`openApp`). "granted"  - a full-screen alert will show, over the lock screen. "denied"   - Android 14+ withheld it; call requestOpenAppPermission(). There is no third answer where the app silently launches itself in the background: Android has blocked background activity starts since Android 10, and a full-screen intent is the whole of what remains.

### `requestOpenAppPermission`

```js
window.WebToApk.requestOpenAppPermission(): Boolean
```

Opens the system screen where the full-screen alert is granted. Android 14+.

### `prepareNotificationSound`

```js
window.WebToApk.prepareNotificationSound(spec: String): Boolean
```

Copies a bundled audio file into place ahead of time and reports whether it is usable as a notification sound. Optional - `notify()` and `scheduleNotificationEx()` do it themselves - but it lets a settings screen verify a sound the moment the user picks it.

### `clearNotificationSounds`

```js
window.WebToApk.clearNotificationSounds(): Int
```

Drops the copied sound files. They are re-created on next use.

## The push inbox

Messages the creator sent from a dashboard, kept on the device so the page can show a history rather than only a tray pop-up.

### `getPushInbox`

```js
window.WebToApk.getPushInbox(): String
```

Appwright Push inbox (creator-sent messages, newest first, max 50): JSON array of {id, title, body, imageUrl, link, receivedAt, read}. Always the full current truth - an app that mounts late loses nothing by having missed the live `appmint:push` events.

### `markPushRead`

```js
window.WebToApk.markPushRead(id: String): Boolean
```

Marks one push message read; pass "" to mark every message read.

### `dismissPushMessage`

```js
window.WebToApk.dismissPushMessage(id: String): Boolean
```

Dismisses ONE message from the inbox - the per-row swipe-away an inbox screen needs (clearPushInbox is all-or-nothing). REVERSIBLE by design: the entry is hidden, not destroyed, so the UI can offer "Undo" for a few seconds after the swipe. Local only: the tray notification and the creator's history are untouched.

### `restorePushMessage`

```js
window.WebToApk.restorePushMessage(id: String): Boolean
```

Undo a dismiss: puts the message back in its place in the inbox.

### `clearPushInbox`

```js
window.WebToApk.clearPushInbox(): Boolean
```

Empties the push inbox (the tray and server history are untouched).

## Sign-in

A real Google account chooser, native rather than a web redirect.

### `signInWithGoogle`

```js
window.WebToApk.signInWithGoogle(serverClientId: String, callbackId: String)
```

===== Native Google Sign-In via Credential Manager ===== Bypasses Google's Android WebView OAuth block (disallowed_useragent error) by running the sign-in flow in native Android and bridging just the resulting Google ID token back to JS, where the Firebase Web SDK consumes it via GoogleAuthProvider.credential(idToken) + signInWithCredential(...). JS contract: window.WebToApk.signInWithGoogle(serverClientId, callbackId) -> on completion, window.__webToApkAuth[callbackId](resultJsonString) is called resultJsonString = '{"ok":true,"idToken":"...","email":"...","displayName":"..."}' or '{"ok":false,"error":"<message>"}'

### `signOutGoogle`

```js
window.WebToApk.signOutGoogle(callbackId: String)
```

Clears the saved Credential Manager state so the next signInWithGoogle call shows the account picker again (rather than silently re-selecting the last account). Generated apps should call this on user-initiated sign-out, in addition to firebase.auth().signOut().

## Biometrics and the keystore

Fingerprint and face unlock, and hardware-backed keys the page can encrypt with but never read.

### `generateKey`

```js
window.WebToApk.generateKey(alias: String, optionsJson: String): String
```

Creates (or replaces) an AES-256-GCM key called [alias]. @param optionsJson `{"requireAuth":bool, "authValiditySeconds":int, "strongBox":bool}`. `requireAuth` binds the key to the device lock; `strongBox` fails outright on devices without a secure element rather than handing back a weaker key than was asked for. @return JSON `{ok, alias, hardwareBacked, error}`.

### `hasKey`

```js
window.WebToApk.hasKey(alias: String): String
```

True when a key called [alias] exists in this app's keystore.

### `listKeys`

```js
window.WebToApk.listKeys(): String
```

JSON array of every key alias this page has created.

### `encryptWithKey`

```js
window.WebToApk.encryptWithKey(alias: String, base64Plaintext: String): String
```

Encrypts base64 [base64Plaintext] with [alias]. Each call generates its own IV, so a large file is encrypted chunk by chunk - chunks are independent and may be decrypted in any order. @return JSON `{ok, base64, error}` where `base64` is `IV || ciphertext || tag`.

### `decryptWithKey`

```js
window.WebToApk.decryptWithKey(alias: String, base64Payload: String): String
```

Reverses [encryptWithKey]. Fails with `error:"no-such-key"` once the key has been deleted - the payload is then unrecoverable by design. @return JSON `{ok, base64, error}`.

### `deleteKey`

```js
window.WebToApk.deleteKey(alias: String): String
```

Destroys [alias] permanently. Everything encrypted with it becomes undecryptable - including by this app, and by anyone holding the device.

### `isKeyHardwareBacked`

```js
window.WebToApk.isKeyHardwareBacked(alias: String): String
```

True when [alias] lives in the TEE or a secure element rather than in software. Worth checking before promising a user that destroying a key is irreversible.

### `isKeystoreEnabled`

```js
window.WebToApk.isKeystoreEnabled(): String
```

Whether the creator enabled the keystore bridge for this app.

### `isBiometricAvailable`

```js
window.WebToApk.isBiometricAvailable(): String
```

WebToApk.isBiometricAvailable()            -> "available" | "no_hardware"                                                | "not_enrolled" | "unavailable"   WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses:   window.__webToApkAuth = window.__webToApkAuth || {};   window.__webToApkAuth['unlock'] = function (json) {       var r = JSON.parse(json);      // {ok:true} | {ok:false,error:"..."}       if (r.ok) showApp();   };   WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');

_Described by its group, Fingerprint / biometric bridge, rather than on its own._

### `authenticateBiometric`

```js
window.WebToApk.authenticateBiometric(callbackId: String, title: String, subtitle: String)
```

WebToApk.isBiometricAvailable()            -> "available" | "no_hardware"                                                | "not_enrolled" | "unavailable"   WebToApk.authenticateBiometric(id, title, subtitle) Result is delivered to the same __webToApkAuth callback table the Google Sign-In bridge uses:   window.__webToApkAuth = window.__webToApkAuth || {};   window.__webToApkAuth['unlock'] = function (json) {       var r = JSON.parse(json);      // {ok:true} | {ok:false,error:"..."}       if (r.ok) showApp();   };   WebToApk.authenticateBiometric('unlock', 'Unlock', 'Use your fingerprint');

_Described by its group, Fingerprint / biometric bridge, rather than on its own._

### `authenticateBiometricEx`

```js
window.WebToApk.authenticateBiometricEx(callbackId: String, optionsJson: String)
```

The full form. [optionsJson] accepts: title, subtitle, description, negativeButtonText, allowDeviceCredential (PIN/pattern/password fallback), strong (require BIOMETRIC_STRONG - use for anything payment-shaped). Errors are distinct codes, not one boolean: no_hardware | none_enrolled | hw_unavailable | lockout | lockout_permanent | user_cancel | permission_missing.

## Files: a folder the user granted

A directory chosen once, readable and writable afterwards, with permission that survives reboots.

### `requestFolderAccess`

```js
window.WebToApk.requestFolderAccess(requestId: String)
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `hasFolderAccess`

```js
window.WebToApk.hasFolderAccess(): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `isFolderAccessEnabled`

```js
window.WebToApk.isFolderAccessEnabled(): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `getFolderAccessUri`

```js
window.WebToApk.getFolderAccessUri(): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `clearFolderAccess`

```js
window.WebToApk.clearFolderAccess()
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `listFolderEntries`

```js
window.WebToApk.listFolderEntries(relativePath: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `readTextFile`

```js
window.WebToApk.readTextFile(relativePath: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `writeTextFile`

```js
window.WebToApk.writeTextFile(relativePath: String, content: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `mkdir`

```js
window.WebToApk.mkdir(relativePath: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `renameEntry`

```js
window.WebToApk.renameEntry(relativePath: String, newName: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

### `deleteEntry`

```js
window.WebToApk.deleteEntry(relativePath: String, recursive: String): String
```

Play-safe folder access using ACTION_OPEN_DOCUMENT_TREE with persistable URI permissions.  JavaScript usage:   window.WebToApk.requestFolderAccess('myRequestId')   window.WebToApk.hasFolderAccess()                // "true" / "false"   window.WebToApk.getFolderAccessUri()             // "content://..." or ""   window.WebToApk.clearFolderAccess()              // releases persisted access  Callback from Android → JS:   window.WebToApkOnFolderAccessResult(success, folderUri, requestId)

_Described by its group, Native Folder Access (SAF) Bridge, rather than on its own._

## Files: one file at a time

The system picker and chunked binary read/write, for files too large to hold in a string.

### `pickFile`

```js
window.WebToApk.pickFile(requestId: String, mimeFilter: String)
```

Opens the system single-file picker (ACTION_OPEN_DOCUMENT). Unlike [requestFolderAccess] this grants access to exactly one file, which is what an editor or a shredder should ask for; the read/write grant is persisted so the URI keeps working after the process is killed. @param mimeFilter e.g. "image/\*", "application/pdf", or "" for any file. (The star is backslash-escaped only because Kotlin block comments nest.)

### `readFileBase64`

```js
window.WebToApk.readFileBase64(pathOrUri: String, offset: Long, length: Long): String
```

Reads up to [length] raw bytes starting at [offset] and returns them base64. @param pathOrUri a `content://` URI from [pickFile], or a path relative to the folder granted by [requestFolderAccess]. @param length bytes to read; clamped to 4 MB per call. 0 or negative means "as much as one call allows". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` tells a chunking loop when to stop without having to compare against `size` itself.

### `writeFileBase64`

```js
window.WebToApk.writeFileBase64(pathOrUri: String, base64: String, offset: Long, mode: String): String
```

Writes raw bytes and confirms how many landed. @param mode `"patch"` writes at [offset] leaving the rest of the file intact (what a multi-pass overwrite needs), `"truncate"` replaces the file, `"append"` ignores [offset] and writes at the end. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` is re-read from the file after an fsync, so a caller can verify the write for real rather than trust a boolean.

### `statFile`

```js
window.WebToApk.statFile(pathOrUri: String): String
```

File metadata without reading any of it. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`.

### `releaseFileAccess`

```js
window.WebToApk.releaseFileAccess(uriString: String): String
```

Releases a persisted single-file grant taken by [pickFile]. Android caps how many URI grants an app may persist, so an app that picks many files should release the ones it is done with.

### `getMaxIoChunkSize`

```js
window.WebToApk.getMaxIoChunkSize(): Long
```

The per-call byte cap for [readFileBase64] / [writeFileBase64], for chunk maths.

## Files: opened with this app

Collecting what another app handed over through "Open with", in chunks the page pulls.

### `openedFilesPending`

```js
window.WebToApk.openedFilesPending(): String
```

The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices - nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

_Described by its group, "Open with this app" file collection, rather than on its own._

### `openedFileChunk`

```js
window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String
```

The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices - nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

_Described by its group, "Open with this app" file collection, rather than on its own._

### `openedFileCollected`

```js
window.WebToApk.openedFileCollected(token: String)
```

The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices - nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

_Described by its group, "Open with this app" file collection, rather than on its own._

### `openedFileRelease`

```js
window.WebToApk.openedFileRelease(token: String)
```

The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices - nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

_Described by its group, "Open with this app" file collection, rather than on its own._

### `openedFileMaxChunk`

```js
window.WebToApk.openedFileMaxChunk(): Int
```

The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Bytes come across in MAX_IO_CHUNK slices - nothing is held whole in memory, which is what lets a 40 MB GPX through where the old 5 MB inline cap refused.

_Described by its group, "Open with this app" file collection, rather than on its own._

## Media and casting

The lock-screen media notification your page drives, and handing a video to a TV.

### `castMedia`

```js
window.WebToApk.castMedia(mediaUrl: String)
```

Offers [mediaUrl] to whatever on this phone can play it on another screen - Chromecast, a Smart TV app, a local player - through the system chooser. This is a hand-off, not a cast session: the app does not stay in control of playback, and there is no callback. A page that needs a real session should use the Cast SDK in JavaScript instead.

### `setMediaMetadata`

```js
window.WebToApk.setMediaMetadata(title: String, artist: String, artworkUrl: String)
```

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

_Described by its group, Media Notification Bridge, rather than on its own._

### `setPlaybackState`

```js
window.WebToApk.setPlaybackState(isPlaying: Boolean, positionMs: Long, durationMs: Long)
```

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

_Described by its group, Media Notification Bridge, rather than on its own._

### `clearMediaNotification`

```js
window.WebToApk.clearMediaNotification()
```

Lets web content drive the Android media-style notification and receive transport control callbacks (play/pause/next/prev) from the lock screen and notification shade.  Usage from JavaScript:   window.WebToApk.setMediaMetadata('Song Title', 'Artist Name', 'https://…/art.jpg')   window.WebToApk.setPlaybackState(true, 30000, 240000)   // playing, pos ms, dur ms   window.WebToApk.clearMediaNotification()  Callback from Android → JS:   window.WebToApkOnMediaAction('play'|'pause'|'next'|'prev'|'stop')

_Described by its group, Media Notification Bridge, rather than on its own._

## The window

Fullscreen, picture-in-picture, the system bar colours, screenshot blocking, the splash screen and closing a window.

### `splashHold`

```js
window.WebToApk.splashHold()
```

Keeps the branded launch splash up until [splashReady] is called. By default the splash hides as soon as the body has laid-out children, which for an app that paints a skeleton and then fetches its first screen means "splash, then a skeleton". An app that would rather reveal a finished first screen calls this at import and [splashReady] once that screen is on. Capped at the same 6s backstop as the paint poll, so a page that never reports ready still opens.

### `splashReady`

```js
window.WebToApk.splashReady()
```

The first screen is on - hide the launch splash now (subject to the creator's minimum splash duration). Idempotent; harmless when nothing was held.

### `enterPip`

```js
window.WebToApk.enterPip(): Boolean
```

Shrinks the whole app into a floating picture-in-picture window right now - a video or a call the user wants to keep watching while they leave. Android 8+ and only when the build enabled PiP (the manifest flag it needs); returns whether the window opened. Leaving the app with PiP enabled does this by itself; this is the explicit "minimise" button.

### `closeWindow`

```js
window.WebToApk.closeWindow()
```

The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

_Described by its group, Share, clipboard and window, rather than on its own._

### `setFullscreen`

```js
window.WebToApk.setFullscreen(enabled: Boolean)
```

Lets the page hide the status bar (clock/battery) and navigation bar itself - e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk - there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

_Described by its group, Fullscreen bridge, rather than on its own._

### `toggleFullscreen`

```js
window.WebToApk.toggleFullscreen()
```

Lets the page hide the status bar (clock/battery) and navigation bar itself - e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk - there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

_Described by its group, Fullscreen bridge, rather than on its own._

### `isFullscreen`

```js
window.WebToApk.isFullscreen(): Boolean
```

Lets the page hide the status bar (clock/battery) and navigation bar itself - e.g. a game entering its play screen:     WebToApk.setFullscreen(true)   // hide the system bars     WebToApk.setFullscreen(false)  // bring them back     WebToApk.toggleFullscreen()     WebToApk.isFullscreen() The choice is remembered across launches. It is a no-op in apps the creator already locked to fullscreen/kiosk - there is nothing to restore to there. The side-menu "Fullscreen" item is the equivalent for end users and is gated by the creator's enableFullscreenToggle option; this bridge is always available to the app's own page.

_Described by its group, Fullscreen bridge, rather than on its own._

### `setStatusBarColor`

```js
window.WebToApk.setStatusBarColor(color: String, style: String): Boolean
```

WebToApk.setStatusBarColor('#0B0F1A', 'light')   // light icons on a dark bar   WebToApk.setStatusBarColor('#FFFFFF', 'dark')    // dark icons on a light bar   WebToApk.setStatusBarColor('#0B0F1A', '')        // icons chosen from luminance  Why the shell and not Capacitor's StatusBar plugin: on API 35+ the plugin's setBackgroundColor is a no-op (it writes window.statusBarColor, which enforced edge-to-edge ignores), so appmintNative's setStatusBar() reported 'native' and changed nothing on every Android 15 device. The shell owns the strip - see systemBarCanvas - so it is the only party that can actually paint it there. Returns false for an unparseable colour; hidden bars (fullscreen/kiosk) accept the colour for when they reappear.

_Described by its group, System bar colour, rather than on its own._

### `setSecureScreen`

```js
window.WebToApk.setSecureScreen(enabled: Boolean): Boolean
```

Sets protection for the CURRENT page and returns the state now in force. The window flag itself is applied on the UI thread, but the state this returns is already committed, so a page can call this and immediately reveal its sensitive content without waiting for a callback.

### `isSecureScreen`

```js
window.WebToApk.isSecureScreen(): Boolean
```

WebToApk.setSecureScreen(true)   // block screenshots on THIS page   WebToApk.setSecureScreen(false)  // release it early   WebToApk.isSecureScreen()        // -> boolean  Protection belongs to the page that asked for it. It is released the moment the app navigates anywhere else - a new document, a pushState route, a #hash route - so a protected screen cannot leave the rest of the app locked down, and a page that wants protection back after a reload simply asks again.  What it does: screenshots, screen recording, casting and the recents-screen thumbnail all stop while it is on. What it cannot do: stop a camera pointed at the screen. Nothing on Android can, and no bridge here will pretend otherwise.

_Described by its group, Screen-capture protection, rather than on its own._

## Share, clipboard and print

The Android share sheet, the system clipboard, text another app shared into yours, and the print dialogue.

### `print`

```js
window.WebToApk.print()
```

Hands the page as it currently stands to Android's print dialogue, which can print on paper or save a PDF. Nothing to configure - the WebView renders the document.

_Described by its group, Printing, rather than on its own._

### `shareNative`

```js
window.WebToApk.shareNative(title: String, text: String, url: String)
```

The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

_Described by its group, Share, clipboard and window, rather than on its own._

### `getSharedText`

```js
window.WebToApk.getSharedText(): String
```

Text or a link another app shared INTO this one ("Share to <app>"), as JSON `{text, subject, url, receivedAt}` or "" when nothing was shared. Kept until read, so an app that mounts after the share still sees it; a share arriving while the app runs also fires `appmint:shared`. Requires the build's "Open with this app" option, which registers the share-sheet entry.

### `clearSharedText`

```js
window.WebToApk.clearSharedText()
```

Clears the kept share once the page has consumed it.

### `copyToClipboard`

```js
window.WebToApk.copyToClipboard(text: String): Boolean
```

The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

_Described by its group, Share, clipboard and window, rather than on its own._

### `shareFiles`

```js
window.WebToApk.shareFiles(filesJson: String, title: String, text: String): Boolean
```

TRUE native file share for navigator.share({files:[...]}). Android WebView has no Web Share API, and text/plain ACTION_SEND can't carry a file - so before this a shared image was merely SAVED, not shared. filesJson is [{name, mimeType, base64}, ...]; we write each to cache, expose it via the app's FileProvider, and fire ACTION_SEND / ACTION_SEND_MULTIPLE so the real Android share sheet (WhatsApp, Gmail, …) opens. Returns true if the sheet opened.

### `readClipboard`

```js
window.WebToApk.readClipboard(): String
```

The Android share sheet, the system clipboard, and closing a window the page opened. These are the plain-web equivalents (navigator.share, navigator.clipboard, window.close) answered natively so they behave the same inside the app.

_Described by its group, Share, clipboard and window, rather than on its own._

## Launch, shortcuts and the back button

How the app was opened, long-press shortcuts on the icon, the in-app review prompt, and telling the shell the page wants the back press.

### `getLaunchUrl`

```js
window.WebToApk.getLaunchUrl(): String
```

The deep link that launched or resumed this app, or "" if it was opened normally. Read it whenever your app mounts and route on it yourself: const u = window.WebToApk?.getLaunchUrl?.(); if (u) router.navigate(new URL(u).pathname); The value is KEPT, not consumed, so a late-mounting app still sees it; a later link arriving while the app runs replaces it and also fires 'appmint:deep-link'.

### `setAppShortcuts`

```js
window.WebToApk.setAppShortcuts(itemsJson: String)
```

Launcher shortcuts (long-press the app icon). Up to four entries: `[{id, title, route}]`, where `route` is a path in the app ('/new-note'). A tap opens the app with that route as its launch deep link (`appmint-shortcut://<package><route>`), which the page reads through `launchUrl()` and routes on exactly like any other deep link. Pass `[]` to remove them. Android 7.1+; a no-op below that.

### `rateApp`

```js
window.WebToApk.rateApp()
```

Opens the Play listing's rating flow. `rateApp()` already existed but only the native drawer entry could reach it, so an app that draws its own UI - every AI build, where the drawer is off - had no way to ask for a rating. It is the one store action a page genuinely cannot perform for itself: `market://` is an intent, not a navigable URL. Gated on the creator's own "Rate App Button" switch, exactly like the drawer entry and the bottom-nav Share item: turning Rate off must mean no rating surface anywhere, or the toggle looks broken.

### `__setBackInterest`

```js
window.WebToApk.__setBackInterest(interested: Boolean)
```

*(Back button)* Internal for AppMint.setBackHandler - tells the shell whether the page registered a back handler. When true, a hardware/gesture back is first offered to the page (cancelable `appmint:back` event + the registered handler); only an unconsumed press falls through to history-back / exit.

## Contacts

Reading, searching, picking and writing - the picker needs no permission at all.

### `listContacts`

```js
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

### `searchContacts`

```js
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

### `getContact`

```js
window.WebToApk.getContact(requestId: String, contactId: String)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

### `pickContact`

```js
window.WebToApk.pickContact(requestId: String)
```

System contact picker - needs NO permission, and is the right choice whenever the page just wants the user to choose one person. Picks from the PHONE table, not the contact table: the picker grants read access only to the row it returns, so picking a phone row is what makes the number readable without READ_CONTACTS. Picking a bare contact would return a row with a name and no number, and reading the number from it would need the permission this method exists to avoid.

### `addContact`

```js
window.WebToApk.addContact(requestId: String, contactJson: String)
```

The system picker (no permission needed) and writing a new contact (WRITE_CONTACTS). The read side - listing and searching - is under the Contacts group above.

_Described by its group, Contacts: pick and add, rather than on its own._

## Phone, SMS and the call log

Placing a call, sending or drafting a message, and reading history. Every one of these is permission-gated and Play asks about them.

### `makePhoneCall`

```js
window.WebToApk.makePhoneCall(phoneNumber: String)
```

Places a call directly when the creator enabled the Phone permission, otherwise opens the dialer with the number prefilled (which needs no permission). A plain `tel:` link in your HTML also works and is often enough.

### `listCallLog`

```js
window.WebToApk.listCallLog(requestId: String, limit: Int, offset: Int, sinceMillis: String)
```

WebToApk.listCallLog(id, limit, offset, sinceMillis) Delivered as `appmint:calllog` / window.onAppMintCallLog.

_Described by its group, Call log, rather than on its own._

### `composeSms`

```js
window.WebToApk.composeSms(phoneNumber: String, message: String): Boolean
```

Opens the user's SMS app with the message prefilled. Needs no permission and cannot be rejected by Play - the right default for anything non-automated.

### `sendSms`

```js
window.WebToApk.sendSms(
```

WebToApk.composeSms(number, message)   <- NO permission, opens the SMS app   WebToApk.sendSms(id, number, message, wantDelivery)   WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms   {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.

_Described by its group, SMS, rather than on its own._

### `listSms`

```js
window.WebToApk.listSms(requestId: String, box: String, limit: Int, offset: Int)
```

WebToApk.composeSms(number, message)   <- NO permission, opens the SMS app   WebToApk.sendSms(id, number, message, wantDelivery)   WebToApk.listSms(id, "inbox"|"sent"|"draft", limit, offset) Send results: `appmint:sms` / window.onAppMintSms   {requestId, status:"sent"|"delivered"|"failed", ok, reason?} Inbound messages: `appmint:sms-received` / window.onAppMintSmsReceived.

_Described by its group, SMS, rather than on its own._

## Vibration and tap sound

Haptic feedback and the app's own click sound, fired on your semantic events rather than every stray tap.

### `vibrate`

```js
window.WebToApk.vibrate(durationMs: Long)
```

Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

_Described by its group, Vibration, rather than on its own._

### `cancelVibrate`

```js
window.WebToApk.cancelVibrate()
```

Simple haptics for the page (navigator.vibrate and a pattern form). Both are ignored unless the build enabled the Vibrate permission.

_Described by its group, Vibration, rather than on its own._

### `vibratePattern`

```js
window.WebToApk.vibratePattern(patternJson: String)
```

Vibrates an on/off pattern, e.g. "[100,50,100]" = buzz 100ms, pause 50, buzz 100. Backs navigator.vibrate(array); prefer the standard API. No-op unless the creator enabled the Vibrate permission.

### `playClick`

```js
window.WebToApk.playClick()
```

Plays the app's tap sound on demand, so a page can fire it on its OWN semantic events (a button press) instead of on every stray tap - which is what a native app actually does, and the reason clickSoundMode can be "off" while this still works. Honours the creator's mode; no-op when tap sound is off.

## Text to speech

The device voices, spoken from the page.

### `ttsSpeak`

```js
window.WebToApk.ttsSpeak(text: String, lang: String, rate: Double, pitch: Double, utteranceId: String)
```

Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

_Described by its group, Text-to-speech, rather than on its own._

### `ttsCancel`

```js
window.WebToApk.ttsCancel()
```

Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

_Described by its group, Text-to-speech, rather than on its own._

### `ttsIsSpeaking`

```js
window.WebToApk.ttsIsSpeaking(): Boolean
```

Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

_Described by its group, Text-to-speech, rather than on its own._

### `ttsWarmUp`

```js
window.WebToApk.ttsWarmUp()
```

Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.

_Described by its group, Text-to-speech, rather than on its own._

### `ttsGetVoices`

```js
window.WebToApk.ttsGetVoices(): String
```

JSON array of installed voices: [{name, lang, default}]. Empty until the engine finishes init - the page gets 'voiceschanged' then and re-queries, exactly like Chrome's async getVoices() contract.

## BLE fitness sensors

Heart rate, speed, cadence, power and battery from standard Bluetooth Low Energy profiles.

### `bleStartScan`

```js
window.WebToApk.bleStartScan()
```

Runtime for the existing Bluetooth permission toggle (permBluetooth) - the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

### `bleStopScan`

```js
window.WebToApk.bleStopScan()
```

Runtime for the existing Bluetooth permission toggle (permBluetooth) - the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

### `bleConnect`

```js
window.WebToApk.bleConnect(address: String)
```

Runtime for the existing Bluetooth permission toggle (permBluetooth) - the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

### `bleDisconnect`

```js
window.WebToApk.bleDisconnect()
```

Runtime for the existing Bluetooth permission toggle (permBluetooth) - the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

### `bleSetWheelCircumference`

```js
window.WebToApk.bleSetWheelCircumference(mm: Int)
```

Runtime for the existing Bluetooth permission toggle (permBluetooth) - the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

## Web Bluetooth

The navigator.bluetooth shape, answered natively so a page written for Chrome works unchanged.

### `wbIsAvailable`

```js
window.WebToApk.wbIsAvailable(): Boolean
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbRequestDevice`

```js
window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbConnect`

```js
window.WebToApk.wbConnect(requestId: String, deviceId: String)
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbDisconnect`

```js
window.WebToApk.wbDisconnect(deviceId: String)
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbGetCharacteristics`

```js
window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbRead`

```js
window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbWrite`

```js
window.WebToApk.wbWrite(
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

### `wbNotify`

```js
window.WebToApk.wbNotify(
```

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

_Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own._

## NFC

Reading a tag through window.AndroidNFC, and the write side behind the NDEFReader polyfill.

### `__nfcWrite`

```js
window.WebToApk.__nfcWrite(messageJson: String)
```

Called from the JS NDEFReader.write() polyfill. Stores the message JSON and waits for the next NFC tag tap.

_Described by its group, NFC Write Bridge, rather than on its own._

### `__nfcMakeReadOnly`

```js
window.WebToApk.__nfcMakeReadOnly()
```

Called from the JS NDEFReader.makeReadOnly() polyfill.

### `__nfcCancelWrite`

```js
window.WebToApk.__nfcCancelWrite()
```

Cancel any pending NFC write/lock operation.

### `startScan`

```js
window.AndroidNFC.startScan(): Boolean
```

Legacy availability probe, present only when the creator enabled NFC. Real NFC work goes through the standard NDEFReader polyfill (window.WebToApk.__nfc*), which is what a page should use; this only reports whether the adapter is on.

_Described by its group, Web NFC, rather than on its own._

## Background jobs

Work that keeps going after the app is closed: a durable auto-save, a periodic sync.

### `workEnqueueOnline`

```js
window.WebToApk.workEnqueueOnline(id: String, url: String, method: String, headersJson: String, payload: String)
```

No permission needed; results echo as `appmint:work` / window.onAppMintWork.

_Described by its group, Background jobs (WorkManager): durable auto-save + periodic native sync, rather than on its own._

### `workSchedulePeriodic`

```js
window.WebToApk.workSchedulePeriodic(id: String, url: String, method: String, headersJson: String, payload: String, intervalMinutes: Int)
```

No permission needed; results echo as `appmint:work` / window.onAppMintWork.

_Described by its group, Background jobs (WorkManager): durable auto-save + periodic native sync, rather than on its own._

### `workCancel`

```js
window.WebToApk.workCancel(id: String)
```

No permission needed; results echo as `appmint:work` / window.onAppMintWork.

_Described by its group, Background jobs (WorkManager): durable auto-save + periodic native sync, rather than on its own._

### `workList`

```js
window.WebToApk.workList(requestId: String)
```

No permission needed; results echo as `appmint:work` / window.onAppMintWork.

_Described by its group, Background jobs (WorkManager): durable auto-save + periodic native sync, rather than on its own._

## Device and app information

What phone this is, which build, and a stable per-install identifier.

### `getAppName`

```js
window.WebToApk.getAppName(): String
```

The app name the creator chose in the wizard - what the launcher shows.

### `getAppVersion`

```js
window.WebToApk.getAppVersion(): String
```

The versionName from the installed package, e.g. "1.4.2" - the string a creator sets when building, not the versionCode. Falls back to "1.0" if the package cannot be read, so a page can print it without a null check.

### `getDeviceInfo`

```js
window.WebToApk.getDeviceInfo(): String
```

WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

_Described by its group, Device / app information, rather than on its own._

### `getPhoneIdentity`

```js
window.WebToApk.getPhoneIdentity(): String
```

WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

_Described by its group, Device / app information, rather than on its own._

### `getInstallId`

```js
window.WebToApk.getInstallId(): String
```

WebToApk.getDeviceInfo()    -> JSON string, NO permission required   WebToApk.getPhoneIdentity() -> carrier/SIM, needs the Phone permission   WebToApk.getInstallId()     -> stable per-install UUID

_Described by its group, Device / app information, rather than on its own._

