# Examples — copy and use

Short, working examples for the most common things.
Copy the code into your HTML page. Change the text and the names to fit your app.

Every example is safe to run in a normal browser too. Nothing breaks.

---

## 1. Check if your page is inside the app

Do this first. `WebToApk` only exists inside the app. In a browser it is missing.

```html
<script>
if (window.WebToApk) {
  console.log('Inside the app');
} else {
  console.log('In a browser');
}
</script>
```

**Rule:** always check before you call. Never call `WebToApk.something()` directly.

---

## 2. Vibrate the phone

Use the normal web API. The app makes it work for you.

```js
navigator.vibrate(100);           // buzz for 100ms
navigator.vibrate([80, 40, 80]);  // buzz, pause, buzz
```

Turn on **Vibrate** in Step 3 (Permissions) when you build the app.

---

## 3. Show a simple notification

```js
function notifyUser() {
  if (!window.WebToApk) return;
  WebToApk.notify(JSON.stringify({
    title: 'Order ready',
    body: 'Your order is ready to collect',
    channel: 'default',
    tag: 'order'
  }));
}
```

`tag` is an ID. If you send again with the same `tag`, it replaces the old one.
Without a `tag`, you get many notifications stacked up.

---

## 4. Notification with a picture

```js
WebToApk.notify(JSON.stringify({
  title: 'New photo',
  body: 'Tap to see it',
  image: 'https://yoursite.com/photo.jpg',   // big picture
  largeIcon: 'https://yoursite.com/logo.png', // small round icon
  channel: 'default',
  tag: 'photo'
}));
```

---

## 5. Important notification the user cannot swipe away

```js
WebToApk.notify(JSON.stringify({
  title: 'Delivery on the way',
  body: 'Driver is 5 minutes away',
  channel: 'ongoing',
  ongoing: true,      // cannot be swiped away
  tag: 'delivery'
}));

// Remove it yourself when the job is done:
WebToApk.cancelNotification('delivery');
```

**Note:** `ongoing` stops swiping. The user can still remove it from Android
settings. No app can make a notification impossible to remove.

---

## 6. Notification with buttons

```js
WebToApk.notify(JSON.stringify({
  title: 'New message',
  body: 'Ali sent you a message',
  actions: [
    { id: 'reply', label: 'Reply' },
    { id: 'ignore', label: 'Ignore' }
  ],
  tag: 'msg-1'
}));

// Listen for the button the user taps:
window.addEventListener('appmint:notification-action', function (e) {
  if (e.detail.actionId === 'reply') openReplyBox();
  if (e.detail.actionId === 'ignore') hideMessage();
});
```

---

## 7. Show progress in a notification

```js
function showProgress(percent) {
  WebToApk.notify(JSON.stringify({
    title: 'Uploading',
    body: percent + '%',
    channel: 'ongoing',
    ongoing: true,
    progress: { current: percent, max: 100 },
    tag: 'upload'
  }));
}

showProgress(40);
// When finished:
WebToApk.cancelNotification('upload');
```

---

## 8. Get phone information

No permission needed.

```js
function showPhoneInfo() {
  if (!window.WebToApk) return;
  var info = JSON.parse(WebToApk.getDeviceInfo());

  document.getElementById('out').innerHTML =
    'Android: ' + info.android.release + '<br>' +
    'Version ID: ' + info.android.buildId + '<br>' +
    'Build number: ' + info.android.incremental + '<br>' +
    'Phone: ' + info.hardware.manufacturer + ' ' + info.hardware.model + '<br>' +
    'Battery: ' + info.runtime.batteryLevel + '%<br>' +
    'Screen: ' + info.screen.widthPx + 'x' + info.screen.heightPx;
}
```

**About device ID:** there is no IMEI or serial number. Android blocked this for
all apps from Android 10. Use `WebToApk.getInstallId()` instead. It is a fixed ID
for this install. It stays the same after updates. It changes if the user
uninstalls the app.

---

## 9. Let the user pick one contact

This is the easy way. **No permission needed.**

Android opens its own contact list. The user chooses a number.
You get back the name and that one number — nothing else, and no permission prompt.

```js
function chooseContact() {
  if (!window.WebToApk) return;

  window.addEventListener('appmint:contacts', function once(e) {
    window.removeEventListener('appmint:contacts', once);
    if (e.detail.error) { alert('Could not open contacts'); return; }
    var c = e.detail.contact;
    if (!c) return;
    document.getElementById('name').value = c.displayName;
    document.getElementById('phone').value = c.phones[0] ? c.phones[0].number : '';
  });

  WebToApk.pickContact('pick1');
}
```

---

## 10. Read the full contact list

This one **needs the Contacts permission** (Step 3 when you build).

```js
function loadContacts() {
  window.addEventListener('appmint:contacts', function once(e) {
    window.removeEventListener('appmint:contacts', once);

    if (e.detail.error === 'not_enabled') {
      alert('Turn on Contacts when you build the app');
      return;
    }
    if (e.detail.error === 'permission_denied') {
      alert('Please allow contacts access');
      return;
    }

    var html = '';
    e.detail.contacts.forEach(function (c) {
      var num = c.phones[0] ? c.phones[0].number : '';
      html += '<li>' + c.displayName + ' — ' + num + '</li>';
    });
    document.getElementById('list').innerHTML = html;
  });

  WebToApk.listContacts('all1', 100, 0);   // 100 contacts, start at 0
}
```

Search instead of loading everything:

```js
WebToApk.searchContacts('find1', 'ahmed', 20);
```

---

## 11. Send an SMS — the easy way

**No permission needed.** This opens the SMS app with your text ready.
The user presses send. Google Play never asks about this.

```js
function textUs() {
  if (!window.WebToApk) return;
  WebToApk.composeSms('+911234567890', 'Hello, I need help with my order');
}
```

Use this unless your app really must send by itself.

---

## 12. Send an SMS from your code

**Needs the SMS permission.** The message is sent without opening any app.

```js
function sendCode() {
  // Note: do NOT use "once" here. With a delivery report you get TWO answers:
  // first "sent", then "delivered" a moment later.
  window.addEventListener('appmint:sms', function (e) {
    if (e.detail.requestId !== 'sms1') return;

    if (e.detail.status === 'sent')      showStatus('Message sent');
    if (e.detail.status === 'delivered') showStatus('Message delivered');
    if (e.detail.status === 'failed')    showStatus('Failed: ' + e.detail.reason);
  });

  WebToApk.sendSms('sms1', '+911234567890', 'Your code is 4821', true);
}
```

The last value `true` asks for a delivery report. Use `false` if you do not need it —
then you get only one answer.

Some networks never send a delivery report. If none arrives in 60 seconds you get
`status: 'sent'` with `reason: 'no_delivery_report'`. The message was still sent.

Long messages are split and sent correctly. You do not need to cut them yourself.

---

## 13. Receive an SMS

**Needs the SMS permission.** Works even when the app was closed —
messages are saved and given to your page when the app opens again.

```js
window.addEventListener('appmint:sms-received', function (e) {
  console.log('From: ' + e.detail.address);
  console.log('Text: ' + e.detail.body);

  // Example: read a 4-digit code out of the message
  var code = (e.detail.body.match(/\d{4}/) || [])[0];
  if (code) document.getElementById('otp').value = code;
});
```

---

## 14. Read the call log

**Needs the Call log permission.**

```js
function loadCalls() {
  window.addEventListener('appmint:calllog', function once(e) {
    window.removeEventListener('appmint:calllog', once);
    if (e.detail.error) { alert(e.detail.error); return; }

    e.detail.calls.forEach(function (c) {
      // c.type is: incoming, outgoing, missed, rejected, blocked
      console.log(c.name + ' ' + c.number + ' (' + c.type + ') ' + c.duration + 's');
    });
  });

  WebToApk.listCallLog('calls1', 50, 0, '0');   // last 50 calls
}
```

---

## 15. Fingerprint lock

```js
function unlock() {
  if (!window.WebToApk) return;

  window.__webToApkAuth = window.__webToApkAuth || {};
  window.__webToApkAuth['unlock'] = function (json) {
    var r = JSON.parse(json);

    if (r.ok) {
      showMyApp();
    } else if (r.error === 'none_enrolled') {
      alert('Please add a fingerprint in phone settings');
    } else if (r.error === 'lockout') {
      alert('Too many tries. Wait and try again.');
    } else if (r.error === 'user_cancel') {
      // user closed it — do nothing
    }
  };

  WebToApk.authenticateBiometricEx('unlock', JSON.stringify({
    title: 'Unlock',
    subtitle: 'Use your fingerprint',
    allowDeviceCredential: true   // PIN also works
  }));
}
```

First check if the phone has a sensor:

```js
var state = WebToApk.isBiometricAvailable();
// "available" | "no_hardware" | "not_enrolled" | "unavailable"
if (state !== 'available') hideFingerprintButton();
```

---

## 16. Tap sound on your buttons

```js
document.querySelectorAll('button').forEach(function (b) {
  b.addEventListener('click', function () {
    if (window.WebToApk && WebToApk.playClick) WebToApk.playClick();
  });
});
```

Set **Tap sound** when you build the app.

`Follow phone setting` (recommended) plays only if the user has Touch sounds on.
`Always` plays on EVERY tap, including taps on ordinary links — usually not what you want.
`playClick()` above always works, so you can keep raw taps quiet and still sound your own buttons.

---

## 17. Make your page fill the screen correctly

When you turn on **Fullscreen**, the Android bars are hidden.
Add this CSS so your content is not under the phone's notch.

```css
body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  min-height: 100vh;    /* old phones */
  min-height: 100dvh;   /* correct height when bars are hidden */
}
```

**Note:** *Fullscreen* hides the **Android** bars (clock, battery, back buttons).
Your app's own coloured bar at the top is a different switch called **Show top bar**.
If you still see a bar, turn that one off.

---

## 18. Share and copy

```js
WebToApk.shareNative('My app', 'Look at this', 'https://mysite.com');
WebToApk.copyToClipboard('Text to copy');
var text = WebToApk.readClipboard();
```

---

## 19. Wait for an answer using async/await

Many functions answer later, not immediately. This helper makes them easy to use.
Paste it once, then use it for any of them.

```js
function ask(eventName, run) {
  return new Promise(function (ok, fail) {
    var id = 'r' + Date.now() + Math.random();
    var timer = setTimeout(function () { stop(); fail(new Error('timeout')); }, 15000);
    function got(e) {
      if (!e.detail || e.detail.requestId !== id) return;
      stop();
      e.detail.error ? fail(new Error(e.detail.error)) : ok(e.detail);
    }
    function stop() { clearTimeout(timer); window.removeEventListener(eventName, got); }
    window.addEventListener(eventName, got);
    run(id);
  });
}
```

Now your code is short and clean:

```js
async function showContacts() {
  try {
    var r = await ask('appmint:contacts', function (id) {
      WebToApk.listContacts(id, 50, 0);
    });
    console.log(r.contacts);
  } catch (err) {
    alert('Problem: ' + err.message);
  }
}
```

---

## 20. Open one file and read its real bytes

Needs **Native Folder Access (SAF)** ticked when building.

```js
async function openFile() {
  const file = await WebToApkFS.pickFile('');   // '' = any file, or 'image/*'
  if (!file.ok) return;                          // user cancelled

  const info = WebToApkFS.stat(file.uri);
  console.log(info.name, info.size, 'bytes');

  const chunk = WebToApkFS.readBytes(file.uri, 0, 65536);
  if (chunk.ok) console.log('first bytes, base64:', chunk.base64);
}
```

Change 4 bytes in the middle without touching the rest of the file:

```js
const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
```

---

## 21. A note the user can delete for real

Needs **Secure Keys (encryption)** ticked too.

Overwriting a file does **not** erase it on a phone — flash memory writes your zeros somewhere
else and keeps the old blocks. So do not try. Save it encrypted instead, and delete the key:

```js
// Once, when the user creates their vault:
await AppMintKeystore.generateKey('vault', { requireAuth: true });

// Saving:
WebToApkFS.writeSealed('secret.bin', btoa(text), 'vault');

// Reading:
const out = WebToApkFS.readSealed('secret.bin', 'vault');
if (out.ok) show(atob(out.base64));
else if (out.error === 'auth-required') askFingerprintThenRetry();

// Deleting, for real:
AppMintKeystore.deleteKey('vault');     // nobody can read secret.bin again — including you
WebToApkFS.delete('secret.bin');
```

---

## 22. "Watch an ad to earn 1 credit" (rewarded ad on demand)

Build the app with **Full Screen Ad → Rewarded** and **Trigger Mode → On Demand**. Nothing then
shows by itself; the ad appears only when your own button asks for it.

```html
<button id="earn">Watch an ad to earn 1 credit</button>

<script>
const ads = window.WebToApk || null;          // undefined in a browser — keep the page usable
const btn = document.getElementById('earn');

// Only offer the button when an ad is actually loaded.
function refresh() { btn.disabled = !(ads && ads.isRewardedAdReady && ads.isRewardedAdReady()); }
refresh();
setInterval(refresh, 2000);

btn.addEventListener('click', () => {
  btn.disabled = true;
  ads && ads.showRewardedAd();                // opens the ad
});

// GRANT ONLY HERE — fires once, after the ad was genuinely watched to the end.
window.addEventListener('appmint:reward', (e) => {
  const { type, amount } = e.detail;          // e.g. {type:'credit', amount:1}
  addCredits(1);                              // your own function
});

// The ad closed. earned === false means the user backed out early: grant NOTHING.
window.addEventListener('appmint:ad-closed', (e) => {
  if (!e.detail.earned) showToast('No credit — the ad was closed early.');
  refresh();
});

// No ad could be shown at all (offline, not loaded yet, ads removed by purchase…).
window.addEventListener('appmint:ad-unavailable', (e) => {
  // reason: ad_free | disabled | consent | not_loaded | cooldown | show_failed
  if (e.detail.reason === 'ad_free') addCredits(1);   // paying users get it free
  refresh();
});
</script>
```

Rules that keep the app compliant: never grant on the button tap, never auto-show a rewarded ad,
and never put a core feature behind it — it is an optional bonus.

---

## 23. Save a file with the name you choose

Your normal download code already works — the app intercepts it and opens the system
"Save as…" sheet with **your** filename in the box:

```html
<a id="dl" download="Site_Report_2026-08.pdf">Download report</a>
<script>
  const blob = await html2pdf().from(el).outputPdf('blob');
  dl.href = URL.createObjectURL(blob);
  dl.click();                       // saves as Site_Report_2026-08.pdf
</script>
```

That covers `<a download>`, `a.click()`, FileSaver.js / `saveAs()`, `msSaveBlob`,
`window.open(blobUrl)` and `URL.createObjectURL(file)` (a `File` brings its own name).

If you would rather just ask, there is a direct call:

```js
AppMint.downloadFile(base64String, 'Ledger_Q3.csv', 'text/csv');
AppMint.saveBlob(myBlob, 'Backup.json');
AppMint.setDownloadName(objectUrl, 'Invoice.pdf');   // name a URL you built earlier
```

---

## 24. Receive a file the user opened with your app

Turn on **Open with this app** when building. When someone taps a `.gpx`, `.csv`, `.json`
(or shares one to your app), ask for it — **however late your app starts**:

```js
const f = await AppMint.getOpenedFile();     // null if the app was opened normally
if (f) {
  console.log(f.name, f.mimeType, f.size);
  const text = f.text ?? await f.file.text();   // f.file is a real File object
  importActivity(text);
}
```

You can also listen, if you prefer:

```js
window.addEventListener('appmint:fileopen', e => handle(e.detail));
```

There is **no size limit** — a 40 MB GPX arrives in one piece. `f.file` is always the whole
file; `f.text` is filled in for text formats and `f.base64` for small binaries, purely as a
shortcut. `AppMint.getOpenedFiles()` returns everything received so far.

---

## 25. Talk to a Bluetooth sensor

Standard **Web Bluetooth** works in the installed app — the same code you would write for
Chrome. Turn on the Bluetooth permission when building (it turns itself on if your code
mentions `navigator.bluetooth`).

```js
const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['heart_rate'] }]
});
const server  = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate');
const chr     = await service.getCharacteristic('heart_rate_measurement');

chr.addEventListener('characteristicvaluechanged', e => {
  const v = e.target.value;                              // a DataView
  const bpm = (v.getUint8(0) & 1) ? v.getUint16(1, true) : v.getUint8(1);
  document.getElementById('bpm').textContent = bpm;
});
await chr.startNotifications();

device.addEventListener('gattserverdisconnected', () => showReconnectButton());
```

The device chooser is drawn by the app, exactly like the browser's — a page can only reach
a device the user picked. `getDevices()`, `watchAdvertisements()` and `requestLEScan()`
are not available and reject with `NotSupportedError`, so feature-detect before using them.

---

## 26. Handle the phone's Back button yourself

If your app changes screens by **showing and hiding elements** (no URL changes, no
`history.pushState`), the phone's Back button can't see those screens — Android history is
empty, so Back exits the app. Register a back handler and decide yourself:

```js
AppMint.setBackHandler(() => {
  if (isMenuOpen)   { closeMenu();      return true; }  // consumed — stay in the app
  if (currentScreen !== 'home') { goTo('home'); return true; }
  return false;   // nothing open — let the app exit normally (exit dialog if enabled)
});
```

Return `true` to say "I handled it"; return `false` to let the normal behaviour run
(page history back, then the exit confirmation, then exit). You can also listen for the
cancelable `appmint:back` event and call `e.preventDefault()` to consume a press.

Apps that use `history.pushState` for every screen do **not** need this — the Back button
already walks their history. Call `AppMint.setBackHandler(null)` to unregister.

---

## 27. Block screenshots on one screen

```js
function showStatement() {
  render(statementView);
  window.WebToApk?.setSecureScreen(true);
}
```

Screenshots fail, screen recording and casting go black, and the app's preview in the recents
screen is hidden. A camera pointed at the screen still works — nothing on Android stops that.

Protection belongs to the screen that asked for it: the app releases it as soon as you navigate
anywhere else, including a `pushState` or `#hash` route. So there is nothing to clean up on the
way out, and a page has to ask again after a reload.

Call `WebToApk.setSecureScreen(false)` yourself only when you hide the sensitive content
*without* navigating — closing a modal, switching a tab inside one page.

Do **not** re-assert this on a timer. Android does not ask your app when the screenshot buttons
are pressed; the flag has to already be on. A one-second lease is a one-second hole.

---

## 28. A reminder with your own sound, that opens the app

```js
const b = window.WebToApk || null;

// Exact timing and the full-screen alert are both user-controlled — ask, don't assume.
if (b?.getAlarmPrecision() === 'inexact') b?.requestExactAlarms();
if (b?.canOpenAppAtTime() === 'denied')   b?.requestOpenAppPermission();

const ok = b?.scheduleNotificationEx('standup', JSON.stringify({
  at: tomorrowAt9am,          // epoch ms (or inSeconds: 3600)
  repeat: 'daily',            // none | minutely | hourly | daily | weekly
  title: 'Stand-up',
  body: 'Daily sync starts now',
  sound: '/audio/alarm.mp3',  // an audio file YOU shipped in the app
  openApp: true               // put the app on screen, over the lock screen
}));
if (!ok) console.warn('check the sound path — nothing was scheduled');

// The app was opened by the reminder: do the work you could not do while closed.
window.addEventListener('appmint:reminder', (e) => {
  // e.detail = { id: 'standup', overLockScreen: true }
  goTo(e.detail.id);
});
```

The sound file is copied out of your bundle when you schedule, so a wrong path comes back as
`false` immediately instead of turning into the default Android ding days later. `mp3`, `wav`,
`ogg`, `m4a`, `flac` and `opus` all work; `'silent'` means no sound.

`openApp: true` is the *only* way an app gets on screen by itself at a set time. Android has
blocked background app starts since Android 10 — a page cannot arrange to be launched quietly,
and any tutorial saying otherwise predates that change.

---

## 29. Errors you will see

| Error | Meaning | What to do |
|---|---|---|
| `not_enabled` | You did not turn the feature on when building | Turn it on in Step 3 (Permissions) and build again |
| `permission_denied` | The user said No | Explain why you need it, then ask again |
| `cancelled` | The user closed the picker | Do nothing |
| `timeout` | No answer in 15 seconds | Try again |
| `none_enrolled` | No fingerprint saved on the phone | Ask the user to add one in settings |
| `permission_missing` | Permission not in the app | Turn it on when building |

---

## Event names

Use these with `window.addEventListener`.

| Event | Comes from |
|---|---|
| `appmint:contacts` | contacts and contact picker |
| `appmint:calllog` | call log |
| `appmint:sms` | sending or reading SMS |
| `appmint:sms-received` | a new SMS arrived |
| `appmint:notification-action` | user tapped a notification button |
| `appmint:reminder` | a scheduled reminder opened the app (`{id, overLockScreen}`) |
| `appmint:products` | in-app purchase price list |
| `appmint:purchase` | a purchase finished |
| `appmint:reward` | a rewarded ad was watched to the end |
| `appmint:ad-closed` | a full-screen ad closed (`earned` says whether to grant) |
| `appmint:ad-unavailable` | an ad you asked for could not be shown |
| `appmint:fileopen` | a file was opened with, or shared to, your app |
| `appmint:fileopenerror` | that file could not be read (the detail says why) |
| `appmint:ble` | the fitness-sensor bridge (`navigator.bluetooth` uses its own DOM events) |
| `appmint:back` | the phone's Back button (cancelable; fires once a back handler is registered) |
